Java并发编程(一)原子性、有序性、可见性

203 阅读3分钟

1.原子性

原子性:一个或多个指令在CPU执行过程中是不允许中断的。


问:i++是否是原子性操作

答:否,i++操作一共有三个指令,不属于原子性操作

image.png

  • getfield:从主内存拉取数据到CPU寄存器

  • iadd:在寄存器内部对数据进行+1

  • putfield:将CPU寄存器中的结果写到主内存中


如何保证i++是原子性的

synchronized

image.png

lock

使用lock锁会有和synchronized类似的概念,在操作i++的三个指令前,先基于AQS成功修改state后才可以操作

CAS(compare and swap)

线程基于CAS修改数据的方式:先获取主内存数据,在修改之前,先比较数据是否一致,如果一致修改主内存数据,如果不一致,放弃这次修改

CAS在Java层面就是Unsafe类中提供的一个native方法,底层通过CPU指令cmpxchg(x86,不同架构CPU或有不同指令支持)保证了操作的原子性,这个方法只提供了CAS成功返回true,失败返回false,如果需要重试策略,需要自己在代码中实现

CAS问题:

  • CAS只能对一个变量的修改实现原子性。

  • CAS存在ABA问题。

    • A线程修改主内存数据从1~2,卡在了获取1之后。
    • B线程修改主内存数据从1~2,完成。
    • C线程修改主内存数据从2~1,完成。
    • A线程执行CAS操作,发现主内存是1,没问题,直接修改
    • 解决方案:加版本号
  • 在CAS执行次数过多,但是依旧无法实现对数据的修改,CPU会一直调度这个线程,造成对CPU的性能损耗

    • synchronized的实现方式:CAS自旋一定次数后,如果还不成,挂起线程
    • LongAdder的实现方式:当CAS失败后,将操作的值,存储起来,后续一起添加

CAS:在多核情况下,有lock指令保证只有一个线程在执行当前CAS

2.有序性

指令在CPU调度执行时,CPU会为了提升执行效率,在不影响结果的前提下,对CPU指令进行重新排序。

如果不希望CPU对指定进行重排序,怎么办?

可以对属性追加volatile修饰,就不会对当前属性的操作进行指令重排序。

什么时候指令重排: 满足happens-before原则,即可重排序


单例模式中为什么要使用DCL(double checked locking)双重判断

正常顺序:

申请内存,初始化,关联

如果CPU对指令重排,可能会造成指令顺序为:

申请内存,关联,初始化

在还没有初始化时,其他线程来获取数据,导致获取到的数据虽然有地址引用,但是内部的数据还没初始化,都是默认值,导致使用时,可能出现与预期不符的结果

3 可见性

可见性:前面说过CPU在处理时,需要将主内存数据拉取到CPU寄存器中再执行指令,指令执行后,需要将寄存器数据写回到主内存中。

基于缓存一致性协议(MESI等)CPU处理完数据后会先放到缓存中,然后同步到内存中。

不是每次操作结束就立刻将CPU缓存数据同步到主内存。造成多个线程看到的数据不一样。

  • volatile每次操作后,立即同步数据到主内存。

  • synchronized,触发同步数据到主内存。

  • final未发生this引用逃逸的情况下也可以保证可见性