Java线程并发之锁

作者: 扈扈哈嘿 | 来源:发表于2017-01-04 11:43 被阅读355次

     既然java内置了synchronized,为什么还要出现lock呢?
     由于synchronized的并发是阻塞的。当一个线程获得了锁,并执行其代码时,其它线程便只能等待锁的释放。
     在这里要释放锁有如下情况:

    • 正常情况下,当代码执行完毕会释放锁。
    • 当线程执行发生异常,JVM会让线程自动释放锁。

     那如果线程内部由于什么原因(IO操作,sleep())阻塞了,又没有抛出异常,那其它钱程只能干等着。
     那我们就要想办法可以中断线程的等待,这就出现了lock。
    lock可以知道是否获取到了锁,而synchronized不能。
    也就是说lock比synchronized有更多的功能。
     注意一下下面两点:

    • synchronized是java语言的关键字,是java的内置特性,而lock只是一个类。
    • synchronzied是系统释放锁,而不需要手动地去释放锁。但是lock需要手动的调用unlock来释放锁,否则会出现死锁。

    Lock

    查看源码Lock是一个接口,它有如下方法:

    public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throwsInterruptedException;   
    void unlock();
    Condition newCondition();
    }
    

    从方法名字可以看出,lock(),tryLock(),tryLock(long time,TimeUnit unit),lockInterruptibly()是用来获取锁的。

    lock()

     此方法就是用来获取一个锁的,如果锁已被别的线程获得,那就进行等待。
     前面讲到必须使用lock必须手动释放锁,并且在线程发生异常时,系统也不会自动释放的,所以我们就应该把任务代码放在try{}catch(){}中执行最后 在finally{}代码里面释放:

    lock.lock();
    try{
    }catch(){
    }finally{
    lock.unlock()
    }
    

    tryLock()&tryLock(long time,TimeUnit unit)

     这个方法是有返回值的,它用来尝试获取锁,如果获取成功则返回true,反之则返回false,这个方法会立即返回,并不会等待。
    tryLock(long time,TimeUnit unit)和tryLock类似,只是前者如果一开始没有获取到锁会等待一段时间。如果在时间范围内拿到了锁就会返回true。
     使用格式:

    if(lock.tryLock()) {
    try{   
    //处理任务
    }catch(Exception ex){
    }finally{
    lock.unlock();   //释放锁  
    }
    }else {
    //如果不能获取锁,则直接做其他事情
    }
    

    lockInterruptibly()

     这个方法比较特殊。当通过这个方法获取锁时,如果此钱程正在等待获取锁,则这个钱程可以响应等待中断,即中断钱程等待状态。举个粟子,当A,B线程都试图使用lockInterruptibly()获取锁时,如果A获得了锁,B线程正在等待获取锁,则可以调用threadB.interrupt()能够中断线程B的等待。
    因为该方法抛出了异常。所以应该放在try代码块或者在方法申明时抛出异常。
     一般使用格式:

    try{
    lock.lockInterruptibly();
    }catch(InterruptedException  e){
    }finally{
    lock.unlock();
    }
    

     注意的是,如果线程已经获得了锁是不会中断的。并且正在执行的线程是不会被中断的,只有在阻塞中的线程才会被中断。
    而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

    newCondition()

     此方法返回一个Condition 对象。以ReentrantLock(稍后介绍)为例。一个ReentrantLock对象可以有多个Condition对象。Condition是为解决Object.wait/notify/notifyAll难以使用的问题。下面就简单说一下Condition。
     ynchronized锁配合的线程等待(Object.wait)与线程通知(Object.notify),那么对于JDK1.5 的 java.util.concurrent.locks.ReentrantLock 锁,JDK也为我们提供了与此功能相应的java.util.concurrent.locks.Condition。
     在Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),传统线程的通信方式,Condition都可以实现,这里注意,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。接口方法名称的改变是为了区分Object中的方法,因为Condition也是Object的子类。
     在调用await()方法前线程必须获得重入锁,调用await()方法后线程会释放当前占用的锁。同理在调用signal()方法时当前线程也必须获得相应重入锁,调用signal()方法后系统会从condition.await()等待队列中唤醒一个线程。当线程被唤醒后,它就会尝试重新获得与之绑定的重入锁,一旦获取成功将继续执行。所以调用signal()方法后一定要释放当前占用的锁,这样被唤醒的线程才能有获得锁的机会,才能继续执行。
    Condition接口中的方法:

    void await() throws InterruptedException;  
    void awaitUninterruptibly();  
    long awaitNanos(long nanosTimeout) throws InterruptedException;  
    boolean await(long time, TimeUnit unit) throws InterruptedException;  
    boolean awaitUntil(Date deadline) throws InterruptedException;  
    void signal();  
    void signalAll();  
    

     Condition是与Lock绑定的,所以也有Lock的公平性:如果是公平锁,线程为按照FIFO的顺序从Condition.await中释放,如果是非公平锁,那么后续的锁竞争就不保证FIFO顺序了。
     一个使用Condition实现生产者消费者的例子:

    final Lock lock = new ReentrantLock();//锁对象  
       final Condition notFull  = lock.newCondition();//写线程条件   
       final Condition notEmpty = lock.newCondition();//读线程条件   
      
       final Object[] items = new Object[100];//缓存队列  
       int putptr/*写索引*/, takeptr/*读索引*/, count/*队列中存在的数据个数*/;  
      
       public void put(Object x) throws InterruptedException {  
         lock.lock();  
         try {  
           while (count == items.length)//如果队列满了   
             notFull.await();//阻塞写线程  
           items[putptr] = x;//赋值   
           if (++putptr == items.length) putptr = 0;//如果写索引写到队列的最后一个位置了,那么置为0  
           ++count;//个数++  
           notEmpty.signal();//唤醒读线程  
         } finally {  
           lock.unlock();  
         }  
       }  
      
       public Object take() throws InterruptedException {  
         lock.lock();  
         try {  
           while (count == 0)//如果队列为空  
             notEmpty.await();//阻塞读线程  
           Object x = items[takeptr];//取值   
           if (++takeptr == items.length) takeptr = 0;//如果读索引读到队列的最后一个位置了,那么置为0  
           --count;//个数--  
           notFull.signal();//唤醒写线程  
           return x;  
         } finally {  
           lock.unlock();  
         }  
       }   
    

    ReentrantLock

     ReentrantLock 类实现了 Lock,它拥有与 synchronized相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。这意味着当许多线程都在争用同一个锁时,使用 ReentrantLock 的总体开支通常要比 synchronized少得多。
     ReentrantLock构造器的一个参数是 boolean 值,它允许您选择想要一个 公平(fair)锁,还是一个 不公平(unfair)锁。公平锁使线程按照请求锁的顺序依次获得锁;而不公平锁则允许讨价还价,在这种情况下,线程有时可以比先请求锁的其他线程先得到锁。当然,公平总是好的,但是你需要保证它公平那你就需要牺牲性能。花掉性能成本来保证它是公平锁。作为默认设置,除非公平对您的算法至关重要,需要严格按照线程排队的顺序对其进行服务。否则我们都应该把公平设置为 false,默认的无参构造函数就是非公平锁。
     传统的synchronized是不公平的,而且永远都不公平。但是没有人抱怨过线程饥渴,因为 JVM 保证了所有线程最终都会得到它们所等候的锁。确保统计上的公平性,对多数情况来说,这就已经足够了,而这花费的成本则要比绝对的公平保证的低得多。
     从ReentrantLock的名字知道,它是一个可重入锁。也就是说支持同一个线程对同一资源的重复加锁。另外synchronized关键字隐式的支持重进入。
    一个小demo:

    public class ReentrantLockTest { 
       private Lock lock = new ReentrantLock();  
      private int count;  
      public static void main(String[] args) { 
       ReentrantLockTest test = new ReentrantLockTest();   
         for (int i = 0; i < 3; i++) {   
          new Thread() {            
        @Override       
       public void run() {  
             test.test(this);   
     }  
    }.start();  
          } 
      }    
    protected void test(Thread thread) {   
         lock.lock();    
        try {           
     for (int i = 0; i < 5; i++) {     
               count++;       
             System.out.println("当前线程" + thread.getName() + "之后" + count);    
            }      
      } finally {  
              lock.unlock();       
     }  
      }}
    

     还有为保证读写效率提高的读写锁:ReentrantReadWriteLock ,它与ReentrantLock是相互独立的实现。没有继承和实现关系。这里就不多说,可以看看其它资料。

    ReentrantLock与synchronized的区别

     算是总结吧。

    • 与synchronized相比,ReentrantLock提供了更多,更加全面的功能,具备更强的扩展性。例如:时间锁等候,可中断锁等候,锁投票。
    • ReentrantLock还提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活,所以在多个条件变量和高度竞争锁的地方,ReentrantLock更加适合(以后会阐述Condition)。
    • ReentrantLock提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而synchronized则一旦进入锁请求要么成功要么阻塞,所以相比synchronized而言,ReentrantLock会不容易产生死锁些。
    • ReentrantLock支持更加灵活的同步代码块,但是使用synchronized时,只能在同一个synchronized块结构中获取和释放。注:ReentrantLock的锁释放一定要在finally中处理,否则可能会产生严重的后果。
    • ReentrantLock支持中断处理,且性能较synchronized会好些。

     看起来ReentrantLock功能比synchronized强大得多,那是不是我们以后在做线程同步的时候都应该用ReentrantLock而抛弃synchronized呢?当然不是,前面 我们说过使用Lock的时候,我们必须在finally块中手动调用lock.unlock()。如果我们忘记了,那么会为程序埋下很大的隐患。还有一个原因就是当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。 Lock类只是普通的类,JVM 不知道具体哪个线程拥有 Lock对象。而且,几乎每个开发人员都熟悉 synchronized,它可以在 JVM 的所有版本中工作。在 JDK 5.0 成为标准(从现在开始可能需要两年)之前,使用 Lock类将意味着要利用的特性不是每个 JVM 都有的,而且不是每个开发人员都熟悉的。
     那我们什么时候应该使用Lock而不是synchronized呢?既然如此,我们什么时候才应该使用 ReentrantLock 呢?答案非常简单 —— 在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。 ReentrantLock还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。我建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用 ReentrantLock “性能会更好”。请记住,这些是供高级用户使用的高级工具。(而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。)。一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。

    相关文章

      网友评论

        本文标题:Java线程并发之锁

        本文链接:https://www.haomeiwen.com/subject/sflmvttx.html