Java并发编程学习笔记—基础篇1-并发编程bug的源头

1.CPU缓存导致的可见性问题

CPU缓存IO>内存IO>硬盘IO

单CPU情况下,所有线程都在同一个CPU喜爱执行,A线程对缓存的操作,对B线程来讲,一定是可见的,如下图


多CPU情况下,CPU缓存和内存的一致性问题就没有那么容易处理了,当多个线程在不同的CPU执行时,操作的是不同的CPU。举个例子:A线程操作CPU-1上的缓存,B线程操作的是CPU-2上的缓存,很明显,A线程对变量V的操作对B线程来讲是不具备可见性的。如下图:

codeDemo
public class VisibilityDemo {

    private static int count;

    public static void addCount() {
        int idx = 0;
        while (idx++ < 10000) {
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            addCount();
        }, "t1");

        Thread t2 = new Thread(() -> {
            addCount();
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);

    }
}

上面的代码,直觉告诉我们count的结果应该是20000,但实际结果缺出乎我们的意料,他最终的结果会是一个10000 到 20000 之间的随机数,为什么呢?
我们假设A线程和B线程同时执行,A线程会将count的值读取到CPU-1的缓存中,B线程会将count的值读取到CPU-2的缓存中,执行完count+=1之后,各自缓存中的值都是1,然后写回到内存,内存中的值变成了1,而不是我们期望的2,之后的执行都会依据各自缓存中的值去计算,最终导致结果不是我们期望的结果。

2.线程切换导致的原子性问题

Java并发编程是基于多线程的,自然会涉及到任务切换,即线程的切换。
高级语言里一条语句往往需要多条 CPU 指令完成,例
如上面代码中的count += 1,至少需要三条 CPU 指令。

  • 指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器;
  • 指令 2:之后,在寄存器中执行 +1 操作;
  • 指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。

如果线程 A
在指令 1 执行完后做线程切换,线程 A 和线程 B 按照下图的序列执行,那么我们会发现
两个线程都执行了 count+=1 的操作,但是得到的结果不是我们期望的 2,而是 1。

CPU保证的原子性是CPU指令级别的,不是高级语言的操作符,所以要保证高级语言编程操作的原子性

3.性能优化导致的指令重排

反例:创建一个实例

我们以为的new操作
1、分配一个内存M
2、在内存M上初始化singleton
3、将M地址赋值给instance变量

实际上有可能是这样的
1、分配一个内存地址M
2、将内存地址M赋值给instance变量
3、在内存地址M上初始化

单例模式的双重锁校验机制,如果允许指令重排,则会发生下图的异常情况,返回一个未初始化的instance

展开阅读全文
©️2020 CSDN 皮肤主题: 深蓝海洋 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值