聊聊并发编程的10个坑( 二 )


为了解决饿汉模式和懒汉模式各自的问题,于是出现了:双重检查锁 。
具体代码如下:
public class SimpleSingleton4 {private static SimpleSingleton4 INSTANCE;private SimpleSingleton4() {}public static SimpleSingleton4 getInstance() {if (INSTANCE == null) {synchronized (SimpleSingleton4.class) {if (INSTANCE == null) {INSTANCE = new SimpleSingleton4();}}}return INSTANCE;}}需要在synchronized前后两次判空 。
但我要告诉你的是:这段代码有漏洞的 。
有什么问题?
public static SimpleSingleton4 getInstance() {if (INSTANCE == null) {//1synchronized (SimpleSingleton4.class) {//2if (INSTANCE == null) {//3INSTANCE = new SimpleSingleton4();//4}}}return INSTANCE;//5}getInstance方法的这段代码,我是按1、2、3、4、5这种顺序写的,希望也按这个顺序执行 。
但是java虚拟机实际上会做一些优化,对一些代码指令进行重排 。重排之后的顺序可能就变成了:1、3、2、4、5,这样在多线程的情况下同样会创建多次实例 。重排之后的代码可能如下:
public static SimpleSingleton4 getInstance() {if (INSTANCE == null) {//1if (INSTANCE == null) {//3synchronized (SimpleSingleton4.class) {//2INSTANCE = new SimpleSingleton4();//4}}}return INSTANCE;//5}原来如此,那有什么办法可以解决呢?
答:可以在定义INSTANCE是加上volatile关键字 。具体代码如下:
public class SimpleSingleton7 {private volatile static SimpleSingleton7 INSTANCE;private SimpleSingleton7() {}public static SimpleSingleton7 getInstance() {if (INSTANCE == null) {synchronized (SimpleSingleton7.class) {if (INSTANCE == null) {INSTANCE = new SimpleSingleton7();}}}return INSTANCE;}}volatile关键字可以保证多个线程的可见性,但是不能保证原子性 。同时它也能禁止指令重排 。
双重检查锁的机制既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间 。
此外,如果你想了解更多单例模式的细节问题,可以看看我的另一篇文章《单例模式,真不简单》
3. volatile的原子性从前面我们已经知道volatile,是一个非常不错的关键字,它能保证变量在多个线程中的可见性,它也能禁止指令重排,但是不能保证原子性 。
使用volatile关键字禁止指令重排,前面已经说过了,这里就不聊了 。
可见性主要体现在:一个线程对某个变量修改了,另一个线程每次都能获取到该变量的最新值 。
先一起看看反例:
public class VolatileTest extends Thread {privateboolean stopFlag = false;public boolean isStopFlag() {return stopFlag;}@Overridepublic void run() {try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}stopFlag = true;System.out.println(Thread.currentThread().getName() + " stopFlag = " + stopFlag);}public static void main(String[] args) {VolatileTest vt = new VolatileTest();vt.start();while (true) {if (vt.isStopFlag()) {System.out.println("stop");break;}}}}上面这段代码中,VolatileTest是一个Thread类的子类,它的成员变量stopFlag默认是false,在它的run方法中修改成了true 。
然后在main方法的主线程中,用vt.isStopFlag()方法判断,如果它的值是true时,则打印stop关键字 。
那么,如何才能让stopFlag的值修改了,在主线程中通过vt.isStopFlag()方法,能够获取最新的值呢?
正例如下:
public class VolatileTest extends Thread {private volatile boolean stopFlag = false;public boolean isStopFlag() {return stopFlag;}@Overridepublic void run() {try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}stopFlag = true;System.out.println(Thread.currentThread().getName() + " stopFlag = " + stopFlag);}public static void main(String[] args) {VolatileTest vt = new VolatileTest();vt.start();while (true) {if (vt.isStopFlag()) {System.out.println("stop");break;}}}}用volatile关键字修饰stopFlag即可 。
下面重点说说volatile的原子性问题 。
使用多线程给count加1,代码如下:
public class VolatileTest {public volatile int count = 0;public void add() {count++;}public static void main(String[] args) {final VolatileTest test = new VolatileTest();for (int i = 0; i < 20; i++) {new Thread() {@Overridepublic void run() {for (int j = 0; j < 1000; j++) {test.add();}};}.start();}while (Thread.activeCount() > 2) {//保证前面的线程都执行完Thread.yield();}System.out.println(test.count);}}执行结果每次都不一样,但可以肯定的是count值每次都小于20000,比如:19999 。
这个例子中count是成员变量,虽说被定义成了volatile的,但由于add方法中的count++是非原子操作 。在多线程环境中,count++的数据可能会出现问题 。


推荐阅读