基础 | 并发编程 - [导论 & volatile]

并发编程

并发编程的特性

  • 原子性
    一个或一组操作在执行过程中不会被其他操作插入或中断
  • 可见性
    一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改
  • 有序性
    程序执行的顺序按照代码的先后顺序执行

先行发生原则(Happens-Before)

  • 先行发生原则,是对 可见性有序性 的约束
  • 若 java 内存中,操作 A 先行发生于 操作 B ,则 操作 B 可以观测到 操作 A 产生的影响
  • 操作 A 与 操作 B具有先行发生关系,并不意味两操作一定按先行发生顺序执行
    若两操作指令重拍后,执行结果与不重排一致,则这种指令重排合法
  • 用于判断数据是否存在线程竞争或是否线程安全,对先行发生的场景的指令重排是有限制的

java 语法天然支持下面 8 个先行发生场景
下面8个场景中,操作 A 先行发生于操作 B 时,操作 B 可以观测到 操作 A 的结果

  • 程序次序规则(Program Order Rule)
    同一个线程内,流程控制上在前的操作先行发生于流程控制中在后的操作时
  • 管程锁定规则(Monitor Lock Rule)
    同一个锁的 unlock 操作 先行发生于 此锁的 lock 操作时
  • volatile 变量规则(Volatile Variable Rule)
    同一个 volatile 变量的写操作 先行发生于对于 这个变量的读操作之前时
  • 线程启动规则(Thread Start Rule)
    同一个线程 start()方法 先行发生 此线程其他方法时
  • 线程终止规则(Thread Termination Rule)
    同一个线程的 所有其他操作 先行发生 此线程的终止检测(join() 和 isAlive())时
  • 线程中断规则(Thread Interruption Rule)
    同一个线程的 interrupt() 方法的调用先行发生 此线程的终止检测(interrupt())时
  • 对象终结规则(Finalizer Rule)
    同一个对象的 构造方法执行结束 先行发生于 此对象 finalize() 方法开始时
  • 传递性(Transitivity)
    操作A 先行发生于 操作B
    操作B 先行发生与 操作C
    操作A 先行发生于 操作C

场景举例

private int value = 0;
public int geValue(){ // 线程 2 调用
	return value;
}
public int setValue(){ // 线程 1 调用
	return ++value;
}

分析

  • 首先,不涉及线程的启动、中断、终止,以及对象的终结
  • 其次,因为是多线程调用,所以不涉及 程序次序
    程序次序的前提是在同一个线程中
  • 再次,两个方法没有使用锁,不满足 管程锁定
  • 又次,涉及变量不被 volatile 修饰,所以不满足 == volatile 变量规则==
  • 最后,一共只有两个操作(取值和赋值),因此无所谓 传递性
  • 综上,不能推定上面两操作具有先行发生关系
    只能认为调用这两个方法的线程根据调用顺序具有一定的优先

volatile 关键字

volatile 关键字是 java 虚拟机提供的轻量级(线程间)同步机制,这意味着 volatile

  • 保证可见性
    写操作时,会立刻将结果同步到主内存
    读操作时,会强迫本地内存与主内存同步
    相当于直接往主内存写,从主内存读
  • 不保证原子性
    volatile 可能导致写操作丢失
    因为其他线程获取了同一个值并在此线程之前提交成功
  • 禁用指令重排

指令重排
为提高性能,编译器和处理器会对指令进行重排
包括编译器优化重排指令并行重排内存重排

  • 指令在执行的过程中,若交换顺序不影响执行结果,则允许指令重排
    即,指令重排不能破坏数据原本的依赖关系
  • 编译器优化重排:编译器可能导致指令重排
    JMM 可以根据重排规则,禁止特定编译器的重排
  • 指令并行重排:处理器可能导致指令重排
    编译器可以通过在指令序列中插入 内存屏障 禁用
  • 内存重排:内存系统可能导致指令重排
    由处理器使用缓存或读写缓冲区导致

单线程时,指令重排前后保持一致
多线程时,可能出现问题

指令重排会出问题的场景

  • 写后读
    若重排,会导致读到旧值
    i = 1;
    j = i;
    
  • 写后写
    若重排,会导致实际最后执行的语句生效
    i = 1;
    i = 10;
    
  • 读后读
    若重排,会导致读取到新值
    i = j;
    j = 10;
    

多线程下指令重排问题示例
说明
假设只有一个 AAA 对象
有两个线程,线程 1 调用 m1,线程 2 调用 m2

此测试程序编译并执行多次,
每次都是 m1 的第一句执行后,m2 开始执行

m1 先执行 a=1 时,输出 6
m1 先执行 f=true 时,输出 5

class  AAA{
    int a = 0;
    boolean f = false;

    public void m1(){
        a = 1;
        f = true;
    }

    public void m2(){
        if (f){
            a = a+5;
            System.out.println(a);
        }
    }
}

内存屏障(Memory Barrier)
内存屏障是一个 CPU 指令,作用如下

  • 内存屏障前后的指令不能与之调换顺序,因此前后指令也不会调换顺序
  • 内存屏障可以强制刷新 CPU 的缓存数据
    详见 内存屏障的划分

当一个变量被声明为 volatile 时,字节码会添加 ACC_VOLATITL 标记
JVM 遇到此标记时,会按下面 JVM 指令重排的规定 的顺序添加内存屏障

内存屏障的划分
写屏障(store fence): 使内存屏障之前的写操作回写主内存
读屏障(load fence): 使内存屏障之后的所有读操作都能获得屏障前写操作的值
全屏障(full fence): 上面两者合并

实际使用时,会更细化分为 4 个屏障

  • LoadLoad,保证两个读操作有序
    对应汇编语句 movq 0(%%rsp) ,%0
    将 rsp (寄存器)中的值存入 thread.sp (线程的共享内存,sp 是共享内存 sharded memory 的一部分)中,完成rsp的存储
  • StoreStore,保证两个写操作有序
  • LoadStore,保证先读后写有序
  • StoreLoad,保证先写后读有序

JVM 指令重排的规定

是否允许指令重排后 普通读后 普通写后 volatile 读后 volatile 写
先 普通读×
先 普通写×
先 volatile 读×(但应该可以? )×××
先 volatile 写××

概括的讲,下面场景禁用指令重排

  • 先 volatile 读时
    防止后面的操作重排到 volatile 读之前
  • 后 volatile 写时
    防止前面的操作重排到 volatile 写之后
    比如先读后写重排为先写后读,会导致读到的值不一致
  • 先 volatile 写后读时
    防止重排为先读后写,导致读的值不一致

JMM 内存屏障策略
JMM 在 volatile 读写前后添加屏障如下面两组

volatile 写组

  • 写写屏障(StoreStore)
  • volatile 写
  • 写读屏障(StoreLoad)

volatile 读组

  • volatile 读
  • 读读屏障(LoadLoad)
  • 读写屏障(LoadStore)

上面两组直接与普通读写自由前后组合,如先 volatile 读,后 volatile 写可以等效为

  • volatile 读
  • 读读屏障(LoadLoad)
  • 读写屏障(LoadStore)
  • 写写屏障(StoreStore)
  • volatile 写
  • 写读屏障(StoreLoad)
是否允许指令重排后 普通读后 普通写后 volatile 读后 volatile 写
先 普通读---写写屏障(StoreStore)
先 普通写---写写屏障(StoreStore)
先 volatile 读读读屏障(LoadLoad)读写屏障(LoadStore)读读屏障(LoadLoad)写写屏障(StoreStore
先 volatile 写--写读屏障(StoreLoad)写读屏障(StoreLoad)

volatile 的适用场景
总结

  • volatile 不保证 原子性,因此不适用于基于当前值的运算
    比如在当前值基础上 + 1 等操作
  • volatile 可以保证 可见性有序性,因此适用于 无所谓原值而直接复制 的操作
    比如用于标识某些状态的 boolean 和 int
    单例
    双重检验懒汉式(DCL)理论上有很小概率出现问题
    语句 instance = new Singleton() 不是原子的
    由 3 步组成
  • 在堆中分配内存空间
  • 初始化对象
  • 变量 instance 指向内存空间

若 2 、3 步发生指令重排,且出现并发问题
则因为引用了内存空间,内外两层校验检查时 != null
但此时返回的是一个未初始化完成的对象
可以使用 volatile 禁用指令重排
读写锁缓存
读通过 volatile 保证可见性和顺序性来代替锁,因为可以认为简单的读天然带有原子性
写操作依然通过锁保证
JUC 包中

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值