【JUC】如何理解Java的volatile关键字?
并发和并行的区别
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在,并发能够在单处理器系统中存在是因为并发是并行的假象,并行要求程序能够同时执行多个操作,而并发只是要求程序假装同时执行多个操作(每个小时间片执行一个操作,多个操作快速切换执行)。
如何理解volatile?
- 保证可见性
- 不保证原子性
- 禁止指令重排(有序)
Java内存模型(JMM)
- 保证可见性
- 保证原子性【volatile不可以保证原子性】
- 保证有序性
volatile的可见性保证
为什么volitale不能保证原子性?
Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i(先读取i的值,再将i的值赋值给j)或者i++(拿i的值,值加一,把值写回i)这样的操作都不是原子操作。
所以,如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性了。
一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作,i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值写会到缓存中。
线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。
线程A已经读取到了i的值为100,也就是说读取的这个原子操作已经结束了(读取比较的还是B修改之前的值,读取结束后才通知到A主存的值已经改了,但这时候A不和主存比较了),所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存,所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。
如何解决volitale不保证原子性的问题?
-
使用synchronized,因为synchronizated对性能影响较大,没必要杀鸡用牛刀。
-
使用java.util.concurrent.atomic (原子):AtomicInteger atomicInteger = new AtomicInteger();atomicInteger.getAndIncrement();【juc包的AtomicInteger的底层原理是CAS】
volitale禁止指令重排
线程安全性如何得到保证?
volatile应用场景
多线程中单例模式的实现
1 package day01Volatile; 2 3 public class SingletonDemo { 4 5 private static volatile SingletonDemo instance = null; 6 7 private SingletonDemo(){ 8 System.out.println(Thread.currentThread().getName()+"构造方法"); 9 } 10 11 //单线程的单例模式的写法 12 public static SingletonDemo getInstance1(){ 13 if(instance == null){ 14 instance = new SingletonDemo(); 15 } 16 return instance; 17 } 18 //多线程的单例模式的写法 19 public static SingletonDemo getInstance2(){ 20 if(instance == null){ 21 synchronized (SingletonDemo.class){ 22 if(instance == null){//加锁后判断一次 23 instance = new SingletonDemo(); 24 } 25 } 26 } 27 return instance; 28 } 29 30 public static void main(String[] args) { 31 for (int i = 0; i < 10; i++) { 32 new Thread(()->{ 33 // getInstance1(); 34 getInstance2(); 35 }).start(); 36 } 37 } 38 }
- 分配对象的内存空间 memory=allocate()
- 初始化对象 new SingletonDemo()
- 设置instance执行刚分配的内存地址,这时instance != null
创建单例对象有三步:分配对象的内存空间、初始化对象、设置对象的引用指向分配的内存地址。第二步和第三步不存在数据依赖关系,可能会出现重排优化。指令重排后,则会出现问题。初始化完成前,就指向内存地址,instance==null。
指令重排只能保证串行语义的执行一致性,不能保证多线程。所以要用volatile禁止指令重排。