1、volatile 关键字
谈谈你对volatile的理解
1.1、volatile 三大特性
volatile是java虚拟机提供的轻量级同步机制
可以将 volatile 看作是乞丐版的 synchronized 锁
- 保证内存可见性
- 禁止指令重排
- 不保证原子性
1.2、JMM 内存模型
1.2.1、谈谈 JMM
谈谈 JMM
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
内存可见性
- 由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域
- Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行
- 一个线程如果想要修改主内存中的变量,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存
- 线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
1.2.2、内存可见性
JMM volatile 的内存可见性
- 通过前面对JMM的介绍,我们知道:各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的
- 这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作
- 但此时A线程工作内存中的共享变量X对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题
代码示例:内存可见性
代码示例 1 :线程间内存不可见
- 代码:number 变量未加 volatile 关键字
public class VolatileDemo {
public static void main(String[] args) { volatileVisibilityDemo(); } /* 验证volatile的可见性 1.1 加入int number=0,number变量之前根本没有添加volatile关键字修饰,没有可见性 1.2 添加了volatile,可以解决可见性问题 */ private static void volatileVisibilityDemo() { System.out.println("可见性测试"); MyData myData = new MyData();//资源类 //启动一个线程操作共享数据 new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t come in"); try { TimeUnit.SECONDS.sleep(3); myData.setTo60(); System.out.println(Thread.currentThread().getName() + "\t update number value: " + myData.number); } catch (InterruptedException e) { e.printStackTrace(); } }, "AAA").start(); while (myData.number == 0) { //main线程持有共享数据的拷贝,一直为0 } System.out.println(Thread.currentThread().getName() + "\t mission is over. main get number value: " + myData.number); }
- }
class MyData {
int number = 0; public void setTo60() { this.number = 60; }
- } 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
- 程序运行结果:程序未能停下来
- 分析:
- 在上述程序中,两个线程:main 线程和 AAA 线程,同时对 myData 数据进行操作
- 由于 AAA 线程先睡眠了 3s ,所以 main 线程先拿到了 myData.number 的值,将该值拷贝回自己线程的工作内存,此时 myData.number = 0
- AAA 线程 3s 后醒来,将 myData.number 拷贝回自己线程的工作内存,修改为 60 后,写回主内存
- 但 AAA 线程将 myData.number 的值写回主内存后,并不会去通知 main 线程,所以 main 线程一直拿着自己线程的工作内存中的 myData.number = 0 ,搁那儿 while 循环呢
代码示例 2 :volatile 保证线程间内存的可见性
- 代码:number 变量加上 volatile 关键字
public class VolatileDemo {
public static void main(String[] args) { volatileVisibilityDemo(); } /* 验证volatile的可见性 1.1 加入int number=0,number变量之前根本没有添加volatile关键字修饰,没有可见性 1.2 添加了volatile,可以解决可见性问题 */ private static void volatileVisibilityDemo() { System.out.println("可见性测试"); MyData myData = new MyData();//资源类 //启动一个线程操作共享数据 new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t come in"); try { TimeUnit.SECONDS.sleep(3); myData.setTo60(); System.out.println(Thread.currentThread().getName() + "\t update number value: " + myData.number); } catch (InterruptedException e) { e.printStackTrace(); } }, "AAA").start(); while (myData.number == 0) { //main 线程收到通知后,会修改自己线程内存中的值 } System.out.println(Thread.currentThread().getName() + "\t mission is over. main get number value: " + myData.number); }
- }
class MyData {
// volatile可以保证可见性,及时通知其它线程主物理内存的值已被修改 volatile int number = 0; public void setTo60() { this.number = 60; }
- } 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
- 程序运行结果:停下来了哦
- 分析:由于有volatile 关键字的存在,当 AAA 线程修改了 myData.number 的值后,main 线程会受到通知,从而刷新自己线程工作内存中的值
1.2.3、原子性
原子性是什么?
原子性是不可分割,完整性。也即某个线程正在做某个具体业务时,中间不可以被加塞或者分割, 需要整体完成,要么同时成功,要么同时失败(类比数据库原子性)
代码示例:volatile 不保证原子性
- 代码
public class VolatileDemo {
public static void main(String[] args) { atomicDemo(); } /* 2 验证volatile不保证原子性 2.1 原子性是不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者分割。 需要整体完成,要么同时成功,要么同时失败。 2.2 volatile不可以保证原子性演示 2.3 如何解决原子性 1)加sync 2)使用我们的JUC下AtomicInteger */ private static void atomicDemo() { System.out.println("原子性测试"); MyData myData = new MyData(); for (int i = 1; i <= 20; i++) { new Thread(() -> { for (int j = 0; j < 1000; j++) { myData.addPlusPlus(); } }, String.valueOf(i)).start(); } /* 需要等待上述20个线程都计算完成后,再用main线程去的最终的结果是多少? 只要上述20个线程还有在执行的,main线程便礼让,让他们执行,直至最后只剩main线程 */ while (Thread.activeCount() > 2) { Thread.yield(); } System.out.println(Thread.currentThread().getName() + "\t int type finally number value: " + myData.number); }
- }
class MyData {
// volatile可以保证可见性,及时通知其它线程主物理内存的值已被修改 volatile int number = 0; public void setTo60() { this.number = 60; } //此时number前面已经加了volatile,但是不保证原子性 public void addPlusPlus() { number++; }
- } 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
- 程序运行结果
原子性测试 main int type finally number value: 19077 12
从字节码角度解释原子性
- java 源代码
public class T1 {
volatile int n = 0; public void add() { n++; }
- } 12345678910111213141516
- n++ 的字节码指令
0 aload_0 1 dup 2 getfield #2 <com/Heygo/T1.n> 5 iconst_1 6 iadd 7 putfield #2 <com/Heygo/T1.n> 10 return 1234567
n++ 分为三步
- 第一步:执行 getfield 指令拿到主内存中 n 的值
- 第二步:执行 iadd 指令执行加 1 的操作(线程工作内存中的变量副本值加 1)
- 第三步:执行 putfield 指令将累加后的 n 值写回主内存
PS :iconst_1 是将常量 1 放入操作数栈中,准备执行 iadd 操作
分析多线程写值,值丢失的原因
- 两个线程:线程 A和线程 B ,同时拿到主内存中 n 的值,并且都执行了加 1 的操作
- 线程 A 先执行 putfield 指令将副本的值写回主内存,线程 B 在线程 A 之后也将副本的值写回主内存
- 此时,就会出现写覆盖、丢失写值的情况
解决原子性问题:
两个解决办法:
- 对 addPlusPlus() 方法加同步锁(加锁这个解决方法太重)
- 使用 Java.util.concurrent.AtomicInteger 类
- 代码:使用 AtomicInteger 类保证 i++ 操作的原子性
public class VolatileDemo {
public static void main(String[] args) { atomicDemo(); } /* 2 验证volatile不保证原子性 2.1 原子性是不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者分割。 需要整体完成,要么同时成功,要么同时失败。 2.2 volatile不可以保证原子性演示 2.3 如何解决原子性 1)加sync 2)使用我们的JUC下AtomicInteger */ private static void atomicDemo() { System.out.println("原子性测试"); MyData myData = new MyData(); for (int i = 1; i <= 20; i++) { new Thread(() -> { for (int j = 0; j < 1000; j++) { myData.addPlusPlus(); myData.addAtomic(); } }, String.valueOf(i)).start(); } /* 需要等待上述20个线程都计算完成后,再用main线程去的最终的结果是多少? 只要上述20个线程还有在执行的,main线程便礼让,让他们执行,直至最后只剩main线程 */ while (Thread.activeCount() > 2) { Thread.yield(); } System.out.println(Thread.currentThread().getName() + "\t int type finally number value: " + myData.number); System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type finally number value: " + myData.atomicInteger); }
- }
class MyData {
// volatile可以保证可见性,及时通知其它线程主物理内存的值已被修改 volatile int number = 0; public void setTo60() { this.number = 60; } //此时number前面已经加了volatile,但是不保证原子性 public void addPlusPlus() { number++; } // Integer 原子包装类 AtomicInteger atomicInteger = new AtomicInteger(); public void addAtomic() { atomicInteger.getAndIncrement(); }
- } 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
- 程序运行结果
原子性测试 main int type finally number value: 17591 main AtomicInteger type finally number value: 20000 123
瞅瞅 AtomicInteger 源码
先获取再修改
- getAndIncrement() 方法
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } 123 - getAndDecrement() 方法
public final int getAndDecrement() { return unsafe.getAndAddInt(this, valueOffset, -1); } 123 - getAndAdd() 方法
public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); } 123 - 总结:以上方法都通过调用 unsafe.getAndAddInt() 实现
先修改再获取
- incrementAndGet() 方法
public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } 123 - decrementAndGet() 方法
public final int decrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, -1) - 1; } 123 - addAndGet() 方法
public final int addAndGet(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta) + delta; } 123 - 总结:以上方法都通过调用 unsafe.getAndAddInt() + delta 实现
1.2.4、代码重排
有序性
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种
理解指令重排序
- 指令重排序,就是出于优化考虑,CPU执行指令的顺序跟程序员自己编写的顺序不一致
- 就好比一份试卷,题号是老师规定的,是程序员规定的,但是考生(CPU)可以先做选择,也可以先做填空
- 单线程环境里面可以确保程序最终执行结果和代码顺序执行的结果一致
- 处理器在进行重排序时必须要考虑指令之间的数据依赖性
- 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
重排代码示例
示例 1
- 代码
public void mySort(){ int x = 11; //语句1 int y = 12; //语句2 x = x + 5; //语句3 y = x * x; //语句4 } 123456 - 以上代码,可能出现的执行顺序有1234、2134、1342,这三个都没有问题,但是语句 4 不能变成第一条,因为存在数据依赖(y 依赖于 x)。
示例 2
- 在代码中定义了 a, b, x, y 四个整形变量
- 线程 1 原本的执行顺序为
x = a; b = 1;
,线程 2 原本的执行顺序为y = b; a = 1;
- 但是经过指令重排后,指令执行顺序变化,导致程序执行结果变化
- 这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。
示例 3
- 代码
分析:
- 变量 a 与 flag 并没有数据依赖性,所以 a = 1; 与 flag = true; 语句无法保证谁先谁后
- 线程操作资源类,线程1访问method1,线程2访问method2,正常情况顺序执行,a=6
- 多线程下假设出现了指令重排,语句2在语句1之前,当执行完flag=true后,另一个线程马上执行method2,则会输出 a=5
禁止指令重排案例小结
- volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
- 我们先了解一个概念,内存屏障(Memory Barrfer)又称内存栅栏,是一个CPU指令,它的作用有两个:
- 一是保证特定操作的执行顺序
- 二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
- 由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。
- 内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
1.3、线程安全性保证
如何使线程安全性获得保证
- 工作内存与主内存同步延迟现象导致的可见性问题可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。
- 对于指令重排导致的可见性问题和有序性问题可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。