下边拿dcl单例模式进行举例说明:
public class Singleton {
private int num=100;
private static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
曾经误以为是jmm的模型中,后续线程T2中工作内存副本中先得到了null(通过了第一次null判断,之后别的线程T1获取锁创建了实例但是未能及时通知T2改变)未被及时通知变化导致的了,所以导致进入在第二次为空判断时还是null,后来知道这么理解是错误的,看下边红字就知道了
补充一些知识:
java内存模型与synchronized关键字
synchronized关键字强制实施一个互斥锁,使得被保护的代码块在同一时间只能有一个线程进入并执行。当然synchronized还有另外一个 方面的作用:在线程进入synchronized块之前,会把工作存内存中的所有内容映射到主内存上,然后把工作内存清空再从主存储器上拷贝最新的值(经测试sleep,wait,park这些主动释放cup的操作都有类似作用)。而 在线程退出synchronized块时,同样会把工作内存中的值映射到主内存,但此时并不会清空工作内存。这样一来就可以强制其按照上面的顺序运行,以 保证线程在执行完代码块后,工作内存中的值和主内存中的值是一致的,保证了数据的一致性!
所以由synchronized修饰的set与get方法都是相当于直接对主内存进行操作,不会出现数据一致性方面的问题。
真正原因:
singleton = new Singleton();
这一步实际的操作可被分为了三步:
1分配内存空间属性赋零值num=0(零值不应定就是0还有可能是false等);
2属性赋初值或构造函数值num=100
3将singleton位置指向此处内存空间
但是这三步有可能被cpu进行指重排序,导致T1在创建singleton时先1后3(则此时singleton已有null变为有真正的指向),若这时T2得到cup进来了发现singleton已不为null,便将其拿入工作副本实用,则会出现问题;
这种概率极小但一旦发生在某些业务中可能就是致命的,所以此处需要禁止指令重排序,而volatile便有这个功能(可见性,禁止指令重排序)
代码改为:
public class Singleton {
private int num=100;
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
补充一个关于volitail小知识:
伪共享问题:volitail 会触发缓存一致性[intel用的是MESI协议],缓存一致性每次都是按照行为单位触发的(缓存行的概念),行内可能有多个变量比如A,B 一旦A失效整个行的变量都将失效,(所以如果线程1只用了volitail A,线程2只用了volitail B 但是两个线程的缓存行却都是A和B,那就会触发不必要的缓存一致性同步)