volatile和synchronize关键字的区别和在单例模式中的应用
一、对于内存模型的三大特性:有序性、原子性、可见性。
1、原子性:
(1)原子的意思代表着——“不可分”;
(2)在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。
2、可见性
线程执行结果在内存中对其它线程的可见性。
变量经过volatile修饰后,对此变量进行写操作时,汇编指令中会有一个LOCK前缀指令,加了这个指令后,会引发两件事情:
发生修改后强制将当前处理器缓存行的数据写回到系统内存。
这个写回内存的操作会使得在其他处理器缓存了该内存地址无效,重新从内存中读取。
3、有序性
在本线程内观察,所有操作都是有序的(即指令重排不会导致单线程程序执行结果与排序前有任何差别)。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
二、线程安全的两个问题,执行控制和内存可见
(1)执行控制(synchronize):控制代码只能顺序执行(执行一次只能被一个线程执行)或者可以多线程并发执行。
(2)内存可见控制(volatile):线程执行结果在内存中对其它线程的可见性。线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。
volatile和synchronize两个关键字就是上述两种作用。
1.synchronize关键字使得同一时刻只有一个线程可以获得当前变量、方法、类的锁,其他线程无法访问,也就无法同步并发执行,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都happens-before于随后获得这个锁的线程的操作,保障有序性、可见性、原子性;
2. volatile通过强制将当前线程修改后的值写回内存并使得其他线程中该值无效的方式保证其可见性,通过禁止指令重排的方式保证有序性,具体为何不能保证原子性在下一部分讨论。
三、为什么volatile不能保证原子性
对于i=1这个赋值操作,由于其本身是原子操作,因此在多线程程序中不会出现不一致问题,但是对于i++这种复合操作,即使使用volatile关键字修饰也不能保证操作的原子性,可能会引发数据不一致问题。
private volatile int i = 0;
i++;
如果启了500条线程并发地去执行i++这个操作 最后的结果i是小于500的
i++操作可以被拆分为三步:
1,线程读取i的值
2、i进行自增计算
3、刷新回i的值
1、线程读取i
2、temp = i + 1
3、i = temp
当 i=5 的时候A,B两个线程同时读入了 i 的值, 然后A线程执行了 temp = i + 1的操作,
要注意,此时的 i 的值还没有变化,然后B线程也执行了 temp = i + 1的操作,注意,此时A,B两个线程保存的 i 的值都是5,temp 的值都是6,
然后A线程执行了 i = temp (6)的操作,此时i的值会立即刷新到主存并通知其他线程保存的 i 值失效,
此时B线程需要重新读取 i 的值那么此时B线程保存的 i 就是6,同时B线程保存的 temp 还仍然是6,
然后B线程执行 i=temp (6),所以导致了计算结果比预期少了1。
四、volatile和synchronized的区别
1、volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2、volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的 。
3、volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性 。
4、volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
5、volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化 。
五、volatile和synchronized在单例模式中的应用
1、懒汉式,线程安全
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。
2、DCL(双端检锁机制)
public class SingleDclDemo {
private volatile static SingleDclDemo instance = null;
private SingleDclDemo(){
System.out.println("我是构造方法");
}
//不锁整个方法,上锁前后进行非空判断
public static SingleDclDemo getInstance(){
if(instance ==null){
synchronized (SingleDclDemo.class){
if(instance ==null) {
instance = new SingleDclDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
//单线程模式下,调用返回true
// for (int i = 0; i < 3; i++) {
// System.out.println(getInstance() == getInstance() );
// System.out.println(getInstance() == getInstance() );
// System.out.println(getInstance() == getInstance() );
// }
//多线程调用不加volatile时,有很小的几率出现false
for (int j = 0; j < 10; j++) {
new Thread(()->{
System.out.println(getInstance() == getInstance() );
}).start();
}
}
}
DCL模式线程不安全
DCL机制不一定线程安全,因为有指令重排的存证
instance = new SingletomDemo();可以分为3步
memory = allocate(); 1:分配内存空间
instance(memory); 2:初始化对象
instance = memory; 3:设置instance指向刚分配的内存空间,instance !=null
由于2,3不存在依赖关系,所以2,3可能会被指令重排
调整成
memory = allocate(); 1:分配内存空间
instance = memory; 3:设置instance指向刚分配的内存空间,instance !=null
instance(memory); 2:初始化对象
这样就会导致instance 虽然不为null,但是取到后的值,却没有被初始化。
所以需要对instance加上volatile ,来禁止指令重排。