代码的守护者:拆解并发安全的三道防线

提示:参考 https://pdai.tech/md/interview/x-interview.html


序言

多线程的出现是要解决什么问题的?

CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:

  • CPU 增加了缓存,以均衡与内存的速度差异;// 导致可见性问题
  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;// 导致原子性问题
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。// 导致有序性问题

一、原子性

我们以3个工人打30000个螺丝为例,来模拟原子性问题

public class ConcurrentBugDemo {

    /**
     * 当前打的螺丝数量
     */
    public static Integer count = 0;

    /**
     * 螺丝+1
     * unsafe
     */
    public static void incr() {
        count++;
    }

    /**
     * 创建了三个工人,每个工人打一万个螺丝,刚好三万个
     */
    public static void main(String[] args) throws InterruptedException {

        Thread worker1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                incr();
            }
        });

        Thread worker2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                incr();
            }
        });

        Thread worker3 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                incr();
            }
        });

        worker1.start();
        worker2.start();
        worker3.start();

        worker1.join();
        worker2.join();
        worker3.join();



        System.out.println(count);
    }
}

正常输出应该是30000个,但是运行程序可以看到结果并不是预期的结果:
在这里插入图片描述
这是为什么了,因为count++这个操作并不是原子操作,它涉及到三个操作:

  1. 从内存读取count的值
  2. 对count执行++操作
  3. 将执行后的结果赋值给count

我们可以通过IDEA的view->show Bytecode 进行验证:

// access flags 0x9
public static incr()V
L0
LINENUMBER 14 L0
//通过getstatic指令获取到count的值,并压到栈里
GETSTATIC count : I
//将常量1压入到栈里
ICONST_1
//从栈顶弹出两个整数,并将整数进行相加,将结果值压到
栈顶
IADD
//将结果值写回到静态变量count里面
PUTSTATIC count : I

对应的解决办法:

  1. incr方法加上synchronized关键字
  2. count改为原子类 AtomicInteger

面试题:下面哪些是操作是原子性的:

x = 10; //语句1: 直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中
y = x; //语句2: 包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
x++; //语句3: x++包括3个操作:读取x的值,进行加1操作,写入新的值。
x = x + 1; //语句4: 同语句3

上面4个语句只有语句1的操作具备原子性。

也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

二、可见性

我们也通过一段代码来模拟:

public class VisibilityDemo {

    public static void main(String[] args) throws InterruptedException {
        Worker shiqi = new Worker();
        shiqi.start();

        Thread.sleep(2000);
        shiqi.flag = true;
        System.out.println("flag更改为true了");
        System.out.println("等待打工人开始搬砖");

    }

    /**
     * 工人线程
     */
    static class Worker extends Thread {
        //公告板
        public boolean flag = false;

        @Override
        public void run() {
            while(true) {
                if(flag) {
                    System.out.println("奋力搬砖");
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }
}

控制台打印结果:
在这里插入图片描述
并没有预期的输出奋力搬砖,这就是可见性的问题。我们通过主线程去更改flag的值,但是对worker者线程来说是不可见的。

我们知道可见性的东西,是因为缓存引起的。 这个缓存不是Java层面,应用服务层面的一个缓存,而是CPU底层的一个缓存。

CPU简单的原理:CPU相当于一个人的大脑,假如有两个数字,一个1,一个2,让你来求一下两个数的和,你是不是先要把1读到你的大脑里,再把2读到你的大脑里,再在你的大脑里把1和2做一个加法计算。CPU其实做加法计算就是先从内存里把1和2加载到CPU里,CPU通过一个加法器给你做一个加法运算,再把这个结果给到你的内存里。

一个简单的比喻:假如你是一名小卖部的老板,有人来买东西,直接从柜台上将商品拿出来的效率肯定是最高的,这就相当于L1缓存。其次就是从柜台里面将商品拿出来的效率,这个效率虽然低一点,但也是非常高的,相当于L2缓存;再其次就是从货架上把商品拿过来,这个就相当于L3缓存,效率低一点。但是也勉强能接受;最后就是从仓库里面把商品拿过来了,这种效率非常低,这仓库其实就相当于主内存了。

问题归根结底就是因为缓存的原因,有缓存就会有缓存一致性的问题

解决方案:

将变量加上volatile关键字

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

三、有序性

有序性是指咱们写的代码是从头到尾执行的,是有顺序的执行。正常思维是会顺序执行的,但是在某些极端情况下啊,是会乱序执行的。好比你觉得送外卖的步骤,必须是先取到餐,才能送餐,但是在并发编程里,就有先送餐再取餐的情况发生。

引入一个新概念指令重排序

CPU它为了提升计算的效率,可能会对编译后的代码做一些重新排序,来提升代码的性能。这里给大家举一个例子就很好理解了,比如说送外卖一般都是同时送很多个人的单的,外卖小哥一般都会制定好计划,第一个单送哪里,第二个单送哪里,第三个单送哪里能够让行程最短,不要走回头路,他并不会说按照接单的顺序去一个个送,不然会绕很多弯路。CPU也是一样的道理,虽然是个机器,但是它也会想办法说规划出更合理的执行方式。

总结

● 原子性问题通过 Synchronized, AtomicXXX、Lock解决
● 可见性问题 Synchronized, volatile 解决
● 有序性通过 Synchronized,volatile 解决

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值