安全,已经成为非常重要的社会话题。所谓“安全第一”,“安全无小事”(手动滑稽),同样,多线程中,线程安全也是非常重要的话题。那么是什么原因造成了线程不安全,又如何解决线程不安全呢?

造成线程不安全的原因

  1. 线程的调度

    各线程之间是抢占式执行的,线程的执行顺序是随机的,因此可能会产生各种问题。现在最流行什么?做核酸!如果做核酸是抢占式的,做核酸没有一个顺序,做核酸顺序完全靠运气,这能安全吗!

  2. 多线程同时修改同一变量

    如果是多线程同时读取同一变量,不涉及修改的操作,是线程安全的。但如果多线程同时修改同一变量,这能不乱吗?想当年家里买了一台电脑,我姐姐要用电脑玩QQ炫舞,我要用电脑玩穿越火线,你争我抢,打的是不可开交。

  3. 操作指令不是原子的

    例如一条加法指令,其实要执行三条指令,load、add、save,先将内存中的变量加载到寄存器,在寄存器中完成加法操作,再将结果写会内存中。假设线程1完成了load、add操作,线程2完成了load、add、save操作,当线程1再去执行save操作,便将线程2的操作覆盖了,线程2说:线程1真是一个猪队友。

  4. 内存可见性

    线程1循环进行读操作,线程2看心情进行修改操作。我们知道,读操作是将内存变量加载到寄存器,然后读取寄存器。而线程1循环加载内存中的值到寄存器,线程2又迟迟不修改,线程1说:你当我傻吗?于是线程1干脆去读取寄存器了,这就是编译器的优化。拿做核酸举例,最初是刮嗓子,但日复一日,现在有些已经是刮舌头了,这难道是疫情的优化?大部分情况下是安全的,但也可能会翻车。线程1循环读操作,线程2突然心情不好,很快啊,修改了内存中的值。而线程1却绕过了内存,从寄存器读值,这能安全吗!

解决方案

针对原因1,线程的调度就是抢占式执行的,我们无能为力。针对原因2,我们可以通过调整代码结构,使不同线程操作不同变量,但我这菜鸡技术,既然能写出一个bug,就能写出无数个bug,一顿调整猛如虎,结果bug乘以5.

所以我们解决方案寄托在原因3、4上。

Java中提供了synchronized关键字来对操作上锁。拿上面的加法操作举例,为加法这一方法加上synchronized后,就为该操作上锁了。想象要在卫生间完成加法操作,(不敢想象),小县城来的孩子线程1来执行加法操作,发现卫生间没人,他进来后把门锁上,进行加法操作。又一个小县城的孩子线程2来到卫生间前,也想要做加法操作,发现门锁了,只好等到线程1出来后才能进去,他是等也得等,不等也得等,这样就保证了操作的原子性。

Java还提供了volatile关键字来解决内存可见性问题,synchronized也可以解决该问题,只不过开销更大。为代码加上volatile后,就能禁止编译器进行优化,线程1无论做多少次循环读操作,都要耐心地从内存中读值,不能直接去读取寄存器中的值。