并发编程 Bug 的源头

并发编程Bug的三种源头

  • 源头之一:缓存导致的可见性问题
    一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性
    在多核CPU上,线程A使用线程 A 操作的是CPU-1 上的缓存,而线程B操作的是CPU-2上的缓存,这是A对V的操作,就不具备可见性
    在这里插入图片描述
    测试代码:
public class Test {
  private long count = 0;
  private void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      count += 1;
    }
  }
  public static long calc() {
    final Test test = new Test();
    // 创建两个线程,执行 add() 操作
    Thread th1 = new Thread(()->{
      test.add10K();
    });
    Thread th2 = new Thread(()->{
      test.add10K();
    });
    // 启动两个线程
    th1.start();
    th2.start();
    // 等待两个线程执行结束
    th1.join();
    th2.join();
    return count;
  }
}

  • 源头之二: 线程切换带来的原子性问题
    我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性
    例如上例中: count += 1; 在cpu中需要三个指令来完成这个操作
  1. 指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器;
  2. 指令 2:之后,在寄存器中执行 +1 操作;
  3. 指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。

但操作系统的线程切换可以发生在任一条指令执行完,
在这里插入图片描述


  • 源头之三: 编译优化带来的有序性问题

编译器为了优化,有时会调整指令的顺序:

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}

  • 上面代码中的getInstance方法,分为三个指令,分配一块内存 M;在内存 M 上初始化 Singleton 对象;然后 M 的地址赋值给 instance 变量。但是实际上优化后的执行路径却是这样的:分配一块内存 M;将 M 的地址赋值给 instance 变量;最后在内存 M 上初始化 Singleton 对象。如果线程A先执行getInstance,当执行完指令2 后发生线程切换,切换到线程B,这时线B判断instance不为null,马上返回了instance实例,但实际上此时的instance并未初始化。

  • 为什么Long型变量在32位机上操作存在并发隐患?
    因为long型是64,在32位机上要完成64的操作,需要一系列的指令合作完成,因此在多个指令可能发生重排序,所以在多线程中就存在了并发隐患。

cc- -极客时间:java并发编程学习总结

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值