1.1 volatile 是什么

volatile 是 Java 虚拟机提供的轻量级的同步机制

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

    1.2 JMM 内存模型之可见性

    JMM(Java 内存模型 Java Memory Model,简称 JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
    JMM 关于同步的规定:

  4. 线程解锁前,必须把共享变量的值刷新回主内存

  5. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  6. 加锁解锁是同一把锁

由于 JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而 Java 内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
image.png
image.png

1.3 可见性的代码验证说明

  1. 验证 volatile 的可见性

假如 int number = 0; number 变量之前根本没有添加 volatile 关键字修饰,没有可见性

  1. package s02.e01;
  2. import java.util.concurrent.TimeUnit;
  3. class MyData {
  4. int number = 0;
  5. public void addTo60() {
  6. this.number = 60;
  7. }
  8. }
  9. public class VolatileDemo {
  10. public static void main(String[] args) {
  11. MyData myData = new MyData();
  12. new Thread(() -> {
  13. System.out.println(Thread.currentThread().getName() + "\t come in");
  14. try {
  15. TimeUnit.SECONDS.sleep(3);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. myData.addTo60();
  20. System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
  21. }, "aaa").start();
  22. while (myData.number == 0) {
  23. // main 线程就一直再这里等待循环,直到 number 值不再等于零
  24. }
  25. System.out.println(Thread.currentThread().getName() + "\t mission is over");
  26. }
  27. }

image.png
添加了 volatile,可以解决可见性问题

  1. package s02.e01;
  2. import java.util.concurrent.TimeUnit;
  3. class MyData {
  4. volatile int number = 0;
  5. public void addTo60() {
  6. this.number = 60;
  7. }
  8. }
  9. public class VolatileDemo {
  10. public static void main(String[] args) {
  11. MyData myData = new MyData();
  12. new Thread(() -> {
  13. System.out.println(Thread.currentThread().getName() + "\t come in");
  14. try {
  15. TimeUnit.SECONDS.sleep(3);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. myData.addTo60();
  20. System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
  21. }, "aaa").start();
  22. while (myData.number == 0) {
  23. // main 线程就一直再这里等待循环,直到 number 值不再等于零
  24. }
  25. System.out.println(Thread.currentThread().getName() + "\t mission is over, main get number value:" + myData.number);
  26. }
  27. }

image.png
通过前面对 JMM 的介绍,我们知道,各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。这就可能存在一个线程 AAA 修改了共享变量 X 的值但还未写回主内存时,另外一个线程 BBB 又对主内存中同一个共享变量 X 进行操作,但此时 A 线程工作内存中共享变量 x 对线程 B 来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题。

1.4 volatile 不保证原子性

原子性指的是什么意思?
不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整要么同时成功,要么同时失败。

  1. package s02.e01;
  2. import java.util.concurrent.TimeUnit;
  3. class MyData {
  4. volatile int number = 0;
  5. public void addTo60() {
  6. this.number = 60;
  7. }
  8. // 请注意,此时 number前面是加了 voLatile 关键字修饰的,voLatile 不保证原子性
  9. public void addPlusPlus() {
  10. number++;
  11. }
  12. }
  13. public class VolatileDemo {
  14. public static void main(String[] args) {
  15. MyData myData = new MyData();
  16. for (int i = 0; i < 20; i++) {
  17. new Thread(() -> {
  18. for (int j = 0; j < 1000; j++) {
  19. myData.addPlusPlus();
  20. }
  21. }, String.valueOf(i)).start();
  22. }
  23. // 需要等待上面 20 个线程都全部计算完成后,再用 main 线程取得最终的结果值看是多少?
  24. while (Thread.activeCount() > 2) {
  25. Thread.yield();
  26. }
  27. System.out.println(Thread.currentThread().getName() + "\t finally number value:" + myData.number);
  28. }
  29. }

image.png
image.png
number++ 在多线程下是非线程安全的,如何不加 synchronized 解决?
image.png
使用我们的 juc 下 AtomicInteger

  1. package s02.e01;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.atomic.AtomicInteger;
  4. class MyData {
  5. volatile int number = 0;
  6. public void addTo60() {
  7. this.number = 60;
  8. }
  9. // 请注意,此时 number前面是加了 voLatile 关键字修饰的,voLatile 不保证原子性
  10. public void addPlusPlus() {
  11. number++;
  12. }
  13. AtomicInteger atomicInteger = new AtomicInteger();
  14. public void addMyAtomic() {
  15. atomicInteger.getAndIncrement();
  16. }
  17. }
  18. public class VolatileDemo {
  19. public static void main(String[] args) {
  20. MyData myData = new MyData();
  21. for (int i = 0; i < 20; i++) {
  22. new Thread(() -> {
  23. for (int j = 0; j < 1000; j++) {
  24. myData.addPlusPlus();
  25. myData.addMyAtomic();
  26. }
  27. }, String.valueOf(i)).start();
  28. }
  29. // 需要等待上面 20 个线程都全部计算完成后,再用 main 线程取得最终的结果值看是多少?
  30. while (Thread.activeCount() > 2) {
  31. Thread.yield();
  32. }
  33. System.out.println(Thread.currentThread().getName() + "\t int type, finally number value:" + myData.number);
  34. System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type, finally number value:" + myData.atomicInteger);
  35. }
  36. }

image.png
image.png

1.5 有序性

计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种
image.png
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。处理器在进行重排序时必须要考虑指令之间的数据依赖性。多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

1.5.1 指令重排 1

  1. public void mySort() {
  2. int x = 11; // 语句1
  3. int y = 12; // 语句2
  4. x = x + 5; // 语句3
  5. y = x * x; // 语句4
  6. }

问题:请问语句 4 可以重排后变成第一个条吗?
答:不可以,处理器在进行重排序时必须要考虑指令之间的数据依赖性。

1.5.2 指令重排 2

  1. int a,b,x,y = 0;
线程 1 线程 2
x = a; y = b;
b = 1; a = 2;
x = 0 y = 0

如果编译器对这段程序代码执行重排优化后,可能出现下列情况

线程1 线程2
b =1; a = 2;
x = a; y = b;
x = 2 y = 1

这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。

  1. package s02.e01;
  2. public class ReSortSeqDemo {
  3. int a = 0;
  4. boolean flag = false;
  5. public void method01() {
  6. a = 1; // 语句1
  7. flag = true; // 语句2
  8. }
  9. // 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
  10. // 语句 1 和 语句 2 的顺序可能会发生改变
  11. public void method02() {
  12. if (flag) {
  13. a = a + 5; // 语句3
  14. System.out.println("*****retValue: " + a);
  15. }
  16. }
  17. }

1.5.3 禁止指令重排小总结

volatile 实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个 CPU 指令,它的作用有两个:

  1. 保证特定操作的执行顺序。
  2. 保证某些变量的内存可见性(利用该特性实现 volatile 的内存可见性)。

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条 Memory Barrier 则会告诉编译器和 CPU,不管什么指令都不能和这条 Memory Barrier 指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种 CPU 的缓存数据,因此任何 CPU 上的线程都能读取到这些数据的最新版本。
对 Volatile 变量进行写操作时,会在写操作后加入一条 store 屏障指令,将工作内存中的共享变量值刷新回到主内存。
image.png
对 Volatile 变量进行读操作时,会在读操作前加入一条 load 屏障指令,从主内存中读取共享变量。
image.png

1.6 线程安全性获得保证

工作内存与主内存饲步延迟现象导致的可见性问题可以使用 synchronized 或 volatile 关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。
对于指令重排导致的可见性问题和有序性问题可以利用 volatile 关键字解决,因为 volatile 的另外一个作用就是禁止重排序优化。

1.7 你在哪些地方用到过 volatile

1.7.1 单例模式 DCL 代码

  1. 单线程 ```java package s02.e01;

public class SingletonDemo {

  1. private static SingletonDemo instance = null;
  2. private SingletonDemo() {
  3. System.out.println(Thread.currentThread().getName() + "\t我是构造方法singletonDemo()");
  4. }
  5. public static SingletonDemo getInstance() {
  6. if (instance == null) {
  7. instance = new SingletonDemo();
  8. }
  9. return instance;
  10. }
  11. public static void main(String[] args) {
  12. // 单线程(main 线程的操作动作......)
  13. System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
  14. System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
  15. System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
  16. }

}

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/390086/1644215457188-8ff27086-d382-4a38-8c4b-bae25f91f02f.png#clientId=u973af9ca-8b86-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=86&id=u35134174&margin=%5Bobject%20Object%5D&name=image.png&originHeight=86&originWidth=286&originalType=binary&ratio=1&rotation=0&showTitle=false&size=3623&status=done&style=none&taskId=udba2114f-cbec-4f24-a9c5-11fd0555f42&title=&width=286)
  2. 2. 多线程
  3. ```java
  4. package s02.e01;
  5. public class SingletonDemo {
  6. private static SingletonDemo instance = null;
  7. private SingletonDemo() {
  8. System.out.println(Thread.currentThread().getName() + "\t我是构造方法singletonDemo()");
  9. }
  10. public static SingletonDemo getInstance() {
  11. if (instance == null) {
  12. instance = new SingletonDemo();
  13. }
  14. return instance;
  15. }
  16. public static void main(String[] args) {
  17. // 并发多线程后,情况发生了很大的变化
  18. for (int i = 1; i <= 10; i++) {
  19. new Thread(() -> {
  20. SingletonDemo.getInstance();
  21. }, String.valueOf(i)).start();
  22. }
  23. }
  24. }

image.png

  1. 方法添加 synchronized,太重 ```java package s02.e01;

public class SingletonDemo {

  1. private static SingletonDemo instance = null;
  2. private SingletonDemo() {
  3. System.out.println(Thread.currentThread().getName() + "\t我是构造方法singletonDemo()");
  4. }
  5. public static synchronized SingletonDemo getInstance() {
  6. if (instance == null) {
  7. instance = new SingletonDemo();
  8. }
  9. return instance;
  10. }
  11. public static void main(String[] args) {
  12. // 并发多线程后,情况发生了很大的变化
  13. for (int i = 1; i <= 10; i++) {
  14. new Thread(() -> {
  15. SingletonDemo.getInstance();
  16. }, String.valueOf(i)).start();
  17. }
  18. }

}

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/390086/1644215679199-d708b952-5690-4dbf-85dd-7e1643d29702.png#clientId=u973af9ca-8b86-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=25&id=u6d69c1d2&margin=%5Bobject%20Object%5D&name=image.png&originHeight=25&originWidth=244&originalType=binary&ratio=1&rotation=0&showTitle=false&size=2687&status=done&style=none&taskId=u44b7e67a-22ab-4eea-9201-d9baaf48be0&title=&width=244)
  2. 4. DCL(DooubLe Check Lock 双端检锁机制),在锁的前后都做判断
  3. ```java
  4. package s02.e01;
  5. public class SingletonDemo {
  6. private static SingletonDemo instance = null;
  7. private SingletonDemo() {
  8. System.out.println(Thread.currentThread().getName() + "\t我是构造方法singletonDemo()");
  9. }
  10. // DCL(DoubLe Check Lock 双端检锁机制)
  11. public static SingletonDemo getInstance() {
  12. if (instance == null) {
  13. synchronized (SingletonDemo.class) {
  14. if (instance == null) {
  15. instance = new SingletonDemo();
  16. }
  17. }
  18. }
  19. return instance;
  20. }
  21. public static void main(String[] args) {
  22. // 并发多线程后,情况发生了很大的变化
  23. for (int i = 1; i <= 10; i++) {
  24. new Thread(() -> {
  25. SingletonDemo.getInstance();
  26. }, String.valueOf(i)).start();
  27. }
  28. }
  29. }

image.png

1.7.2 单例模式 volatile 分析

DCL(双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入 volatile 可以禁止指令重排
原因在于某一个线程执行到第一次检测,读取到的 instance 不为 null 时,instance 的引用对象可能没有完成初始化。
instance = new SingletonDemo();可以分为以下 3 步完成(伪代码)

  1. 分配对象内存空间

    1. memory = allocate();
  2. 初始化对象

    1. instance(memory);
  3. 设置 instance 指向刚分配的内存地址,此时 instance! =null

    1. instance = memory;

    步骤 2 和步骤 3 不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。

  4. 分配对象内存空间

    1. memory = allocate();
  5. 设置 instance 指向刚分配的内存地址,此时 instance! =null,但是对象还没有初始化完成!

    1. instance = memory;
  6. 初始化对象

    1. instance(memory);

    但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问 instance 不为 null 时,由于 instance 实例未必已初始化完成,也就造成了线程安全问题。(取的对象地址不为空,通过了双端检锁机制的判断,但是用的时候发现值为 null) ```java package s02.e01;

public class SingletonDemo {

  1. private static volatile SingletonDemo instance = null;
  2. private SingletonDemo() {
  3. System.out.println(Thread.currentThread().getName() + "\t我是构造方法singletonDemo()");
  4. }
  5. // DCL(DoubLe Check Lock 双端检锁机制)
  6. public static SingletonDemo getInstance() {
  7. if (instance == null) {
  8. synchronized (SingletonDemo.class) {
  9. if (instance == null) {
  10. instance = new SingletonDemo();
  11. }
  12. }
  13. }
  14. return instance;
  15. }
  16. public static void main(String[] args) {
  17. // 并发多线程后,情况发生了很大的变化
  18. for (int i = 1; i <= 10; i++) {
  19. new Thread(() -> {
  20. SingletonDemo.getInstance();
  21. }, String.valueOf(i)).start();
  22. }
  23. }

} ```