JMM(Java Memory Model) Java内存模型

定义了一组规范,当多线程访问一个主内存中的共享变量时,并不是直接操作主内存中的共享变量,而是将主内存中的变量拷贝到各自线程的工作内存中,在各自的工作内存中进行数据的操作,操作完成之后再写回给主内存。

多线程下的三大特性

  • 原子性

指一段操作,是不可中断的,一旦开始,不能被其他线程打断

  • 有序性(禁止指令重排)

指在单线程的情况下,同一段代码的执行结果是相同的

  • 可见性

多线程操作共享变量,线程A的操作结果对线程B可见。

JMM对上述三个特性的支持

JMM仅支持有序性和可见性,并不保证原子性

原子性:

当多个线程同时操作同一数据时,首先需要将主内存的数据拷贝至各自线程的工作空间中,在各自的工作空间中进行数据的改变,改变完成之后将数据写回給主内存,但由于多线程的调度问题,在多线程操作时,存在着同一操作重复写的情况,(例如++操作,一个线程正准备往主内存写回数据时,被挂起,另一个线程写回数据后,此线程由于内存的可见性,本应该获取新的数据再次++,但却未如此,而是直接将数据写回给主内存,导致了数据重复写,丢失数据的情况)。使用atomic各种类可以使volatile保证原子性

有序性:

编译器/优化器会将源代码的执行顺序按照自己认为最优的方式进行重排(前提为重排之后的命令没有数据依赖关系 例如int x=5, int y=6, x= x +5, y= xx,不可能将y=xx排到第一条语句中),但是如此进行指令重排的话,在多线程环境下,操作同一个数据,如果发生指令重排,将会导致多线程运行结果的不确定性( int a,b,x,y=0, 1线程 x=a, b=1 2线程y=b, a=2,不发生指令重排,结果为x=0,y=0, 发生指令重排,可能导致1线程变为b=1,x=a,2线程a=2,y=b,在多线程调度的情况下,发生指令重排会导致运行结果变成x=2,y=1,导致了多线程运行结果的不确定),所以可以使用volatile进行禁止指令重排,导致运行结果的一致性

volatile 实现有序性,是通过添加内存屏障来实现的,什么是内存屏障,参照附录一:https://www.yuque.com/zhanyifan-rkxpe/grf7g5/gi2zb4#pMh4h

可见性:

当一个线程修改了自己工作控件的变量,并写回给主内存,要及时通知给其他线程,这种特性叫做jmm的第一个特性, 内存可见性

单例模式-双端检索机制下的问题

  1. // 但这种方法并不能完全解决多线程的单例模式问题,
  2. // 有几率存在着错误,因为存在着指令重排的问题,
  3. // 会导致在new 实例的时候发生指令重排,在new实例中分为三步,
  4. // 1. 分配空间,2. 调用构造函数,3. 将实例指向刚分配的空间,有可能2,3发生重排,
  5. // 导致instance在实例没初始化结束(未调用构造函数结束)时,另一个线程进入判断,结果不为空。
  6. // 所以最终的方式应该采用双端检测机制+在需要单例模式的变量前加上volatile关键字,
  7. // 禁止指令重排,从而解决问题
  8. ifinstance == null){
  9. synchronized Singleton.class){
  10. ifinstance== null){
  11. instance = new Singleton()
  12. }
  13. }
  14. }

CAS(Compare And Swap) 比较并交换

CAS 是什么

CAS的全称是Compare-And-Swap,它是一条CPU并发原语
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中CAS方法,JVM会帮我们实现CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现原子小左。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干指令组成的,用语完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致性问题。
比较当前工作内存中的值和主内存的值是否相同,如果相同执行操作,否则继续比较直到主内存和工作内存的值一致为止

CAS的实现原理

采用自旋锁+sun.misc.Unsafe类

CAS举例(AtomicInteger)

初始化unsafe 以及 value值的偏移量

image.png

获取并自增

  1. 调用unsafe,将初始化计算出的value属性的偏移量(offset)传入
  2. 根据传入的偏移量(offset)获取出该内存地址上的变量值
  3. 底层调用native方法,直接去比较内存地址上的变量值,是否与传入的期望值相同,如果相同,将新值(var5+var4)设置到该内存地址中,结束循环,设置成功。否则,重新循环2-3步,直到设置成功

    1. public final int getAndIncrement() {
    2. return unsafe.getAndAddInt(this, valueOffset, 1);
    3. }
    4. // unsafe
    5. public final int getAndAddInt(Object var1, long var2, int var4) {
    6. int var5;
    7. do {
    8. var5 = this.getIntVolatile(var1, var2);
    9. } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    10. return var5;
    11. }
    12. // native method
    13. public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

    详细流程解释

    假设有两个线程(threadA,threadB)同时执行getAndIncrement操作

  4. AtomicInteger中的value值,假设为2,即主内存中的AtomicInteger中的value为2,根据JMM内存模型,threadA和threadB各自拷贝了一份变量副本(value = 2)到各自的工作内存中。

  5. threadA执行getIntVolatile(var1,var2) 拿到value的值为2,假设此时threadA被挂起
  6. threadB此时也通过getIntVolatile(var1,var2)拿到value的值为2,此时threadB还有时间片,接着执行 compareAndSwapInt()方法,比较内存地址中的值也是2,成功修改内存中的值为3,。threadB执行完毕。
  7. 此时threadA,执行compareAndSwapInt(),但是此时内存地址中的值是3,和threadA工作内存中的值2不相等,说明这个变量已经被其他线程先一步修改过了,compareAndSwapInt返回false,只能在此循环,重新getIntVolatile获取主内存中新的变量值
  8. threadA重新获取value的值,因为value的值被volatile修改,所以对其他线程是可见的,所以threadA循环4-5,直到成功。

    CAS的缺点

  9. 采用自旋锁,循环时间长,开销大

  10. 只能保证一个共享变量的原子性
  11. ABA问题

    ABA问题描述与解决方案

    何为ABA问题

    当多线程CAS在进行compareAndSet方法的时候(例如AtomicInteger),仅判断了原有值与当前工作空间的值是否相同,那么,假设A线程操作需要10秒中,B线程操作需要2秒中,在A还没将数据写回给主内存的情况下,B线程可以对主内存的数据进行多次操作,即可以这样说,主内存的值为100,A,B各自的工作空间中都为100初始值,B线程第一次将修改为10,第二次修改为20,第三次修改成100,并写回主内存。那么对于A线程来说,在使用compareAndSet(CAS)方法来判断主内存的值和当前工作区的值的话,那么,此次在A线程的修改就能够成功,因为A线程并不知道B线程将值又修改回了主内存的原有值,但并不代表这种过程是没有问题的。也就产生了狸猫换太子的情况

    问题代码演示

    1. public void testABA() {
    2. AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    3. new Thread(() -> {
    4. atomicReference.compareAndSet(100,101);
    5. atomicReference.compareAndSet(101,100);
    6. },"threadA").start();
    7. new Thread(() -> {
    8. try {
    9. TimeUnit.SECONDS.sleep(1);
    10. } catch (InterruptedException e) {
    11. e.printStackTrace();
    12. }
    13. boolean b = atomicReference.compareAndSet(100, 2020);
    14. System.out.println("compare result: " + b);
    15. Integer integer = atomicReference.get();
    16. System.out.println("atomic value:" + integer);
    17. }, "threadB").start();
    18. try {
    19. TimeUnit.SECONDS.sleep(3);
    20. } catch (InterruptedException e) {
    21. e.printStackTrace();
    22. }
    23. }
    24. // 输出结果
    25. compare result: true
    26. atomic value:2020

    ABA问题解决:

    在每次修改成功的时候加上Stamp版本号,修改之前比较当前的stamp版本号是否正确,符合则进行修改,否则修改失败,使用 AtomicStampedReference 添加版本号进行控制

    1. public void testABASolution() {
    2. AtomicStampedReference<Integer> atomicReference = new AtomicStampedReference<>(100,1);
    3. new Thread(() -> {
    4. System.out.println(Thread.currentThread().getName() + "\n 第一次的版本号:" + atomicReference.getStamp());
    5. try {
    6. TimeUnit.SECONDS.sleep(1);
    7. } catch (InterruptedException e) {
    8. e.printStackTrace();
    9. }
    10. atomicReference.compareAndSet(100,101, atomicReference.getStamp(), atomicReference.getStamp() + 1);
    11. System.out.println(Thread.currentThread().getName() + "\n 第二次的版本号:" + atomicReference.getStamp());
    12. atomicReference.compareAndSet(101,100, atomicReference.getStamp(), atomicReference.getStamp() + 1);
    13. System.out.println(Thread.currentThread().getName() + "\n 第三次的版本号:" + atomicReference.getStamp());
    14. },"threadA").start();
    15. new Thread(() -> {
    16. int stamp = atomicReference.getStamp();
    17. System.out.println(Thread.currentThread().getName() + "\n 第一次的版本号:" + stamp);
    18. try {
    19. TimeUnit.SECONDS.sleep(1);
    20. } catch (InterruptedException e) {
    21. e.printStackTrace();
    22. }
    23. boolean b = atomicReference.compareAndSet(100, 2020, stamp, atomicReference.getStamp() + 1);
    24. System.out.println("compare result: " + b);
    25. System.out.println("当前最新的版本:" + atomicReference.getStamp());
    26. }, "threadB").start();
    27. try {
    28. TimeUnit.SECONDS.sleep(3);
    29. } catch (InterruptedException e) {
    30. e.printStackTrace();
    31. }
    32. }
    33. // 输出结果
    34. threadA
    35. 第一次的版本号:1
    36. threadB
    37. 第一次的版本号:1
    38. threadA
    39. 第二次的版本号:2
    40. threadA
    41. 第三次的版本号:3
    42. compare result: false
    43. 当前最新的版本:3

    可以看出,使用上述的AtomicStampedReference上即可解决ABA问题,因为每次的stamp版本号已经变化,需要重新获取新的版本号之后再进行修改。

    附录

    附录一:volatile内存屏障(Memory Barrier)

    在Intel的硬件中,提供了如下的几种内存屏障

  12. 读屏障

  13. 写屏障
  14. 全能屏障(读写屏障)
  15. Lock 总线加锁

在Java中,有以下的几种内存屏障

屏障类型 指令示例 说明
LoadLoad Load1; LoadLoad; Load2 保证load1的读取操作在load2及后续读取操作之前执行
StoreStore Store1; StoreStore; Store2 在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存
LoadStore Load1; LoadStore; Store2 在stroe2及其后的写操作执行前,保证load1的读操作已读取结束
StoreLoad Store1; StoreLoad; Load2 保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行

ps: 内存屏障,除了可以通过volatile来生成之外,还可以通过 Unsafe来手动的添加内存屏障
如下在Unsafe中的几个API,可以手动的添加读屏障,写屏障,以及全能屏障(读写屏障)
image.png
而且,通过Unsafe也可以实现synchronized的效果(synchronized底层通过monitor对象来实现互斥的)
image.png