1.1 volatile是Java虚拟机提供的轻量级同步机制,三大特性:

  • 保证可见性 : 一个线程修改了主内存的值,其他线程能够看到知道,第一时间的通知机制就是可见性
  • 不保证原子性
  • 禁止指令重排

    1.2 JMM理解

  • jvm:java虚拟机

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

1648375484(1).png

JMM关于同步的定义

  1. 线程解锁前,必须把共享变量的值刷新到主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁解锁是同一把锁。

速率: 硬盘 < 内存 < 缓存cache < cpu

JMM的三大特性

可见性
  1. package com.interview.demo;
  2. import java.util.concurrent.TimeUnit;
  3. /**
  4. * @Author leijs
  5. * @date 2022/3/28
  6. */
  7. public class VolatileDemo {
  8. /**
  9. * 验证可见性
  10. *
  11. * @param args
  12. */
  13. public static void main(String[] args) {
  14. MyData myData = new MyData();
  15. new Thread(() -> {
  16. System.out.println(Thread.currentThread().getName() + " come in.");
  17. try {
  18. TimeUnit.SECONDS.sleep(3);
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. myData.addToo60();
  23. System.out.println(Thread.currentThread().getName() + "update " + myData.num);
  24. }).start();
  25. while (myData.num == 0) {
  26. }
  27. System.out.println(Thread.currentThread().getName() + " over!");
  28. }
  29. }
  30. class MyData {
  31. volatile int num = 0;
  32. public void addToo60() {
  33. this.num = 60;
  34. }
  35. }

各个线程对主内存中的共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作再回写到主内存中的。这就可能存在一个线程AAA修改了共享变量X的值但是还未写回主内存,另一个线程BBB又对主内存中的同一个共享变量X进行操作,但此时A线程工作内存中的共享变量X对B不可见,这种工作内存和主内存同步延迟现象就造成了可见性问题。

原子性

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

  1. package com.interview.demo;
  2. import java.util.concurrent.TimeUnit;
  3. /**
  4. * @Author leijs
  5. * @date 2022/3/28
  6. */
  7. public class VolatileDemo {
  8. public static void main(String[] args) throws InterruptedException {
  9. // seeOkByVolatile();
  10. MyData myData = new MyData();
  11. for (int i = 0; i < 20; i++) {
  12. new Thread(() -> {
  13. for (int j = 0; j < 1000; j++) {
  14. myData.addPlus();
  15. }
  16. }, String.valueOf(i)).start();
  17. }
  18. while (Thread.activeCount() > 2) {
  19. Thread.yield();
  20. }
  21. System.out.println(Thread.currentThread().getName() + ":" + myData.num);
  22. }
  23. /**
  24. * 验证可见性
  25. */
  26. private static void seeOkByVolatile() {
  27. MyData myData = new MyData();
  28. new Thread(() -> {
  29. System.out.println(Thread.currentThread().getName() + " come in.");
  30. try {
  31. TimeUnit.SECONDS.sleep(3);
  32. } catch (InterruptedException e) {
  33. e.printStackTrace();
  34. }
  35. myData.addToo60();
  36. System.out.println(Thread.currentThread().getName() + "update " + myData.num);
  37. }).start();
  38. while (myData.num == 0) {
  39. }
  40. System.out.println(Thread.currentThread().getName() + " over!");
  41. }
  42. }
  43. class MyData {
  44. volatile int num = 0;
  45. public void addToo60() {
  46. this.num = 60;
  47. }
  48. public void addPlus() {
  49. num++;
  50. }
  51. }

原因

num++:

  • 执行getField拿到初始值
  • 执行ipad进行+1操作
  • 执行putfield写把累加值写回主内存

如何解决
  • synchronized
  • AtomicInteger

    1. AtomicInteger automiInteger = new AtomicInteger();
    2. public void addAtomic() {
    3. automiInteger.incrementAndGet();
    4. }

    有序性

    计算机在执行程序时,为了提升性能,编译器和处理器常常会对指令做重排,一般分以下三种:

    源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行的指令

处理器在进行重排序时必须考虑指令之间的数据依赖性。多线程环境线程交替执行,由于编译器优化重排的存在,多线程使用的变量能否保证一致性是无法确定的,结果无法预测。

  1. package com.interview.demo;
  2. /**
  3. * @Author leijs
  4. * @date 2022/3/28
  5. */
  6. public class ReSortSeqDemo {
  7. int a = 0;
  8. boolean flag = false;
  9. public void method01() {
  10. // 这两个数据不存在依赖性,先后顺序多线程执行不确定
  11. a = 1;
  12. flag = true;
  13. }
  14. /**
  15. * 可能存在指令重排
  16. */
  17. public void method02() {
  18. if (flag) {
  19. a = a + 5;
  20. System.out.println("value: " + a);
  21. }
  22. }
  23. }

volatile禁止指令重排

volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。

内存屏障(memory barrier)

又称内存栅栏,是一个CPU指令,作用有两个:

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

由于编译器和处理器都能执行指令重排优化,也就是说通过插入内存屏障禁止内存屏障前后的指令进行重排序优化。内存屏障的另一作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

volatitle变量写操作时

写操作后加上一条store屏障指令,将工作内存中的共享变量刷新到主内存。
volatile的理解 - 图2

volatitle变量读操作时

写操作后加上一条load屏障指令,从主内存中读取共享变量
volatile的理解 - 图3

1.3 哪些地方使用过volatile?

单例模式DCL

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

  1. memory = allocate // 分配对象内存空间
  2. instance(memory) // 初始化对象
  3. instance = memory // 设置instance指向刚分配的内存地址,此时instance != null

步骤2和步骤3不存在数据依赖关系,重排前后在单线程中并没有改变,因此这种重排优化是允许的。
当一条线程访问instance != null的时候,由于instance实例未必初始化完成,也就造成了线程安全问题。

  1. package com.interview.demo;
  2. /**
  3. * @Author leijs
  4. * @date 2022/3/28
  5. */
  6. public class SingletonDemo {
  7. private static volatile SingletonDemo instance = null;
  8. private SingletonDemo() {
  9. System.out.println(Thread.currentThread().getName() + "我是构造方法");
  10. }
  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. // System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
  23. // System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
  24. // System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
  25. //
  26. // System.out.println();
  27. // 并发多线程,构造方法
  28. for (int i = 0; i < 10; i++) {
  29. new Thread(() -> {
  30. SingletonDemo.getInstance();
  31. }, String.valueOf(i)).start();
  32. }
  33. }
  34. }