【Java并发编程】之十一=十二-13-14-15-16-17--18-19-20-21-22-二十三

来源:互联网 发布:vm虚拟机破解版 mac 编辑:程序博客网 时间:2024/06/17 19:33

【Java并发编程】之十一:线程间通信中notify通知的遗漏(含代码)


notify通知的遗漏很容易理解,即threadA还没开始wait的时候,threadB已经notify了,这样,threadB通知是没有任何响应的,当threadB退出synchronized代码块后,threadA再开始wait,便会一直阻塞等待,直到被别的线程打断。

 

遗漏通知的代码

    下面给出一段代码演示通知是如何遗漏的,如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public class MissedNotify extends Object {  
  2.     private Object proceedLock;  
  3.   
  4.     public MissedNotify() {  
  5.         print("in MissedNotify()");  
  6.         proceedLock = new Object();  
  7.     }  
  8.   
  9.     public void waitToProceed() throws InterruptedException {  
  10.         print("in waitToProceed() - entered");  
  11.   
  12.         synchronized ( proceedLock ) {  
  13.             print("in waitToProceed() - about to wait()");  
  14.             proceedLock.wait();  
  15.             print("in waitToProceed() - back from wait()");  
  16.         }  
  17.   
  18.         print("in waitToProceed() - leaving");  
  19.     }  
  20.   
  21.     public void proceed() {  
  22.         print("in proceed() - entered");  
  23.   
  24.         synchronized ( proceedLock ) {  
  25.             print("in proceed() - about to notifyAll()");  
  26.             proceedLock.notifyAll();  
  27.             print("in proceed() - back from notifyAll()");  
  28.         }  
  29.   
  30.         print("in proceed() - leaving");  
  31.     }  
  32.   
  33.     private static void print(String msg) {  
  34.         String name = Thread.currentThread().getName();  
  35.         System.out.println(name + ": " + msg);  
  36.     }  
  37.   
  38.     public static void main(String[] args) {  
  39.         final MissedNotify mn = new MissedNotify();  
  40.   
  41.         Runnable runA = new Runnable() {  
  42.                 public void run() {  
  43.                     try {  
  44.                         //休眠1000ms,大于runB中的500ms,  
  45.                         //是为了后调用waitToProceed,从而先notifyAll,后wait,  
  46.                         //从而造成通知的遗漏  
  47.                         Thread.sleep(1000);  
  48.                         mn.waitToProceed();  
  49.                     } catch ( InterruptedException x ) {  
  50.                         x.printStackTrace();  
  51.                     }  
  52.                 }  
  53.             };  
  54.   
  55.         Thread threadA = new Thread(runA, "threadA");  
  56.         threadA.start();  
  57.   
  58.         Runnable runB = new Runnable() {  
  59.                 public void run() {  
  60.                     try {  
  61.                         //休眠500ms,小于runA中的1000ms,  
  62.                         //是为了先调用proceed,从而先notifyAll,后wait,  
  63.                         //从而造成通知的遗漏  
  64.                         Thread.sleep(500);  
  65.                         mn.proceed();  
  66.                     } catch ( InterruptedException x ) {  
  67.                         x.printStackTrace();  
  68.                     }  
  69.                 }  
  70.             };  
  71.   
  72.         Thread threadB = new Thread(runB, "threadB");  
  73.         threadB.start();  
  74.   
  75.         try {   
  76.             Thread.sleep(10000);  
  77.         } catch ( InterruptedException x ) {}  
  78.   
  79.         //试图打断wait阻塞  
  80.         print("about to invoke interrupt() on threadA");  
  81.         threadA.interrupt();  
  82.     }  
  83. }  


    执行结果如下:

 

    分析:由于threadB在执行mn.proceed()之前只休眠了500ms,而threadA在执行mn.waitToProceed()之前休眠了1000ms,因此,threadB会先苏醒,继而执行mn.proceed(),获取到proceedLock的对象锁,继而执行其中的notifyAll(),当退出proceed()方法中的synchronized代码块时,threadA才有机会获取proceedLock的对象锁,继而执行其中的wait()方法,但此时notifyAll()方法已经执行完毕,threadA便漏掉了threadB的通知,便会阻塞下去。后面主线程休眠10秒后,尝试中断threadA线程,使其抛出InterruptedException。

   

修正后的代码

    为了修正MissedNotify,需要添加一个boolean指示变量,该变量只能在同步代码块内部访问和修改。修改后的代码如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public class MissedNotifyFix extends Object {  
  2.     private Object proceedLock;  
  3.     //该标志位用来指示线程是否需要等待  
  4.     private boolean okToProceed;  
  5.   
  6.     public MissedNotifyFix() {  
  7.         print("in MissedNotify()");  
  8.         proceedLock = new Object();  
  9.         //先设置为false  
  10.         okToProceed = false;  
  11.     }  
  12.   
  13.     public void waitToProceed() throws InterruptedException {  
  14.         print("in waitToProceed() - entered");  
  15.   
  16.         synchronized ( proceedLock ) {  
  17.             print("in waitToProceed() - entered sync block");  
  18.             //while循环判断,这里不用if的原因是为了防止早期通知  
  19.             while ( okToProceed == false ) {  
  20.                 print("in waitToProceed() - about to wait()");  
  21.                 proceedLock.wait();  
  22.                 print("in waitToProceed() - back from wait()");  
  23.             }  
  24.   
  25.             print("in waitToProceed() - leaving sync block");  
  26.         }  
  27.   
  28.         print("in waitToProceed() - leaving");  
  29.     }  
  30.   
  31.     public void proceed() {  
  32.         print("in proceed() - entered");  
  33.   
  34.         synchronized ( proceedLock ) {  
  35.             print("in proceed() - entered sync block");  
  36.             //通知之前,将其设置为true,这样即使出现通知遗漏的情况,也不会使线程在wait出阻塞  
  37.             okToProceed = true;  
  38.             print("in proceed() - changed okToProceed to true");  
  39.             proceedLock.notifyAll();  
  40.             print("in proceed() - just did notifyAll()");  
  41.   
  42.             print("in proceed() - leaving sync block");  
  43.         }  
  44.   
  45.         print("in proceed() - leaving");  
  46.     }  
  47.   
  48.     private static void print(String msg) {  
  49.         String name = Thread.currentThread().getName();  
  50.         System.out.println(name + ": " + msg);  
  51.     }  
  52.   
  53.     public static void main(String[] args) {  
  54.         final MissedNotifyFix mnf = new MissedNotifyFix();  
  55.   
  56.         Runnable runA = new Runnable() {  
  57.                 public void run() {  
  58.                     try {  
  59.                         //休眠1000ms,大于runB中的500ms,  
  60.                         //是为了后调用waitToProceed,从而先notifyAll,后wait,  
  61.                         Thread.sleep(1000);  
  62.                         mnf.waitToProceed();  
  63.                     } catch ( InterruptedException x ) {  
  64.                         x.printStackTrace();  
  65.                     }  
  66.                 }  
  67.             };  
  68.   
  69.         Thread threadA = new Thread(runA, "threadA");  
  70.         threadA.start();  
  71.   
  72.         Runnable runB = new Runnable() {  
  73.                 public void run() {  
  74.                     try {  
  75.                         //休眠500ms,小于runA中的1000ms,  
  76.                         //是为了先调用proceed,从而先notifyAll,后wait,  
  77.                         Thread.sleep(500);  
  78.                         mnf.proceed();  
  79.                     } catch ( InterruptedException x ) {  
  80.                         x.printStackTrace();  
  81.                     }  
  82.                 }  
  83.             };  
  84.   
  85.         Thread threadB = new Thread(runB, "threadB");  
  86.         threadB.start();  
  87.   
  88.         try {   
  89.             Thread.sleep(10000);  
  90.         } catch ( InterruptedException x ) {}  
  91.   
  92.         print("about to invoke interrupt() on threadA");  
  93.         threadA.interrupt();  
  94.     }  
  95. }  

    执行结果如下:

    注意代码中加了注释的部分,在threadB进行通知之前,先将okToProceed置为true,这样如果threadA将通知遗漏,那么就不会进入while循环,也便不会执行wait方法,线程也就不会阻塞。如果通知没有被遗漏,wait方法返回后,okToProceed已经被置为true,下次while循环判断条件不成立,便会退出循环。

    这样,通过标志位和wait、notifyAll的配合使用,便避免了通知遗漏而造成的阻塞问题。

 

   总结:在使用线程的等待/通知机制时,一般都要配合一个boolean变量值(或者其他能够判断真假的条件),在notify之前改变该boolean变量的值,让wait返回后能够退出while循环(一般都要在wait方法外围加一层while循环,以防止早期通知),或在通知被遗漏后,不会被阻塞在wait方法处。这样便保证了程序的正确性。

【Java并发编程】之十二:线程间通信中notifyAll造成的早期通知问题(含代码)


如果线程在等待时接到通知,但线程等待的条件还不满足,此时,线程接到的就是早期通知,如果条件满足的时间很短,但很快又改变了,而变得不再满足,这时也将发生早期通知。这种现象听起来很奇怪,下面通过一个示例程序来说明问题。

    很简单,两个线程等待删除List中的元素,同时另外一个线程正要向其中添加项目。代码如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. import java.util.*;  
  2.   
  3. public class EarlyNotify extends Object {  
  4.     private List list;  
  5.   
  6.     public EarlyNotify() {  
  7.         list = Collections.synchronizedList(new LinkedList());  
  8.     }  
  9.   
  10.     public String removeItem() throws InterruptedException {  
  11.         print("in removeItem() - entering");  
  12.   
  13.         synchronized ( list ) {  
  14.             if ( list.isEmpty() ) {  //这里用if语句会发生危险  
  15.                 print("in removeItem() - about to wait()");  
  16.                 list.wait();  
  17.                 print("in removeItem() - done with wait()");  
  18.             }  
  19.   
  20.             //删除元素  
  21.             String item = (String) list.remove(0);  
  22.   
  23.             print("in removeItem() - leaving");  
  24.             return item;  
  25.         }  
  26.     }  
  27.   
  28.     public void addItem(String item) {  
  29.         print("in addItem() - entering");  
  30.         synchronized ( list ) {  
  31.             //添加元素  
  32.             list.add(item);  
  33.             print("in addItem() - just added: '" + item + "'");  
  34.   
  35.             //添加后,通知所有线程  
  36.             list.notifyAll();  
  37.             print("in addItem() - just notified");  
  38.         }  
  39.         print("in addItem() - leaving");  
  40.     }  
  41.   
  42.     private static void print(String msg) {  
  43.         String name = Thread.currentThread().getName();  
  44.         System.out.println(name + ": " + msg);  
  45.     }  
  46.   
  47.     public static void main(String[] args) {  
  48.         final EarlyNotify en = new EarlyNotify();  
  49.   
  50.         Runnable runA = new Runnable() {  
  51.                 public void run() {  
  52.                     try {  
  53.                         String item = en.removeItem();  
  54.                         print("in run() - returned: '" +   
  55.                                 item + "'");  
  56.                     } catch ( InterruptedException ix ) {  
  57.                         print("interrupted!");  
  58.                     } catch ( Exception x ) {  
  59.                         print("threw an Exception!!!\n" + x);  
  60.                     }  
  61.                 }  
  62.             };  
  63.   
  64.         Runnable runB = new Runnable() {  
  65.                 public void run() {  
  66.                     en.addItem("Hello!");  
  67.                 }  
  68.             };  
  69.   
  70.         try {  
  71.             //启动第一个删除元素的线程  
  72.             Thread threadA1 = new Thread(runA, "threadA1");  
  73.             threadA1.start();  
  74.   
  75.             Thread.sleep(500);  
  76.       
  77.             //启动第二个删除元素的线程  
  78.             Thread threadA2 = new Thread(runA, "threadA2");  
  79.             threadA2.start();  
  80.   
  81.             Thread.sleep(500);  
  82.             //启动增加元素的线程  
  83.             Thread threadB = new Thread(runB, "threadB");  
  84.             threadB.start();  
  85.   
  86.             Thread.sleep(10000); // wait 10 seconds  
  87.   
  88.             threadA1.interrupt();  
  89.             threadA2.interrupt();  
  90.         } catch ( InterruptedException x ) {}  
  91.     }  
  92. }  

    执行结果如下:

    

     分析:首先启动threadA1,threadA1在removeItem()中调用wait(),从而释放list上的对象锁。再过5

【Java并发编程】之十八:第五篇中volatile意外问题的正确分析解答(含代码)

标签: JAVAvolatile可见
 65人阅读 评论(0) 收藏 举报
 分类:
 

《Java并发编程学习笔记之五:volatile变量修饰符—意料之外的问题》一文中遗留了一个问题,就是volatile只修饰了missedIt变量,而没修饰value变量,但是在线程读取value的值的时候,也读到的是最新的数据。但是在网上查了很多资料都无果,看来很多人对volatile的规则并不是太清晰,或者说只停留在很表面的层次,一知半解。

    这两天看《深入Java虚拟机——JVM高级特性与最佳实践》第12章:Java内存模型与线程,并在网上查阅了Java内存模型相关资料,学到了不少东西,尤其在看这篇文章的volatile部分的讲解之后,算是确定了问题出现的原因。

    首先明确一点:假如有两个线程分别读写volatile变量时,线程A写入了某volatile变量,线程B在读取该volatile变量时,便能看到线程A对该volatile变量的写入操作,关键在这里,它不仅会看到对该volatile变量的写入操作,A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,都将立即变得对B线程可见。

   回过头来看文章中出现的问题,由于程序中volatile变量missedIt的写入操作在value变量写入操作之后,而且根据volatile规则,又不能重排序,因此,在线程B读取由线程A改变后的missedIt之后,它之前的value变量在线程A的改变也对线程B变得可见了。

     我们颠倒一下value=50和missedIt=true这两行代码试下,即missedIt=true在前,value=50在后,这样便会得到我们想要的结果:value值的改变不会被看到。

    这应该是JDK1.2之后对volatile规则做了一些修订的结果。


    修改后的代码如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public class Volatile extends Object implements Runnable {  
  2.     //value变量没有被标记为volatile  
  3.     private int value;    
  4.     //missedIt变量被标记为volatile  
  5.     private volatile boolean missedIt;  
  6.     //creationTime不需要声明为volatile,因为代码执行中它没有发生变化  
  7.     private long creationTime;   
  8.   
  9.     public Volatile() {  
  10.         value = 10;  
  11.         missedIt = false;  
  12.         //获取当前时间,亦即调用Volatile构造函数时的时间  
  13.         creationTime = System.currentTimeMillis();  
  14.     }  
  15.   
  16.     public void run() {  
  17.         print("entering run()");  
  18.   
  19.         //循环检查value的值是否不同  
  20.         while ( value < 20 ) {  
  21.             //如果missedIt的值被修改为true,则通过break退出循环  
  22.             if  ( missedIt ) {  
  23.                 //进入同步代码块前,将value的值赋给currValue  
  24.                 int currValue = value;  
  25.                 //在一个任意对象上执行同步语句,目的是为了让该线程在进入和离开同步代码块时,  
  26.                 //将该线程中的所有变量的私有拷贝与共享内存中的原始值进行比较,  
  27.                 //从而发现没有用volatile标记的变量所发生的变化  
  28.                 Object lock = new Object();  
  29.                 synchronized ( lock ) {  
  30.                     //不做任何事  
  31.                 }  
  32.                 //离开同步代码块后,将此时value的值赋给valueAfterSync  
  33.                 int valueAfterSync = value;  
  34.                 print("in run() - see value=" + currValue +", but rumor has it that it changed!");  
  35.                 print("in run() - valueAfterSync=" + valueAfterSync);  
  36.                 break;   
  37.             }  
  38.         }  
  39.         print("leaving run()");  
  40.     }  
  41.   
  42.     public void workMethod() throws InterruptedException {  
  43.         print("entering workMethod()");  
  44.         print("in workMethod() - about to sleep for 2 seconds");  
  45.         Thread.sleep(2000);  
  46.         //仅在此改变value的值  
  47.         missedIt = true;  
  48. //      value = 50;  
  49.         print("in workMethod() - just set value=" + value);  
  50.         print("in workMethod() - about to sleep for 5 seconds");  
  51.         Thread.sleep(5000);  
  52.         //仅在此改变missedIt的值  
  53. //      missedIt = true;  
  54.         value = 50;  
  55.         print("in workMethod() - just set missedIt=" + missedIt);  
  56.         print("in workMethod() - about to sleep for 3 seconds");  
  57.         Thread.sleep(3000);  
  58.         print("leaving workMethod()");  
  59.     }  
  60.   
  61. /* 
  62. *该方法的功能是在要打印的msg信息前打印出程序执行到此所化去的时间,以及打印msg的代码所在的线程 
  63. */  
  64.     private void print(String msg) {  
  65.         //使用java.text包的功能,可以简化这个方法,但是这里没有利用这一点  
  66.         long interval = System.currentTimeMillis() - creationTime;  
  67.         String tmpStr = "    " + ( interval / 1000.0 ) + "000";       
  68.         int pos = tmpStr.indexOf(".");  
  69.         String secStr = tmpStr.substring(pos - 2, pos + 4);  
  70.         String nameStr = "        " + Thread.currentThread().getName();  
  71.         nameStr = nameStr.substring(nameStr.length() - 8, nameStr.length());      
  72.         System.out.println(secStr + " " + nameStr + ": " + msg);  
  73.     }  
  74.   
  75.     public static void main(String[] args) {  
  76.         try {  
  77.             //通过该构造函数可以获取实时时钟的当前时间  
  78.             Volatile vol = new Volatile();  
  79.   
  80.             //稍停100ms,以让实时时钟稍稍超前获取时间,使print()中创建的消息打印的时间值大于0  
  81.             Thread.sleep(100);    
  82.   
  83.             Thread t = new Thread(vol);  
  84.             t.start();  
  85.   
  86.             //休眠100ms,让刚刚启动的线程有时间运行  
  87.             Thread.sleep(100);    
  88.             //workMethod方法在main线程中运行  
  89.             vol.workMethod();  
  90.         } catch ( InterruptedException x ) {  
  91.             System.err.println("one of the sleeps was interrupted");  
  92.         }  
  93.     }  
  94. }  
    执行结果如下:


   很明显,这其实并不符合使用volatile的第二个条件:该变量要没有包含在具有其他变量的不变式中。因此,在这里使用volatile是不安全的。

00ms,启动threadA2,threadA2调用removeItem(),获取list上的对象锁,也发现列表为空,从而在wait()方法处阻塞,释放list上的对象锁。再过500ms后,启动threadB,并调用addItem,获得list上的对象锁,并在list中添加一个元素,同时用notifyAll通知所有线程。

    threadA1和threadA2都从wait()返回,等待获取list对象上的对象锁,并试图从列表中删除添加的元素,这就会产生麻烦,只有其中一个操作能成功。假设threadA1获取了list上的对象锁,并删除元素成功,在退出synchronized代码块时,它便会释放list上的对象锁,此时threadA2便会获取list上的对象锁,会继续删除list中的元素,但是list已经为空了,这便会抛出IndexOutOfBoundsException。

 

    要避免以上问题只需将wait外围的if语句改为while循环即可,这样当list为空时,线程便会继续等待,而不会继续去执行删除list中元素的代码。

    修改后的执行结果如下:

 

     总结:在使用线程的等待/通知机制时,一般都要在while循环中调用wait()方法,满足条件时,才让while循环退出,这样一般也要配合使用一个boolean变量(或其他能判断真假的条件,如本文中的list.isEmpty()),满足while循环的条件时,进入while循环,执行wait()方法,不满足while循环的条件时,跳出循环,执行后面的代码。

【Java并发编程】之十三:生产者—消费者模型(含代码)

生产者消费者问题是线程模型中的经典问题:生产者和消费者在同一时间段内共用同一存储空间,生产者向空间里生产数据,而消费者取走数据。

    这里实现如下情况的生产--消费模型:

    生产者不断交替地生产两组数据“姓名--1 --> 内容--1”,“姓名--2--> 内容--2”,消费者不断交替地取得这两组数据,这里的“姓名--1”和“姓名--2”模拟为数据的名称,“内容--1 ”和“内容--2 ”模拟为数据的内容。

     由于本程序中牵扯到线程运行的不确定性,因此可能会出现以下问题:

     1、假设生产者线程刚向数据存储空间添加了数据的名称,还没有加入该信息的内容,程序就切换到了消费者线程,消费者线程将把信息的名称和上一个信息的内容联系在一起;

     2、生产者生产了若干次数据,消费者才开始取数据,或者是,消费者取完一次数据后,还没等生产者放入新的数据,又重复取出了已取过的数据。

 

     问题1很明显要靠同步来解决,问题2则需要线程间通信,生产者线程放入数据后,通知消费者线程取出数据,消费者线程取出数据后,通知生产者线程生产数据,这里用wait/notify机制来实现。

 

     详细的实现代码如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. class Info{ // 定义信息类  
  2.     private String name = "name";//定义name属性,为了与下面set的name属性区别开  
  3.     private String content = "content" ;// 定义content属性,为了与下面set的content属性区别开  
  4.     private boolean flag = true ;   // 设置标志位,初始时先生产  
  5.     public synchronized void set(String name,String content){  
  6.         while(!flag){  
  7.             try{  
  8.                 super.wait() ;  
  9.             }catch(InterruptedException e){  
  10.                 e.printStackTrace() ;  
  11.             }  
  12.         }  
  13.         this.setName(name) ;    // 设置名称  
  14.         try{  
  15.             Thread.sleep(300) ;  
  16.         }catch(InterruptedException e){  
  17.             e.printStackTrace() ;  
  18.         }  
  19.         this.setContent(content) ;  // 设置内容  
  20.         flag  = false ; // 改变标志位,表示可以取走  
  21.         super.notify();  
  22.     }  
  23.     public synchronized void get(){  
  24.         while(flag){  
  25.             try{  
  26.                 super.wait() ;  
  27.             }catch(InterruptedException e){  
  28.                 e.printStackTrace() ;  
  29.             }  
  30.         }  
  31.         try{  
  32.             Thread.sleep(300) ;  
  33.         }catch(InterruptedException e){  
  34.             e.printStackTrace() ;  
  35.         }  
  36.         System.out.println(this.getName() +   
  37.             " --> " + this.getContent()) ;  
  38.         flag  = true ;  // 改变标志位,表示可以生产  
  39.         super.notify();  
  40.     }  
  41.     public void setName(String name){  
  42.         this.name = name ;  
  43.     }  
  44.     public void setContent(String content){  
  45.         this.content = content ;  
  46.     }  
  47.     public String getName(){  
  48.         return this.name ;  
  49.     }  
  50.     public String getContent(){  
  51.         return this.content ;  
  52.     }  
  53. }  
  54. class Producer implements Runnable{ // 通过Runnable实现多线程  
  55.     private Info info = null ;      // 保存Info引用  
  56.     public Producer(Info info){  
  57.         this.info = info ;  
  58.     }  
  59.     public void run(){  
  60.         boolean flag = true ;   // 定义标记位  
  61.         for(int i=0;i<10;i++){  
  62.             if(flag){  
  63.                 this.info.set("姓名--1","内容--1") ;    // 设置名称  
  64.                 flag = false ;  
  65.             }else{  
  66.                 this.info.set("姓名--2","内容--2") ;    // 设置名称  
  67.                 flag = true ;  
  68.             }  
  69.         }  
  70.     }  
  71. }  
  72. class Consumer implements Runnable{  
  73.     private Info info = null ;  
  74.     public Consumer(Info info){  
  75.         this.info = info ;  
  76.     }  
  77.     public void run(){  
  78.         for(int i=0;i<10;i++){  
  79.             this.info.get() ;  
  80.         }  
  81.     }  
  82. }  
  83. public class ThreadCaseDemo03{  
  84.     public static void main(String args[]){  
  85.         Info info = new Info(); // 实例化Info对象  
  86.         Producer pro = new Producer(info) ; // 生产者  
  87.         Consumer con = new Consumer(info) ; // 消费者  
  88.         new Thread(pro).start() ;  
  89.         //启动了生产者线程后,再启动消费者线程  
  90.         try{  
  91.             Thread.sleep(500) ;  
  92.         }catch(InterruptedException e){  
  93.             e.printStackTrace() ;  
  94.         }  
  95.   
  96.         new Thread(con).start() ;  
  97.     }  
  98. }  


     执行结果如下:

 

     另外,在run方法中,二者循环的次数要相同,否则,当一方的循环结束时,另一方的循环依然继续,它会阻塞在wait()方法处,而等不到对方的notify通知。

【Java并发编程】之十四:图文讲述同步的另一个重要功能:内存可见性

加锁(synchronized同步)的功能不仅仅局限于互斥行为,同时还存在另外一个重要的方面:内存可见性。我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且还希望确保当一个线程修改了对象状态后,其他线程能够看到该变化。而线程的同步恰恰也能够实现这一点。

     内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。为了确保所有的线程都能看到共享变量的最新值,可以在所有执行读操作或写操作的线程上加上同一把锁。下图示例了同步的可见性保证。

     当线程A执行某个同步代码块时,线程B随后进入由同一个锁保护的同步代码块,这种情况下可以保证,当锁被释放前,A看到的所有变量值(锁释放前,A看到的变量包括y和x)在B获得同一个锁后同样可以由B看到。换句话说,当线程B执行由锁保护的同步代码块时,可以看到线程A之前在同一个锁保护的同步代码块中的所有操作结果。如果在线程A unlock M之后,线程B才进入lock M,那么线程B都可以看到线程A unlock M之前的操作,可以得到i=1,j=1。如果在线程B unlock M之后,线程A才进入lock M,那么线程B就不一定能看到线程A中的操作,因此j的值就不一定是1。

     现在考虑如下代码:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public class  MutableInteger  
  2. {  
  3.     private int value;  
  4.   
  5.     public int get(){  
  6.         return value;  
  7.     }  
  8.     public void set(int value){  
  9.         this.value = value;  
  10.     }  
  11. }  


     以上代码中,get和set方法都在没有同步的情况下访问value。如果value被多个线程共享,假如某个线程调用了set,那么另一个正在调用get的线程可能会看到更新后的value值,也可能看不到。

     通过对set和get方法进行同步,可以使MutableInteger成为一个线程安全的类,如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public class  SynchronizedInteger  
  2. {  
  3.     private int value;  
  4.   
  5.     public synchronized int get(){  
  6.         return value;  
  7.     }  
  8.     public synchronized void set(int value){  
  9.         this.value = value;  
  10.     }  
  11. }  


     对set和get方法进行了同步,加上了同一把对象锁,这样get方法可以看到set方法中value值的变化,从而每次通过get方法取得的value的值都是最新的value值。

【Java并发编程】之十五:并发编程中实现内存可见的两种方法比较:加锁和volatile变量


这里比较下同步实现内存可见性的方法和通过volatile变量实现内存可见性的方法的区别。

 

    1、volatile变量是一种稍弱的同步机制在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。

    2、从内存可见性的角度看,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。

    3、在代码中如果过度依赖volatile变量来控制状态的可见性,通常会比使用锁的代码更脆弱,也更难以理解。仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它。一般来说,用同步机制会更安全些。

    4、加锁机制(即同步机制)既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性,原因是声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:“count++”、“count = count+1”。

 

     当且仅当满足以下所有条件时,才应该使用volatile变量:

     1、对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。

     2、该变量没有包含在具有其他变量的不变式中。


总结:在需要同步的时候,第一选择应该是synchronized关键字,这是最安全的方式,尝试其他任何方式都是有风险的。尤其在、jdK1.5之后,对synchronized同步机制做了很多优化,如:自适应的自旋锁、锁粗化、锁消除、轻量级锁等,使得它的性能明显有了很大的提升。

【Java并发编程】之十六:深入Java内存模型——happen-before规则及其对DCL的分析(含代码)

happen—before规则介绍

    Java语言中有一个“先行发生”(happen—before)的规则,它是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,其意思就是说,在发生操作B之前,操作A产生的影响都能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,它与时间上的先后发生基本没有太大关系。这个原则特别重要,它是判断数据是否存在竞争、线程是否安全的主要依据。

    举例来说,假设存在如下三个线程,分别执行对应的操作:

---------------------------------------------------------------------------

线程A中执行如下操作:i=1

线程B中执行如下操作:j=i

线程C中执行如下操作:i=2

---------------------------------------------------------------------------

    假设线程A中的操作”i=1“ happen—before线程B中的操作“j=i”,那么就可以保证在线程B的操作执行后,变量j的值一定为1,即线程B观察到了线程A中操作“i=1”所产生的影响;现在,我们依然保持线程A和线程B之间的happen—before关系,同时线程C出现在了线程A和线程B的操作之间,但是C与B并没有happen—before关系,那么j的值就不确定了,线程C对变量i的影响可能会被线程B观察到,也可能不会,这时线程B就存在读取到不是最新数据的风险,不具备线程安全性。

    下面是Java内存模型中的八条可保证happen—before的规则,它们无需任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们进行随机地重排序。


    1、程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作。

    2、管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序,下同)对同一个锁的lock操作。

    3、volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。

    4、线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。

    5、线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

    6、线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。

    7、对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。

    8、传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。


时间上先后顺序和happen—before原则


    ”时间上执行的先后顺序“与”happen—before“之间有何不同呢?

    1、首先来看操作A在时间上先与操作B发生,是否意味着操作A happen—before操作B?

    一个常用来分析的例子如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. private int value = 0;  
  2.   
  3. public int get(){  
  4.     return value;  
  5. }  
  6. public void set(int value){  
  7.     this.value = value;  
  8. }  
    假设存在线程A和线程B,线程A先(时间上的先)调用了setValue(3)操作,然后(时间上的后)线程B调用了同一对象的getValue()方法,那么线程B得到的返回值一定是3吗?

    对照以上八条happen—before规则,发现没有一条规则适合于这里的value变量,从而我们可以判定线程A中的setValue(3)操作与线程B中的getValue()操作不存在happen—before关系。因此,尽管线程A的setValue(3)在操作时间上先于操作B的getvalue(),但无法保证线程B的getValue()操作一定观察到了线程A的setValue(3)操作所产生的结果,也即是getValue()的返回值不一定为3(有可能是之前setValue所设置的值)。这里的操作不是线程安全的。

    因此,”一个操作时间上先发生于另一个操作“并不代表”一个操作happen—before另一个操作“。

    解决方法:可以将setValue(int)方法和getValue()方法均定义为synchronized方法,也可以把value定义为volatile变量(value的修改并不依赖value的原值,符合volatile的使用场景),分别对应happen—before规则的第2和第3条。注意,只将setValue(int)方法和getvalue()方法中的一个定义为synchronized方法是不行的,必须对同一个变量的所有读写同步,才能保证不读取到陈旧的数据,仅仅同步读或写是不够的 


    2、其次来看,操作A happen—before操作B,是否意味着操作A在时间上先与操作B发生?

    看有如下代码:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. x = 1;  
  2. y = 2;  
    假设同一个线程执行上面两个操作:操作A:x=1和操作B:y=2。根据happen—before规则的第1条,操作A happen—before 操作B,但是由于编译器的指令重排序(Java语言规范规定了JVM线程内部维持顺序化语义,也就是说只要程序的最终结果等同于它在严格的顺序化环境下的结果,那么指令的执行顺序就可能与代码的顺序不一致。这个过程通过叫做指令的重排序。指令重排序存在的意义在于:JVM能够根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥机器的性能。在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整)等原因,操作A在时间上有可能后于操作B被处理器执行,但这并不影响happen—before原则的正确性。

    因此,”一个操作happen—before另一个操作“并不代表”一个操作时间上先发生于另一个操作“。


    最后,一个操作和另一个操作必定存在某个顺序,要么一个操作或者是先于或者是后于另一个操作,或者与两个操作同时发生。同时发生是完全可能存在的,特别是在多CPU的情况下。而两个操作之间却可能没有happen-before关系,也就是说有可能发生这样的情况,操作A不happen-before操作B,操作B也不happen-before操作A,用数学上的术语happen-before关系是个偏序关系。两个存在happen-before关系的操作不可能同时发生,一个操作A happen-before操作B,它们必定在时间上是完全错开的,这实际上也是同步的语义之一(独占访问)。


利用happen—before规则分析DCL


   DCL即双重检查加锁,关于单例模式的DCL机制,可以参看:http://blog.csdn.net/ns_code/article/details/17359719一文,这里不再详细介绍。下面是一个典型的在单例模式中使用DCL的例子:
[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public class LazySingleton {  
  2.     private int someField;  
  3.       
  4.     private static LazySingleton instance;  
  5.       
  6.     private LazySingleton() {  
  7.         this.someField = new Random().nextInt(200)+1;         // (1)  
  8.     }  
  9.       
  10.     public static LazySingleton getInstance() {  
  11.         if (instance == null) {                               // (2)  
  12.             synchronized(LazySingleton.class) {               // (3)  
  13.                 if (instance == null) {                       // (4)  
  14.                     instance = new LazySingleton();           // (5)  
  15.                 }  
  16.             }  
  17.         }  
  18.         return instance;                                      // (6)  
  19.     }  
  20.       
  21.     public int getSomeField() {  
  22.         return this.someField;                                // (7)  
  23.     }  
  24. }  

    这里得到单一的instance实例是没有问题的,问题的关键在于尽管得到了Singleton的正确引用,但是却有可能访问到其成员变量不正确值。具体来说Singleton.getInstance().getSomeField()有可能返回someField的默认值0。如果程序行为正确的话,这应当是不可能发生的事,因为在构造函数里设置的someField的值不可能为0。为也说明这种情况理论上有可能发生,我们只需要说明语句(1)和语句(7)并不存在happen-before关系。

   假设线程Ⅰ是初次调用getInstance()方法,紧接着线程Ⅱ也调用了getInstance()方法和getSomeField()方法,我们要说明的是线程Ⅰ的语句(1)并不happen-before线程Ⅱ的语句(7)。线程Ⅱ在执行getInstance()方法的语句(2)时,由于对instance的访问并没有处于同步块中,因此线程Ⅱ可能观察到也可能观察不到线程Ⅰ在语句(5)时对instance的写入,也就是说instance的值可能为空也可能为非空。我们先假设instance的值非空,也就观察到了线程Ⅰ对instance的写入,这时线程Ⅱ就会执行语句(6)直接返回这个instance的值,然后对这个instance调用getSomeField()方法,该方法也是在没有任何同步情况被调用,因此整个线程Ⅱ的操作都是在没有同步的情况下调用 ,这时我们便无法利用上述8条happen-before规则得到线程Ⅰ的操作和线程Ⅱ的操作之间的任何有效的happen-before关系(主要考虑规则的第2条,但由于线程Ⅱ没有在进入synchronized块,因此不存在lock与unlock锁的问题),这说明线程Ⅰ的语句(1)和线程Ⅱ的语句(7)之间并不存在happen-before关系,这就意味着线程Ⅱ在执行语句(7)完全有可能观测不到线程Ⅰ在语句(1)处对someFiled写入的值,这就是DCL的问题所在。很荒谬,是吧?DCL原本是为了逃避同步,它达到了这个目的,也正是因为如此,它最终受到惩罚,这样的程序存在严重的bug,虽然这种bug被发现的概率绝对比中彩票的概率还要低得多,而且是转瞬即逝,更可怕的是,即使发生了你也不会想到是DCL所引起的。

    前面我们说了,线程Ⅱ在执行语句(2)时也有可能观察空值,如果是种情况,那么它需要进入同步块,并执行语句(4)。在语句(4)处线程Ⅱ还能够读到instance的空值吗?不可能。这里因为这时对instance的写和读都是发生在同一个锁确定的同步块中,这时读到的数据是最新的数据。为也加深印象,我再用happen-before规则分析一遍。线程Ⅱ在语句(3)处会执行一个lock操作,而线程Ⅰ在语句(5)后会执行一个unlock操作,这两个操作都是针对同一个锁--Singleton.class,因此根据第2条happen-before规则,线程Ⅰ的unlock操作happen-before线程Ⅱ的lock操作,再利用单线程规则,线程Ⅰ的语句(5) -> 线程Ⅰ的unlock操作,线程Ⅱ的lock操作 -> 线程Ⅱ的语句(4),再根据传递规则,就有线程Ⅰ的语句(5) -> 线程Ⅱ的语句(4),也就是说线程Ⅱ在执行语句(4)时能够观测到线程Ⅰ在语句(5)时对Singleton的写入值。接着对返回的instance调用getSomeField()方法时,我们也能得到线程Ⅰ的语句(1) -> 线程Ⅱ的语句(7)(由于线程Ⅱ有进入synchronized块,根据规则2可得),这表明这时getSomeField能够得到正确的值。但是仅仅是这种情况的正确性并不妨碍DCL的不正确性,一个程序的正确性必须在所有的情况下的行为都是正确的,而不能有时正确,有时不正确。

    对DCL的分析也告诉我们一条经验原则:对引用(包括对象引用和数组引用)的非同步访问,即使得到该引用的最新值,却并不能保证也能得到其成员变量(对数组而言就是每个数组元素)的最新值。

   解决方案:
    1、最简单而且安全的解决方法是使用static内部类的思想,它利用的思想是:一个类直到被使用时才被初始化,而类初始化的过程是非并行的,这些都有JLS保证。
如下述代码:
[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public class Singleton {  
  2.   
  3.   private Singleton() {}  
  4.   
  5.   // Lazy initialization holder class idiom for static fields  
  6.   private static class InstanceHolder {  
  7.    private static final Singleton instance = new Singleton();  
  8.   }  
  9.   
  10.   public static Singleton getSingleton() {   
  11.     return InstanceHolder.instance;   
  12.   }  
  13. }  

 
    2、另外,可以将instance声明为volatile,即
private volatile static LazySingleton instance; 
 

    这样我们便可以得到,线程Ⅰ的语句(5) -> 语线程Ⅱ的句(2),根据单线程规则,线程Ⅰ的语句(1) -> 线程Ⅰ的语句(5)和语线程Ⅱ的句(2) -> 语线程Ⅱ的句(7),再根据传递规则就有线程Ⅰ的语句(1) -> 语线程Ⅱ的句(7),这表示线程Ⅱ能够观察到线程Ⅰ在语句(1)时对someFiled的写入值,程序能够得到正确的行为。


   注:

    1、volatile屏蔽指令重排序的语义在JDK1.5中才被完全修复,此前的JDK中及时将变量声明为volatile,也仍然不能完全避免重排序所导致的问题(主要是volatile变量前后的代码仍然存在重排序问题),这点也是在JDK1.5之前的Java中无法安全使用DCL来实现单例模式的原因。

    2、把volatile写和volatile读这两个操作综合起来看,在读线程B读一个volatile变量后,写线程A在写这个volatile变量之前,所有可见的共享变量的值都将立即变得对读线程B可见。

 

   3、 在java5之前对final字段的同步语义和其它变量没有什么区别,在java5中,final变量一旦在构造函数中设置完成(前提是在构造函数中没有泄露this引用),其它线程必定会看到在构造函数中设置的值。而DCL的问题正好在于看到对象的成员变量的默认值,因此我们可以将LazySingleton的someField变量设置成final,这样在java5中就能够正确运行了。



参考资料:http://www.iteye.com/topic/260515/

                 《深入理解Java虚拟机——JVM高级特性与最佳实践》第12章

 

【Java并发编程】之十七:深入Java内存模型—内存操作规则总结

主内存与工作内存

    Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量主要是指共享变量,存在竞争问题的变量。Java内存模型规定所有的变量都存储在主内存中,而每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(根据Java虚拟机规范的规定,volatile变量依然有共享内存的拷贝,但是由于它特殊的操作顺序性规定——从工作内存中读写数据前,必须先将主内存中的数据同步到工作内存中,所有看起来如同直接在主内存中读写访问一般,因此这里的描述对于volatile也不例外)。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值得传递均需要通过主内存来完成。


内存间交互操作


    Java内存模型中定义了以下8中操作来完成主内存与工作内存之间交互的实现细节:


    1、luck(锁定):作用于主内存的变量,它把一个变量标示为一条线程独占的状态。

    2、unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

    3、read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到工作内存中,以便随后的load动作使用。

    4、load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

    5、use(使用):作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作。

    6、assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

    7、store(存储):作用于工作内存的变量,它把工作内存中的一个变量的值传递到主内存中,以便随后的write操作使用。

    8、write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量值放入主内存的变量中。


   Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:


    1、不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。

    2、不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。

    3、不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。

    4、一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。

    5、一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。

    6、如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。

    7、如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。

    8、对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。


volatile型变量的特殊规则


    Java内存模型对volatile专门定义了一些特殊的访问规则,当一个变量被定义成volatile之后,他将具备两种特性:

    1、保证此变量对所有线程的可见性。这里不具体解释了。需要注意,volatile变量的写操作除了对它本身的读操作可见外,volatile写操作之前的所有共享变量均对volatile读操作之后的操作可见,另外注意其适用场景,详见http://blog.csdn.net/ns_code/article/details/17290021http://blog.csdn.net/ns_code/article/details/17101369这两篇博文。

    2、禁止指令重排序优化。普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获得正确的结果,而不能保证变量赋值操作的顺序与程序中的执行顺序一致,在单线程中,我们是无法感知这一点的。


    补充:Java语言规范规定了JVM线程内部维持顺序化语义,也就是说只要程序的最终结果等同于它在严格的顺序化环境下的结果,那么指令的执行顺序就可能与代码的顺序不一致,这个过程通过叫做指令的重排序。指令重排序存在的意义在于:JVM能够根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥机器的性能。在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整


final域

    final类型的域是不能修改的,除了这一点外,在Java内存模型中,final域还有着特殊的语义,final域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无须同步。具体而言,就是被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那么在其他线程中就能看到final字段的值,而且其外、外部可见状态永远也不会改变。它所带来的安全性是最简单最纯粹的。


long和double型变量的特殊规则


    Java内存模型要求lock、unlock、read、load、assign、use、store和write这8个操作都具有原子性,但是对于64位的数据类型long和double,在模型中特别定义了一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行。这样,如果有多个线程共享一个未被声明为volatile的long或double类型的变量,并且同时对它们进行读取和修改操作,那么某些线程可能会读到一个既非原值,也非其他线程修改值得代表了“半个变量”的数值。不过这种读取到“半个变量”的情况非常罕见,因为Java内存模型虽然允许虚拟机不把long和double变量的读写实现成原子操作,但允许迅疾选择把这些操作实现为具有原子性的操作,而且还“强烈建议”虚拟机这样实现。目前各种平台下的商用虚拟机几乎都选择吧64位数据的读写操作作为原子操作来对待,因此在编码时,不需要将long和double变量专门声明为volatile。

【Java并发编程】之十八:第五篇中volatile意外问题的正确分析解答(含代码)

《Java并发编程学习笔记之五:volatile变量修饰符—意料之外的问题》一文中遗留了一个问题,就是volatile只修饰了missedIt变量,而没修饰value变量,但是在线程读取value的值的时候,也读到的是最新的数据。但是在网上查了很多资料都无果,看来很多人对volatile的规则并不是太清晰,或者说只停留在很表面的层次,一知半解。

    这两天看《深入Java虚拟机——JVM高级特性与最佳实践》第12章:Java内存模型与线程,并在网上查阅了Java内存模型相关资料,学到了不少东西,尤其在看这篇文章的volatile部分的讲解之后,算是确定了问题出现的原因。

    首先明确一点:假如有两个线程分别读写volatile变量时,线程A写入了某volatile变量,线程B在读取该volatile变量时,便能看到线程A对该volatile变量的写入操作,关键在这里,它不仅会看到对该volatile变量的写入操作,A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,都将立即变得对B线程可见。

   回过头来看文章中出现的问题,由于程序中volatile变量missedIt的写入操作在value变量写入操作之后,而且根据volatile规则,又不能重排序,因此,在线程B读取由线程A改变后的missedIt之后,它之前的value变量在线程A的改变也对线程B变得可见了。

     我们颠倒一下value=50和missedIt=true这两行代码试下,即missedIt=true在前,value=50在后,这样便会得到我们想要的结果:value值的改变不会被看到。

    这应该是JDK1.2之后对volatile规则做了一些修订的结果。


    修改后的代码如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public class Volatile extends Object implements Runnable {  
  2.     //value变量没有被标记为volatile  
  3.     private int value;    
  4.     //missedIt变量被标记为volatile  
  5.     private volatile boolean missedIt;  
  6.     //creationTime不需要声明为volatile,因为代码执行中它没有发生变化  
  7.     private long creationTime;   
  8.   
  9.     public Volatile() {  
  10.         value = 10;  
  11.         missedIt = false;  
  12.         //获取当前时间,亦即调用Volatile构造函数时的时间  
  13.         creationTime = System.currentTimeMillis();  
  14.     }  
  15.   
  16.     public void run() {  
  17.         print("entering run()");  
  18.   
  19.         //循环检查value的值是否不同  
  20.         while ( value < 20 ) {  
  21.             //如果missedIt的值被修改为true,则通过break退出循环  
  22.             if  ( missedIt ) {  
  23.                 //进入同步代码块前,将value的值赋给currValue  
  24.                 int currValue = value;  
  25.                 //在一个任意对象上执行同步语句,目的是为了让该线程在进入和离开同步代码块时,  
  26.                 //将该线程中的所有变量的私有拷贝与共享内存中的原始值进行比较,  
  27.                 //从而发现没有用volatile标记的变量所发生的变化  
  28.                 Object lock = new Object();  
  29.                 synchronized ( lock ) {  
  30.                     //不做任何事  
  31.                 }  
  32.                 //离开同步代码块后,将此时value的值赋给valueAfterSync  
  33.                 int valueAfterSync = value;  
  34.                 print("in run() - see value=" + currValue +", but rumor has it that it changed!");  
  35.                 print("in run() - valueAfterSync=" + valueAfterSync);  
  36.                 break;   
  37.             }  
  38.         }  
  39.         print("leaving run()");  
  40.     }  
  41.   
  42.     public void workMethod() throws InterruptedException {  
  43.         print("entering workMethod()");  
  44.         print("in workMethod() - about to sleep for 2 seconds");  
  45.         Thread.sleep(2000);  
  46.         //仅在此改变value的值  
  47.         missedIt = true;  
  48. //      value = 50;  
  49.         print("in workMethod() - just set value=" + value);  
  50.         print("in workMethod() - about to sleep for 5 seconds");  
  51.         Thread.sleep(5000);  
  52.         //仅在此改变missedIt的值  
  53. //      missedIt = true;  
  54.         value = 50;  
  55.         print("in workMethod() - just set missedIt=" + missedIt);  
  56.         print("in workMethod() - about to sleep for 3 seconds");  
  57.         Thread.sleep(3000);  
  58.         print("leaving workMethod()");  
  59.     }  
  60.   
  61. /* 
  62. *该方法的功能是在要打印的msg信息前打印出程序执行到此所化去的时间,以及打印msg的代码所在的线程 
  63. */  
  64.     private void print(String msg) {  
  65.         //使用java.text包的功能,可以简化这个方法,但是这里没有利用这一点  
  66.         long interval = System.currentTimeMillis() - creationTime;  
  67.         String tmpStr = "    " + ( interval / 1000.0 ) + "000";       
  68.         int pos = tmpStr.indexOf(".");  
  69.         String secStr = tmpStr.substring(pos - 2, pos + 4);  
  70.         String nameStr = "        " + Thread.currentThread().getName();  
  71.         nameStr = nameStr.substring(nameStr.length() - 8, nameStr.length());      
  72.         System.out.println(secStr + " " + nameStr + ": " + msg);  
  73.     }  
  74.   
  75.     public static void main(String[] args) {  
  76.         try {  
  77.             //通过该构造函数可以获取实时时钟的当前时间  
  78.             Volatile vol = new Volatile();  
  79.   
  80.             //稍停100ms,以让实时时钟稍稍超前获取时间,使print()中创建的消息打印的时间值大于0  
  81.             Thread.sleep(100);    
  82.   
  83.             Thread t = new Thread(vol);  
  84.             t.start();  
  85.   
  86.             //休眠100ms,让刚刚启动的线程有时间运行  
  87.             Thread.sleep(100);    
  88.             //workMethod方法在main线程中运行  
  89.             vol.workMethod();  
  90.         } catch ( InterruptedException x ) {  
  91.             System.err.println("one of the sleeps was interrupted");  
  92.         }  
  93.     }  
  94. }  
    执行结果如下:


   很明显,这其实并不符合使用volatile的第二个条件:该变量要没有包含在具有其他变量的不变式中。因此,在这里使用volatile是不安全的。

【Java并发编程】之十九:并发新特性—Executor框架与线程池(含代码)

Executor框架简介

    在Java 5之后并发编程引入了一堆新的启动、调度和管理线程的API。Executor框架便是Java 5中引入的,其内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免this逃逸问题——如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象用Executor在构造器中。


    Executor框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。


    Executor接口中之定义了一个方法execute(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类。ExecutorService接口继承自Executor接口,它提供了更丰富的实现多线程的方法,比如,ExecutorService提供了关闭自己的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 可以调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。


    ExecutorService的生命周期包括三种状态:运行、关闭、终止。创建后便进入运行状态,当调用了shutdown()方法时,便进入关闭状态,此时意味着ExecutorService不再接受新的任务,但它还在执行已经提交了的任务,当素有已经提交了的任务执行完后,便到达终止状态。如果不调用shutdown()方法,ExecutorService会一直处在运行状态,不断接收新的任务,执行新的任务,服务器端一般不需要关闭它,保持一直运行即可。



    Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。   

    public static ExecutorService newFixedThreadPool(int nThreads)

    创建固定数目线程的线程池。

    public static ExecutorService newCachedThreadPool()

    创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线   程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。

    public static ExecutorService newSingleThreadExecutor()

    创建一个单线程化的Executor。

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

    创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。


    这四种方法都是用的Executors中的ThreadFactory建立的线程,下面就以上四个方法做个比较




newCachedThreadPool()                                                                                                                                         

-缓存型池子,先查看池中有没有以前建立的线程,如果有,就 reuse.如果没有,就建一个新的线程加入池中
-缓存型池子通常用于执行一些生存期很短的异步型任务
 因此在一些面向连接的daemon型SERVER中用得不多。但对于生存期短的异步任务,它是Executor的首选。
-能reuse的线程,必须是timeout IDLE内的池中线程,缺省     timeout是60s,超过这个IDLE时长,线程实例将被终止及移出池。
  注意,放入CachedThreadPool的线程不必担心其结束,超过TIMEOUT不活动,其会自动被终止。



newFixedThreadPool(int)                                                      

-newFixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程

-其独特之处:任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待直到当前的线程中某个线程终止直接被移出池子
-和cacheThreadPool不同,FixedThreadPool没有IDLE机制(可能也有,但既然文档没提,肯定非常长,类似依赖上层的TCP或UDP IDLE机制之类的),所以FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器
-从方法的源代码看,cache池和fixed 池调用的是同一个底层 池,只不过参数不同:
fixed池线程数固定,并且是0秒IDLE(无IDLE)    
cache池线程数支持0-Integer.MAX_VALUE(显然完全没考虑主机的资源承受能力),60秒IDLE  


newScheduledThreadPool(int)

-调度型线程池
-这个池子里的线程可以按schedule依次delay执行,或周期执行

SingleThreadExecutor()

-单例线程,任意时间池中只能有一个线程
-用的是和cache池和fixed池相同的底层池,但线程数目是1-1,0秒IDLE(无IDLE)


    一般来说,CachedTheadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它是合理的Executor的首选,只有当这种方式会引发问题时(比如需要大量长时间面向连接的线程时),才需要考虑用FixedThreadPool。(该段话摘自《Thinking in Java》第四版)

                         

Executor执行Runnable任务

    通过Executors的以上四个静态工厂方法获得 ExecutorService实例,而后调用该实例的execute(Runnable command)方法即可。一旦Runnable任务传递到execute()方法,该方法便会自动在一个线程上执行。下面是是Executor执行Runnable任务的示例代码:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. import java.util.concurrent.ExecutorService;   
  2. import java.util.concurrent.Executors;   
  3.   
  4. public class TestCachedThreadPool{   
  5.     public static void main(String[] args){   
  6.         ExecutorService executorService = Executors.newCachedThreadPool();   
  7. //      ExecutorService executorService = Executors.newFixedThreadPool(5);  
  8. //      ExecutorService executorService = Executors.newSingleThreadExecutor();  
  9.         for (int i = 0; i < 5; i++){   
  10.             executorService.execute(new TestRunnable());   
  11.             System.out.println("************* a" + i + " *************");   
  12.         }   
  13.         executorService.shutdown();   
  14.     }   
  15. }   
  16.   
  17. class TestRunnable implements Runnable{   
  18.     public void run(){   
  19.         System.out.println(Thread.currentThread().getName() + "线程被调用了。");   
  20.     }   
  21. }  
   某次执行后的结果如下:


   从结果中可以看出,pool-1-thread-1和pool-1-thread-2均被调用了两次,这是随机的,execute会首先在线程池中选择一个已有空闲线程来执行任务,如果线程池中没有空闲线程,它便会创建一个新的线程来执行任务。


Executor执行Callable任务

    在Java 5之后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,但是Runnable任务没有返回值,而Callable任务有返回值。并且Callable的call()方法只能通过ExecutorService的submit(Callable<T> task) 方法来执行,并且返回一个 <T>Future<T>,是表示任务等待完成的 Future。


    Callable接口类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常而Callable又返回结果,而且当获取返回结果时可能会抛出异常。Callable中的call()方法类似Runnable的run()方法,区别同样是有返回值,后者没有。


    当将一个Callable的对象传递给ExecutorService的submit方法,则该call方法自动在一个线程上执行,并且会返回执行结果Future对象。同样,将Runnable的对象传递给ExecutorService的submit方法,则该run方法自动在一个线程上执行,并且会返回执行结果Future对象,但是在该Future对象上调用get方法,将返回null。


    下面给出一个Executor执行Callable任务的示例代码:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. import java.util.ArrayList;   
  2. import java.util.List;   
  3. import java.util.concurrent.*;   
  4.   
  5. public class CallableDemo{   
  6.     public static void main(String[] args){   
  7.         ExecutorService executorService = Executors.newCachedThreadPool();   
  8.         List<Future<String>> resultList = new ArrayList<Future<String>>();   
  9.   
  10.         //创建10个任务并执行   
  11.         for (int i = 0; i < 10; i++){   
  12.             //使用ExecutorService执行Callable类型的任务,并将结果保存在future变量中   
  13.             Future<String> future = executorService.submit(new TaskWithResult(i));   
  14.             //将任务执行结果存储到List中   
  15.             resultList.add(future);   
  16.         }   
  17.   
  18.         //遍历任务的结果   
  19.         for (Future<String> fs : resultList){   
  20.                 try{   
  21.                     while(!fs.isDone);//Future返回如果没有完成,则一直循环等待,直到Future返回完成  
  22.                     System.out.println(fs.get());     //打印各个线程(任务)执行的结果   
  23.                 }catch(InterruptedException e){   
  24.                     e.printStackTrace();   
  25.                 }catch(ExecutionException e){   
  26.                     e.printStackTrace();   
  27.                 }finally{   
  28.                     //启动一次顺序关闭,执行以前提交的任务,但不接受新任务  
  29.                     executorService.shutdown();   
  30.                 }   
  31.         }   
  32.     }   
  33. }   
  34.   
  35.   
  36. class TaskWithResult implements Callable<String>{   
  37.     private int id;   
  38.   
  39.     public TaskWithResult(int id){   
  40.         this.id = id;   
  41.     }   
  42.   
  43.     /**  
  44.      * 任务的具体过程,一旦任务传给ExecutorService的submit方法, 
  45.      * 则该方法自动在一个线程上执行 
  46.      */   
  47.     public String call() throws Exception {  
  48.         System.out.println("call()方法被自动调用!!!    " + Thread.currentThread().getName());   
  49.         //该返回结果将被Future的get方法得到  
  50.         return "call()方法被自动调用,任务返回的结果是:" + id + "    " + Thread.currentThread().getName();   
  51.     }   
  52. }  
    某次执行结果如下:

   


    从结果中可以同样可以看出,submit也是首先选择空闲线程来执行任务,如果没有,才会创建新的线程来执行任务。另外,需要注意:如果Future的返回尚未完成,则get()方法会阻塞等待,直到Future完成返回,可以通过调用isDone()方法判断Future是否完成了返回。



自定义线程池

    自定义线程池,可以用ThreadPoolExecutor类创建,它有多个构造方法来创建线程池,用该类很容易实现自定义的线程池,这里先贴上示例程序:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. import java.util.concurrent.ArrayBlockingQueue;   
  2. import java.util.concurrent.BlockingQueue;   
  3. import java.util.concurrent.ThreadPoolExecutor;   
  4. import java.util.concurrent.TimeUnit;   
  5.   
  6. public class ThreadPoolTest{   
  7.     public static void main(String[] args){   
  8.         //创建等待队列   
  9.         BlockingQueue<Runnable> bqueue = new ArrayBlockingQueue<Runnable>(20);   
  10.         //创建线程池,池中保存的线程数为3,允许的最大线程数为5  
  11.         ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,50,TimeUnit.MILLISECONDS,bqueue);   
  12.         //创建七个任务   
  13.         Runnable t1 = new MyThread();   
  14.         Runnable t2 = new MyThread();   
  15.         Runnable t3 = new MyThread();   
  16.         Runnable t4 = new MyThread();   
  17.         Runnable t5 = new MyThread();   
  18.         Runnable t6 = new MyThread();   
  19.         Runnable t7 = new MyThread();   
  20.         //每个任务会在一个线程上执行  
  21.         pool.execute(t1);   
  22.         pool.execute(t2);   
  23.         pool.execute(t3);   
  24.         pool.execute(t4);   
  25.         pool.execute(t5);   
  26.         pool.execute(t6);   
  27.         pool.execute(t7);   
  28.         //关闭线程池   
  29.         pool.shutdown();   
  30.     }   
  31. }   
  32.   
  33. class MyThread implements Runnable{   
  34.     @Override   
  35.     public void run(){   
  36.         System.out.println(Thread.currentThread().getName() + "正在执行。。。");   
  37.         try{   
  38.             Thread.sleep(100);   
  39.         }catch(InterruptedException e){   
  40.             e.printStackTrace();   
  41.         }   
  42.     }   
  43. }  
    运行结果如下:

    从结果中可以看出,七个任务是在线程池的三个线程上执行的。这里简要说明下用到的ThreadPoolExecuror类的构造方法中各个参数的含义。   

public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long         keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue)

corePoolSize:线程池中所保存的核心线程数,包括空闲线程。

maximumPoolSize:池中允许的最大线程数。

keepAliveTime:线程池中的空闲线程所能持续的最长时间。

unit:持续时间的单位。

workQueue:任务执行前保存任务的队列,仅保存由execute方法提交的Runnable任务。

    根据ThreadPoolExecutor源码前面大段的注释,我们可以看出,当试图通过excute方法讲一个Runnable任务添加到线程池中时,按照如下顺序来处理:

    1、如果线程池中的线程数量少于corePoolSize,即使线程池中有空闲线程,也会创建一个新的线程来执行新添加的任务;

    2、如果线程池中的线程数量大于等于corePoolSize,但缓冲队列workQueue未满,则将新添加的任务放到workQueue中,按照FIFO的原则依次等待执行(线程池中有线程空闲出来后依次将缓冲队列中的任务交付给空闲的线程执行);

    3、如果线程池中的线程数量大于等于corePoolSize,且缓冲队列workQueue已满,但线程池中的线程数量小于maximumPoolSize,则会创建新的线程来处理被添加的任务;

    4、如果线程池中的线程数量等于了maximumPoolSize,有4种才处理方式(该构造方法调用了含有5个参数的构造方法,并将最后一个构造方法为RejectedExecutionHandler类型,它在处理线程溢出时有4种方式,这里不再细说,要了解的,自己可以阅读下源码)。

    总结起来,也即是说,当有新的任务要处理时,先看线程池中的线程数量是否大于corePoolSize,再看缓冲队列workQueue是否满,最后看线程池中的线程数量是否大于maximumPoolSize。

    另外,当线程池中的线程数量大于corePoolSize时,如果里面有线程的空闲时间超过了keepAliveTime,就将其移除线程池,这样,可以动态地调整线程池中线程的数量。

    我们大致来看下Executors的源码,newCachedThreadPool的不带RejectedExecutionHandler参数(即第五个参数,线程数量超过maximumPoolSize时,指定处理方式)的构造方法如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public static ExecutorService newCachedThreadPool() {  
  2.     return new ThreadPoolExecutor(0, Integer.MAX_VALUE,  
  3.                                   60L, TimeUnit.SECONDS,  
  4.                                   new SynchronousQueue<Runnable>());  
  5. }  
    它将corePoolSize设定为0,而将maximumPoolSize设定为了Integer的最大值,线程空闲超过60秒,将会从线程池中移除。由于核心线程数为0,因此每次添加任务,都会先从线程池中找空闲线程,如果没有就会创建一个线程(SynchronousQueue<Runnalbe>决定的,后面会说)来执行新的任务,并将该线程加入到线程池中,而最大允许的线程数为Integer的最大值,因此这个线程池理论上可以不断扩大。

    再来看newFixedThreadPool的不带RejectedExecutionHandler参数的构造方法,如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public static ExecutorService newFixedThreadPool(int nThreads) {  
  2.     return new ThreadPoolExecutor(nThreads, nThreads,  
  3.                                   0L, TimeUnit.MILLISECONDS,  
  4.                                   new LinkedBlockingQueue<Runnable>());  
  5. }  
    它将corePoolSize和maximumPoolSize都设定为了nThreads,这样便实现了线程池的大小的固定,不会动态地扩大,另外,keepAliveTime设定为了0,也就是说线程只要空闲下来,就会被移除线程池,敢于LinkedBlockingQueue下面会说。

    下面说说几种排队的策略:

    1、直接提交。缓冲队列采用 SynchronousQueue,它将任务直接交给线程处理而不保持它们。如果不存在可用于立即运行任务的线程(即线程池中的线程都在工作),则试图把任务加入缓冲队列将会失败,因此会构造一个新的线程来处理新添加的任务,并将其加入到线程池中。直接提交通常要求无界 maximumPoolSizes(Integer.MAX_VALUE) 以避免拒绝新提交的任务。newCachedThreadPool采用的便是这种策略。

    2、无界队列。使用无界队列(典型的便是采用预定义容量的 LinkedBlockingQueue,理论上是该缓冲队列可以对无限多的任务排队)将导致在所有 corePoolSize 线程都工作的情况下将新任务加入到缓冲队列中。这样,创建的线程就不会超过 corePoolSize,也因此,maximumPoolSize 的值也就无效了。当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列。newFixedThreadPool采用的便是这种策略。

    3、有界队列。当使用有限的 maximumPoolSizes 时,有界队列(一般缓冲队列使用ArrayBlockingQueue,并制定队列的最大长度)有助于防止资源耗尽,但是可能较难调整和控制,队列大小和最大池大小需要相互折衷,需要设定合理的参数。

【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加锁和释放锁的一般形式如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  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放弃等待,转而去处理其他事情。

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

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

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

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

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  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的响应中断锁,即改为如下代码: 

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  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语句块中的内容,也输出了后面的“读结束”,说明线程对互斥锁的等待被中断了,也就是该互斥锁响应了对读线程的中断。

条件变量实现线程间协作


    在生产者——消费者模型一文中,我们用synchronized实现互斥,并配合使用Object对象的wait()和notify()或notifyAll()方法来实现线程间协作。Java 5之后,我们可以用Reentrantlock锁配合Condition对象上的await()和signal()或signalAll()方法来实现线程间协作。在ReentrantLock对象上newCondition()可以得到一个Condition对象,可以通过在Condition上调用await()方法来挂起一个任务(线程),通过在Condition上调用signal()来通知任务,从而唤醒一个任务,或者调用signalAll()来唤醒所有在这个Condition上被其自身挂起的任务。另外,如果使用了公平锁,signalAll()的与Condition关联的所有任务将以FIFO队列的形式获取锁,如果没有使用公平锁,则获取锁的任务是随机的,这样我们便可以更好地控制处在await状态的任务获取锁的顺序。与notifyAll()相比,signalAll()是更安全的方式。另外,它可以指定唤醒与自身Condition对象绑定在一起的任务。

    下面将生产者——消费者模型一文中的代码改为用条件变量实现,如下:
[java] view plaincopy在CODE上查看代码片派生到我的代码片
  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中提供了读写锁,它将读锁和写锁分离,使得读读操作不互斥,获取读锁和写锁的一般形式如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. ReadWriteLock rwl = new ReentrantReadWriteLock();      
  2. rwl.writeLock().lock()  //获取写锁  
  3. rwl.readLock().lock()  //获取读锁  
   用读锁来锁定读操作,用写锁来锁定写操作,这样写操作和写操作之间会互斥,读操作和写操作之间会互斥,但读操作和读操作就不会互斥。

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

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

【Java并发编程】之二十一:并发新特性—阻塞队列和阻塞栈(含代码)

阻塞队列

    阻塞队列是Java 5并发新特性中的内容,阻塞队列的接口是java.util.concurrent.BlockingQueue,它有多个实现类:ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等,用法大同小异,具体可查看JDK文档,这里简单举例看下ArrayBlockingQueue,它实现了一个有界队列,当队列满时,便会阻塞等待,直到有元素出队,后续的元素才可以被加入队列。

    看下面的例子:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. import java.util.concurrent.BlockingQueue;   
  2. import java.util.concurrent.ArrayBlockingQueue;   
  3.   
  4. public class BlockingQueueTest{   
  5.         public static void main(String[] args) throws InterruptedException {   
  6.                 BlockingQueue<String> bqueue = new ArrayBlockingQueue<String>(20);   
  7.                 for (int i = 0; i < 30; i++) {   
  8.                         //将指定元素添加到此队列中   
  9.                         bqueue.put("加入元素" + i);   
  10.                         System.out.println("向阻塞队列中添加了元素:" + i);   
  11.                 }   
  12.                 System.out.println("程序到此运行结束,即将退出----");   
  13.         }   
  14. }  
    输出结果如下:

    从执行结果中可以看出,由于队列中元素的数量限制在了20个,因此添加20个元素后,其他元素便在队列外阻塞等待,程序并没有终止。

    如果队列已满后,我们将队首元素移出,并可以继续向阻塞队列中添加元素,修改代码如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. import java.util.concurrent.BlockingQueue;   
  2. import java.util.concurrent.ArrayBlockingQueue;   
  3.   
  4. public class BlockingQueueTest{   
  5.         public static void main(String[] args) throws InterruptedException {   
  6.                 BlockingQueue<String> bqueue = new ArrayBlockingQueue<String>(20);   
  7.                 for (int i = 0; i < 30; i++) {   
  8.                         //将指定元素添加到此队列中   
  9.                         bqueue.put("" + i);   
  10.                         System.out.println("向阻塞队列中添加了元素:" + i);   
  11.                         if(i > 18){  
  12.                             //从队列中获取队头元素,并将其移出队列  
  13.                             System.out.println("从阻塞队列中移除元素:" + bqueue.take());  
  14.                         }  
  15.                 }   
  16.                 System.out.println("程序到此运行结束,即将退出----");   
  17.         }   
  18. }  
    执行结果如下:

    从结果中可以看出,当添加了第20个元素后,我们从队首移出一个元素,这样便可以继续向队列中添加元素,之后每添加一个元素,便从将队首元素移除,这样程序便可以执行结束。

     

阻塞栈

   阻塞栈与阻塞队列相似,只是它是Java 6中加入的新特性,阻塞栈的接口java.util.concurrent.BlockingDeque也有很多实现类,使用方法也比较相似,具体查看JDK文档。

    下面同样给出一个简单的例子:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. import java.util.concurrent.BlockingDeque;   
  2. import java.util.concurrent.LinkedBlockingDeque;   
  3.   
  4. public class BlockingDequeTest {   
  5.     public static void main(String[] args) throws InterruptedException {   
  6.             BlockingDeque<String> bDeque = new LinkedBlockingDeque<String>(20);   
  7.             for (int i = 0; i < 30; i++) {   
  8.                 //将指定元素添加到此阻塞栈中  
  9.                 bDeque.putFirst("" + i);   
  10.                 System.out.println("向阻塞栈中添加了元素:" + i);   
  11.             }   
  12.             System.out.println("程序到此运行结束,即将退出----");   
  13.     }   
  14. }  
    执行结果如下:


    程序依然会阻塞等待,我们改为如下代码:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. import java.util.concurrent.BlockingDeque;   
  2. import java.util.concurrent.LinkedBlockingDeque;   
  3.   
  4. public class BlockingDequeTest {   
  5.     public static void main(String[] args) throws InterruptedException {   
  6.             BlockingDeque<String> bDeque = new LinkedBlockingDeque<String>(20);   
  7.             for (int i = 0; i < 30; i++) {   
  8.                 //将指定元素添加到此阻塞栈中  
  9.                 bDeque.putFirst("" + i);   
  10.                 System.out.println("向阻塞栈中添加了元素:" + i);   
  11.                 if(i > 18){  
  12.                     //从阻塞栈中取出栈顶元素,并将其移出  
  13.                     System.out.println("从阻塞栈中移出了元素:" + bDeque.pollFirst());  
  14.                 }  
  15.             }   
  16.             System.out.println("程序到此运行结束,即将退出----");   
  17.     }   
  18. }  
    执行结果如下:

    从结果中可以看出,当添加了第20个元素后,我们从将栈顶元素移处,这样便可以继续向栈中添加元素,之后每添加一个元素,便将栈顶元素移出,这样程序便可以执行结束。

【Java并发编程】之二十二:并发新特性—障碍器CyclicBarrier(含代码)

 CyclicBarrier(又叫障碍器)同样是Java 5中加入的新特性,使用时需要导入java.util.concurrent.CylicBarrier。它适用于这样一种情况:你希望创建一组任务,它们并发地执行工作,另外的一个任务在这一组任务并发执行结束前一直阻塞等待,直到该组任务全部执行结束,这个任务才得以执行。这非常像CountDownLatch,只是CountDownLatch是只触发一次的事件,而CyclicBarrier可以多次重用。

    下面给出一个简单的实例来说明其用法:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. import java.util.concurrent.BrokenBarrierException;   
  2. import java.util.concurrent.CyclicBarrier;   
  3.   
  4. public class CyclicBarrierTest {   
  5.         public static void main(String[] args) {   
  6.                 //创建CyclicBarrier对象,  
  7.                 //并设置执行完一组5个线程的并发任务后,再执行MainTask任务  
  8.                 CyclicBarrier cb = new CyclicBarrier(5new MainTask());   
  9.                 new SubTask("A", cb).start();   
  10.                 new SubTask("B", cb).start();   
  11.                 new SubTask("C", cb).start();   
  12.                 new SubTask("D", cb).start();   
  13.                 new SubTask("E", cb).start();  
  14.         }   
  15. }   
  16.   
  17. /**  
  18. * 最后执行的任务 
  19. */   
  20. class MainTask implements Runnable {   
  21.         public void run() {   
  22.                 System.out.println("......终于要执行最后的任务了......");   
  23.         }   
  24. }   
  25.   
  26. /**  
  27. * 一组并发任务  
  28. */   
  29. class SubTask extends Thread {   
  30.         private String name;   
  31.         private CyclicBarrier cb;   
  32.   
  33.         SubTask(String name, CyclicBarrier cb) {   
  34.                 this.name = name;   
  35.                 this.cb = cb;   
  36.         }   
  37.   
  38.         public void run() {   
  39.                 System.out.println("[并发任务" + name + "]  开始执行");   
  40.                 for (int i = 0; i < 999999; i++) ;    //模拟耗时的任务   
  41.                 System.out.println("[并发任务" + name + "]  开始执行完毕,通知障碍器");   
  42.                 try {   
  43.                         //每执行完一项任务就通知障碍器   
  44.                         cb.await();   
  45.                 } catch (InterruptedException e) {   
  46.                         e.printStackTrace();   
  47.                 } catch (BrokenBarrierException e) {   
  48.                         e.printStackTrace();   
  49.                 }   
  50.         }   
  51. }  
    某次执行的结果如下:

[并发任务A]  开始执行
[并发任务B]  开始执行
[并发任务D]  开始执行
[并发任务E]  开始执行
[并发任务A]  开始执行完毕,通知障碍器
[并发任务E]  开始执行完毕,通知障碍器
[并发任务D]  开始执行完毕,通知障碍器
[并发任务C]  开始执行
[并发任务B]  开始执行完毕,通知障碍器
[并发任务C]  开始执行完毕,通知障碍器
......终于要执行最后的任务了......

    从结果可以看出:MainTask任务在一组中的5个任务执行完后才开始执行。

【Java并发编程】之二十三:并发新特性—信号量Semaphore(含代码)

在操作系统中,信号量是个很重要的概念,它在控制进程间的协作方面有着非常重要的作用,通过对信号量的不同操作,可以分别实现进程间的互斥与同步。当然它也可以用于多线程的控制,我们完全可以通过使用信号量来自定义实现类似Java中的synchronized、wait、notify机制。

    Java并发包中的信号量Semaphore实际上是一个功能完毕的计数信号量,从概念上讲,它维护了一个许可集合,对控制一定资源的消费与回收有着很重要的意义。Semaphore可以控制某个资源被同时访问的任务数,它通过acquire()获取一个许可,release()释放一个许可。如果被同时访问的任务数已满,则其他acquire的任务进入等待状态,直到有一个任务被release掉,它才能得到许可。

    下面给出一个采用Semaphore控制并发访问数量的示例程序:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. import java.util.concurrent.ExecutorService;  
  2. import java.util.concurrent.Executors;  
  3. import java.util.concurrent.Semaphore;  
  4. public class SemaphoreTest{  
  5.     public static void main(String[] args) {  
  6.     //采用新特性来启动和管理线程——内部使用线程池  
  7.     ExecutorService exec = Executors.newCachedThreadPool();  
  8.     //只允许5个线程同时访问  
  9.     final Semaphore semp = new Semaphore(5);  
  10.     //模拟10个客户端访问  
  11.     for (int index = 0; index < 10; index++){  
  12.         final int num = index;  
  13.         Runnable run = new Runnable() {  
  14.             public void run() {  
  15.                 try {  
  16.                     //获取许可  
  17.                     semp.acquire();  
  18.                     System.out.println("线程" +   
  19.                         Thread.currentThread().getName() + "获得许可:"  + num);  
  20.                     //模拟耗时的任务  
  21.                     for (int i = 0; i < 999999; i++) ;  
  22.                     //释放许可  
  23.                     semp.release();  
  24.                     System.out.println("线程" +   
  25.                         Thread.currentThread().getName() + "释放许可:"  + num);  
  26.                     System.out.println("当前允许进入的任务个数:" +  
  27.                         semp.availablePermits());  
  28.                 }catch(InterruptedException e){  
  29.                     e.printStackTrace();  
  30.                 }  
  31.             }  
  32.         };  
  33.           exec.execute(run);  
  34.     }  
  35.     //关闭线程池  
  36.     exec.shutdown();  
  37.     }  
  38. }  
    某次执行的结果如下:

线程pool-1-thread-1获得许可:0
线程pool-1-thread-1释放许可:0
当前允许进入的任务个数:5
线程pool-1-thread-2获得许可:1
线程pool-1-thread-6获得许可:5
线程pool-1-thread-4获得许可:3
线程pool-1-thread-8获得许可:7
线程pool-1-thread-2释放许可:1
当前允许进入的任务个数:2
线程pool-1-thread-5获得许可:4
线程pool-1-thread-8释放许可:7
线程pool-1-thread-3获得许可:2
线程pool-1-thread-4释放许可:3
线程pool-1-thread-10获得许可:9
线程pool-1-thread-6释放许可:5
线程pool-1-thread-10释放许可:9
当前允许进入的任务个数:2
线程pool-1-thread-3释放许可:2
当前允许进入的任务个数:1
线程pool-1-thread-5释放许可:4
当前允许进入的任务个数:3
线程pool-1-thread-7获得许可:6
线程pool-1-thread-9获得许可:8
线程pool-1-thread-7释放许可:6
当前允许进入的任务个数:5
当前允许进入的任务个数:3
当前允许进入的任务个数:3
当前允许进入的任务个数:3
线程pool-1-thread-9释放许可:8
当前允许进入的任务个数:5

    可以看出,Semaphore允许并发访问的任务数一直为5,当然,这里还很容易看出一点,就是Semaphore仅仅是对资源的并发访问的任务数进行监控,而不会保证线程安全,因此,在访问的时候,要自己控制线程的安全访问。





0 0
原创粉丝点击