之前的时候看《并发编程的艺术》,书中提到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一段时间,这期间让另一个线程去读取被改变的值,会发现其实是可以读到的。