之前的时候看《并发编程的艺术》,书中提到dcl写法的单例模式是有问题的,有可能会导致调用者得到一个创建了一半的对象,从而导致报错。修复办法是将单例对象的引用添加volatile进行修饰,禁用重排序,则外界获取的就一定是已经创建好的对象了。
光说总是不行的,上代码:
public class SingleTest { private static SingleTest singleTest; // 这个应该用volatile修饰 //获取单例的方法 public static SingleTest getInstance() { if(singleTest == null){ synchronized (SingleTest.class){ if(singleTest == null){ singleTest = new SingleTest(); } } } return singleTest; } }
对于这一段的分析说的很清楚,网上也有大量的文章,但我有一个疑问:不是说synchronized有原子性、可见性么,而且可见性是通过monitor exit的时候强制刷新内容到主内存来实现的,既然这样,那synchornized结束前,没有刷新到内存,外面的程序应该读不到这个单例对象的值才对啊,为什么会读到呢?这个synchronized 的可见性究竟该怎么理解?
先说理解的错误之处:synchronized的可见性是通过monitor exit来保证的,这点没错,但monitor exit之前就不会刷新到主内存么,显然不是。现在jvm的机制,已经尽量快速的将改变同步到缓存了,这个机制是怎么确定的不清楚,但简单测试会发现非常短。
另外,synchronized 的可见性的正确理解是:对于被synchronized修饰的代码块,如果A线程执行结束,会强制刷新线程缓存内容到内存,同时通知其它synchronized修饰的线程x的值无效,需要重新读取(这点跟volatile很相似),因此B线程在执行的时候也就能读到A线程对x的修改了,这就是synchronized的可见性。
试一下如下示例:
//可见性验证 @Test public void testA() throws InterruptedException { //启动线程在不停监视str变化 Thread th1 = new Thread(() -> { while(true){ if(str.equals("b")){ System.out.println("th1 ==> str 已经被改为 b ," + Thread.currentThread()); } } }); Thread th2 = new Thread(() -> { while(true){ synchronized (str){ if(str.equals("b")){ System.out.println("th2 ==> str 已经被改为 b ," + Thread.currentThread()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }); th1.start(); th2.start(); //让监视线程都启动完毕 Thread.sleep(3000); System.out.println("修改str的值为b"); synchronized (str){ str = "b"; } Thread.sleep(3000); }
执行结果:
可以看到th1并没有输出,因为它线程中的str换出内容一致是“a”。实际上,33-35行可以不用synchronized,也会有相同结果,因为现在的jvm会尽最快速度将改变同步到缓存,而synchronized在执行的时候会重新读取,因此也会发现str的值被改变了,而th1则没有重新读取的机制,也就无法进行输出了。
对于monitor exit之前也会刷新到内存这点,也可以通过程序进行验证,可以在synchronized中修改某个值,然后sleep一段时间,这期间让另一个线程去读取被改变的值,会发现其实是可以读到的。