• 转: 【Java并发编程】之二十:并发新特性—Lock锁和条件变量(含代码)



    简单使用Lock锁

        Java 5中引入了新的锁机制——Java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作。Lock接口有3个实现它的类:ReentrantLock、ReetrantReadWriteLock.ReadLock和ReetrantReadWriteLock.WriteLock,即重入锁、读锁和写锁。lock必须被显式地创建、锁定和释放,为了可以使用更多的功能,一般用ReentrantLock为其实例化。为了保证锁最终一定会被释放(可能会有异常发生),要把互斥区放在try语句块内,并在finally语句块中释放锁,尤其当有return语句时,return语句必须放在try字句中,以确保unlock()不会过早发生,从而将数据暴露给第二个任务。因此,采用lock加锁和释放锁的一般形式如下:

    1. Lock lock = new ReentrantLock();//默认使用非公平锁,如果要使用公平锁,需要传入参数true  
    2. ........  
    3. lock.lock();  
    4. try {  
    5.      //更新对象的状态  
    6.     //捕获异常,必要时恢复到原来的不变约束  
    7.    //如果有return语句,放在这里  
    8.  finally {  
    9.        lock.unlock();        //锁必须在finally块中释放  


    ReetrankLock与synchronized比较


        性能比较

        在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的Lock对象,性能更高一些。Brian Goetz对这两种锁在JDK1.5、单核处理器及双Xeon处理器环境下做了一组吞吐量对比的实验,发现多线程环境下,synchronized的吞吐量下降的非常严重,而ReentrankLock则能基本保持在同一个比较稳定的水平上。但与其说ReetrantLock性能好,倒不如说synchronized还有非常大的优化余地,于是到了JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。


        下面浅析以下两种锁机制的底层的实现策略。

        互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因而这种同步又称为阻塞同步,它属于一种悲观的并发策略,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。synchronized采用的便是这种并发策略。

        随着指令集的发展,我们有了另一种选择:基于冲突检测的乐观并发策略,通俗地讲就是先进性操作,如果没有其他线程争用共享数据,那操作就成功了,如果共享数据被争用,产生了冲突,那就再进行其他的补偿措施(最常见的补偿措施就是不断地重拾,直到试成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步被称为非阻塞同步。ReetrantLock采用的便是这种并发策略。

        在乐观的并发策略中,需要操作和冲突检测这两个步骤具备原子性,它靠硬件指令来保证,这里用的是CAS操作(Compare and Swap)。JDK1.5之后,Java程序才可以使用CAS操作。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState,这里其实就是调用的CPU提供的特殊指令。现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起。

        Java 5中引入了注入AutomicInteger、AutomicLong、AutomicReference等特殊的原子性变量类,它们提供的如:compareAndSet()、incrementAndSet()和getAndIncrement()等方法都使用了CAS操作。因此,它们都是由硬件指令来保证的原子方法。


       用途比较

        基本语法上,ReentrantLock与synchronized很相似,它们都具备一样的线程重入特性,只是代码写法上有点区别而已,一个表现为API层面的互斥锁(Lock),一个表现为原生语法层面的互斥锁(synchronized)。ReentrantLock相对synchronized而言还是增加了一些高级功能,主要有以下三项:

        1、等待可中断:当持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待,改为处理其他事情,它对处理执行时间非常上的同步块很有帮助。而在等待由synchronized产生的互斥锁时,会一直阻塞,是不能被中断的。

        2、可实现公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序排队等待,而非公平锁则不保证这点,在锁释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁时非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过构造方法ReentrantLock(ture)来要求使用公平锁。

        3、锁可以绑定多个条件:ReentrantLock对象可以同时绑定多个Condition对象(名曰:条件变量或条件队列),而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含条件,但如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无需这么做,只需要多次调用newCondition()方法即可。而且我们还可以通过绑定Condition对象来判断当前线程通知的是哪些线程(即与Condition对象绑定在一起的其他线程)。


    可中断锁

        ReetrantLock有两种锁:忽略中断锁和响应中断锁。忽略中断锁与synchronized实现的互斥锁一样,不能响应中断,而响应中断锁可以响应中断。

        如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,如果此时ReetrantLock提供的是忽略中断锁,则它不会去理会该中断,而是让线程B继续等待,而如果此时ReetrantLock提供的是响应中断锁,那么它便会处理中断,让线程B放弃等待,转而去处理其他事情。

      获得响应中断锁的一般形式如下:

    1. ReentrantLock lock = new ReentrantLock();  
    2. ...........  
    3. lock.lockInterruptibly();//获取响应中断锁  
    4. try {  
    5.       //更新对象的状态  
    6.       //捕获异常,必要时恢复到原来的不变约束  
    7.       //如果有return语句,放在这里  
    8. }finally{  
    9.     lock.unlock();        //锁必须在finally块中释放  
    10. }  

        这里有一个不错的分析中断的示例代码(摘自网上)

        当用synchronized中断对互斥锁的等待时,并不起作用,该线程依然会一直等待,如下面的实例:

    1. public class Buffer {  
    2.   
    3.     private Object lock;  
    4.   
    5.     public Buffer() {  
    6.         lock = this;  
    7.     }  
    8.   
    9.     public void write() {  
    10.         synchronized (lock) {  
    11.             long startTime = System.currentTimeMillis();  
    12.             System.out.println("开始往这个buff写入数据…");  
    13.             for (;;)// 模拟要处理很长时间      
    14.             {  
    15.                 if (System.currentTimeMillis()  
    16.                         - startTime > Integer.MAX_VALUE) {  
    17.                     break;  
    18.                 }  
    19.             }  
    20.             System.out.println("终于写完了");  
    21.         }  
    22.     }  
    23.   
    24.     public void read() {  
    25.         synchronized (lock) {  
    26.             System.out.println("从这个buff读数据");  
    27.         }  
    28.     }  
    29.   
    30.     public static void main(String[] args) {  
    31.         Buffer buff = new Buffer();  
    32.   
    33.         final Writer writer = new Writer(buff);  
    34.         final Reader reader = new Reader(buff);  
    35.   
    36.         writer.start();  
    37.         reader.start();  
    38.   
    39.         new Thread(new Runnable() {  
    40.   
    41.             @Override  
    42.             public void run() {  
    43.                 long start = System.currentTimeMillis();  
    44.                 for (;;) {  
    45.                     //等5秒钟去中断读      
    46.                     if (System.currentTimeMillis()  
    47.                             - start > 5000) {  
    48.                         System.out.println("不等了,尝试中断");  
    49.                         reader.interrupt();  //尝试中断读线程  
    50.                         break;  
    51.                     }  
    52.   
    53.                 }  
    54.   
    55.             }  
    56.         }).start();  
    57.         // 我们期待“读”这个线程能退出等待锁,可是事与愿违,一旦读这个线程发现自己得不到锁,  
    58.         // 就一直开始等待了,就算它等死,也得不到锁,因为写线程要21亿秒才能完成 T_T ,即使我们中断它,  
    59.         // 它都不来响应下,看来真的要等死了。这个时候,ReentrantLock给了一种机制让我们来响应中断,  
    60.         // 让“读”能伸能屈,勇敢放弃对这个锁的等待。我们来改写Buffer这个类,就叫BufferInterruptibly吧,可中断缓存。  
    61.     }  
    62. }  
    63.   
    64. class Writer extends Thread {  
    65.   
    66.     private Buffer buff;  
    67.   
    68.     public Writer(Buffer buff) {  
    69.         this.buff = buff;  
    70.     }  
    71.   
    72.     @Override  
    73.     public void run() {  
    74.         buff.write();  
    75.     }  
    76. }  
    77.   
    78. class Reader extends Thread {  
    79.   
    80.     private Buffer buff;  
    81.   
    82.     public Reader(Buffer buff) {  
    83.         this.buff = buff;  
    84.     }  
    85.   
    86.     @Override  
    87.     public void run() {  
    88.   
    89.         buff.read();//这里估计会一直阻塞      
    90.   
    91.         System.out.println("读结束");  
    92.   
    93.     }  
    94. }  

        执行结果如下:

        我们等待了很久,后面依然没有输出,说明读线程对互斥锁的等待并没有被中断,也就是该户吃锁没有响应对读线程的中断。
        我们再将上面代码中synchronized的互斥锁改为ReentrantLock的响应中断锁,即改为如下代码: 
    1. import java.util.concurrent.locks.ReentrantLock;  
    2.   
    3. public class BufferInterruptibly {  
    4.   
    5.     private ReentrantLock lock = new ReentrantLock();  
    6.   
    7.     public void write() {  
    8.         lock.lock();  
    9.         try {  
    10.             long startTime = System.currentTimeMillis();  
    11.             System.out.println("开始往这个buff写入数据…");  
    12.             for (;;)// 模拟要处理很长时间      
    13.             {  
    14.                 if (System.currentTimeMillis()  
    15.                         - startTime > Integer.MAX_VALUE) {  
    16.                     break;  
    17.                 }  
    18.             }  
    19.             System.out.println("终于写完了");  
    20.         } finally {  
    21.             lock.unlock();  
    22.         }  
    23.     }  
    24.   
    25.     public void read() throws InterruptedException {  
    26.         lock.lockInterruptibly();// 注意这里,可以响应中断      
    27.         try {  
    28.             System.out.println("从这个buff读数据");  
    29.         } finally {  
    30.             lock.unlock();  
    31.         }  
    32.     }  
    33.   
    34.     public static void main(String args[]) {  
    35.         BufferInterruptibly buff = new BufferInterruptibly();  
    36.   
    37.         final Writer2 writer = new Writer2(buff);  
    38.         final Reader2 reader = new Reader2(buff);  
    39.   
    40.         writer.start();  
    41.         reader.start();  
    42.   
    43.         new Thread(new Runnable() {  
    44.   
    45.             @Override  
    46.             public void run() {  
    47.                 long start = System.currentTimeMillis();  
    48.                 for (;;) {  
    49.                     if (System.currentTimeMillis()  
    50.                             - start > 5000) {  
    51.                         System.out.println("不等了,尝试中断");  
    52.                         reader.interrupt();  //此处中断读操作  
    53.                         break;  
    54.                     }  
    55.                 }  
    56.             }  
    57.         }).start();  
    58.   
    59.     }  
    60. }  
    61.   
    62. class Reader2 extends Thread {  
    63.   
    64.     private BufferInterruptibly buff;  
    65.   
    66.     public Reader2(BufferInterruptibly buff) {  
    67.         this.buff = buff;  
    68.     }  
    69.   
    70.     @Override  
    71.     public void run() {  
    72.   
    73.         try {  
    74.             buff.read();//可以收到中断的异常,从而有效退出      
    75.         } catch (InterruptedException e) {  
    76.             System.out.println("我不读了");  
    77.         }  
    78.   
    79.         System.out.println("读结束");  
    80.   
    81.     }  
    82. }  
    83.   
    84. class Writer2 extends Thread {  
    85.   
    86.     private BufferInterruptibly buff;  
    87.   
    88.     public Writer2(BufferInterruptibly buff) {  
    89.         this.buff = buff;  
    90.     }  
    91.   
    92.     @Override  
    93.     public void run() {  
    94.         buff.write();  
    95.     }  
    96.       
    97. }  

        执行结果如下:

        从结果中可以看出,尝试中断后输出了catch语句块中的内容,也输出了后面的“读结束”,说明线程对互斥锁的等待被中断了,也就是该互斥锁响应了对读线程的中断。

    条件变量实现线程间协作


        在
        下面将一文中的代码改为用条件变量实现,如下:
    1. import java.util.concurrent.*;  
    2. import java.util.concurrent.locks.*;  
    3.   
    4. class Info{ // 定义信息类  
    5.     private String name = "name";//定义name属性,为了与下面set的name属性区别开  
    6.     private String content = "content" ;// 定义content属性,为了与下面set的content属性区别开  
    7.     private boolean flag = true ;   // 设置标志位,初始时先生产  
    8.     private Lock lock = new ReentrantLock();    
    9.     private Condition condition = lock.newCondition(); //产生一个Condition对象  
    10.     public  void set(String name,String content){  
    11.         lock.lock();  
    12.         try{  
    13.             while(!flag){  
    14.                 condition.await() ;  
    15.             }  
    16.             this.setName(name) ;    // 设置名称  
    17.             Thread.sleep(300) ;  
    18.             this.setContent(content) ;  // 设置内容  
    19.             flag  = false ; // 改变标志位,表示可以取走  
    20.             condition.signal();  
    21.         }catch(InterruptedException e){  
    22.             e.printStackTrace() ;  
    23.         }finally{  
    24.             lock.unlock();  
    25.         }  
    26.     }  
    27.   
    28.     public void get(){  
    29.         lock.lock();  
    30.         try{  
    31.             while(flag){  
    32.                 condition.await() ;  
    33.             }     
    34.             Thread.sleep(300) ;  
    35.             System.out.println(this.getName() +   
    36.                 " --> " + this.getContent()) ;  
    37.             flag  = true ;  // 改变标志位,表示可以生产  
    38.             condition.signal();  
    39.         }catch(InterruptedException e){  
    40.             e.printStackTrace() ;  
    41.         }finally{  
    42.             lock.unlock();  
    43.         }  
    44.     }  
    45.   
    46.     public void setName(String name){  
    47.         this.name = name ;  
    48.     }  
    49.     public void setContent(String content){  
    50.         this.content = content ;  
    51.     }  
    52.     public String getName(){  
    53.         return this.name ;  
    54.     }  
    55.     public String getContent(){  
    56.         return this.content ;  
    57.     }  
    58. }  
    59. class Producer implements Runnable{ // 通过Runnable实现多线程  
    60.     private Info info = null ;      // 保存Info引用  
    61.     public Producer(Info info){  
    62.         this.info = info ;  
    63.     }  
    64.     public void run(){  
    65.         boolean flag = true ;   // 定义标记位  
    66.         for(int i=0;i<10;i++){  
    67.             if(flag){  
    68.                 this.info.set("姓名--1","内容--1") ;    // 设置名称  
    69.                 flag = false ;  
    70.             }else{  
    71.                 this.info.set("姓名--2","内容--2") ;    // 设置名称  
    72.                 flag = true ;  
    73.             }  
    74.         }  
    75.     }  
    76. }  
    77. class Consumer implements Runnable{  
    78.     private Info info = null ;  
    79.     public Consumer(Info info){  
    80.         this.info = info ;  
    81.     }  
    82.     public void run(){  
    83.         for(int i=0;i<10;i++){  
    84.             this.info.get() ;  
    85.         }  
    86.     }  
    87. }  
    88. public class ThreadCaseDemo{  
    89.     public static void main(String args[]){  
    90.         Info info = new Info(); // 实例化Info对象  
    91.         Producer pro = new Producer(info) ; // 生产者  
    92.         Consumer con = new Consumer(info) ; // 消费者  
    93.         new Thread(pro).start() ;  
    94.         //启动了生产者线程后,再启动消费者线程  
    95.         try{  
    96.             Thread.sleep(500) ;  
    97.         }catch(InterruptedException e){  
    98.             e.printStackTrace() ;  
    99.         }  
    100.   
    101.         new Thread(con).start() ;  
    102.     }  
    103. }  
        执行后,同样可以得到如下的结果:
    姓名--1 --> 内容--1
    姓名--2 --> 内容--2
    姓名--1 --> 内容--1
    姓名--2 --> 内容--2
    姓名--1 --> 内容--1
    姓名--2 --> 内容--2
    姓名--1 --> 内容--1
    姓名--2 --> 内容--2
    姓名--1 --> 内容--1
    姓名--2 --> 内容--2
        从以上并不能看出用条件变量的await()、signal()、signalAll()方法比用Object对象的wait()、notify()、notifyAll()方法实现线程间协作有多少优点,但它在处理更复杂的多线程问题时,会有明显的优势。所以,Lock和Condition对象只有在更加困难的多线程问题中才是必须的。
        

    读写锁

        另外,synchronized获取的互斥锁不仅互斥读写操作、写写操作,还互斥读读操作,而读读操作时不会带来数据竞争的,因此对对读读操作也互斥的话,会降低性能。Java 5中提供了读写锁,它将读锁和写锁分离,使得读读操作不互斥,获取读锁和写锁的一般形式如下:

    1. ReadWriteLock rwl = new ReentrantReadWriteLock();      
    2. rwl.writeLock().lock()  //获取写锁  
    3. rwl.readLock().lock()  //获取读锁  

       用读锁来锁定读操作,用写锁来锁定写操作,这样写操作和写操作之间会互斥,读操作和写操作之间会互斥,但读操作和读操作就不会互斥。

       《Java并发编程实践》一书给出了使用 ReentrantLock的最佳时机:

        当你需要以下高级特性时,才应该使用:可定时的、可轮询的与可中断的锁获取操作,公平队列,或者非块结构的锁。否则,请使用synchronized。

  • 相关阅读:
    Score, ACM/ICPC Seoul 2005, UVa 1585
    Score, ACM/ICPC Seoul 2005, UVa 1585
    Molar Mass, ACM/ICPC Seoul 2007, UVa 1586
    Molar Mass, ACM/ICPC Seoul 2007, UVa 1586
    Digit Counting, ACM/ICPC Danang 2007, UVa 1225
    Digit Counting, ACM/ICPC Danang 2007, UVa 1225
    Periodic Strigs, UVa455
    【开发技术】java+mysql 更改表字段的步骤
    【编程技巧】JAVA读取url地址中的文本内容
    【问题解决】java中为什么不建议使用DataInputStream 的readLine()方法
  • 原文地址:https://www.cnblogs.com/xuyatao/p/6919474.html
Copyright © 2020-2023  润新知