乐趣区

关于面试:面试突击36线程安全问题是怎么产生的

线程平安是指某个办法或某段代码,在多线程中可能正确的执行,不会呈现数据不统一或数据净化的状况,咱们把这样的程序称之为线程平安的,反之则为非线程平安的。

举个例子来说,比方银行只有张三一个人来办理业务,这种状况在程序中就叫做单线程执行,而单线程执行是没有问题的,也就是线程平安的。但忽然有一天来了很多人同时办理业务,这种状况就叫做多线程执行。如果所有人都一起争抢着办理业务,很有可能会导致谬误,而这种谬误就叫非线程平安。如果每个人都能有序排队办理业务,且工作人员不会操作失误,咱们就把这种状况称之为线程平安的。

问题演示

接下来咱们演示一下,程序中非线程平安的示例。咱们先创立一个变量 number 等于 0,而后开启线程 1 执行 100 万次 number++ 操作,同时再开启线程 2 执行 100 万次 number– 操作,期待线程 1 和线程 2 都执行完,正确的后果 number 应该还是 0,但不加干涉的多线程执行后果却与预期的正确后果不统一,如下代码所示:

public class ThreadSafeTest {
    // 全局变量
    private static int number = 0;
    // 循环次数(100W)
    private static final int COUNT = 1_000_000;

    public static void main(String[] args) throws InterruptedException {
        // 线程 1:执行 100W 次 number+1 操作
        Thread t1 = new Thread(() -> {for (int i = 0; i < COUNT; i++) {number++;}
        });
        t1.start();

        // 线程 2:执行 100W 次 number-1 操作
        Thread t2 = new Thread(() -> {for (int i = 0; i < COUNT; i++) {number--;}
        });
        t2.start();

        // 期待线程 1 和线程 2,执行完,打印 number 最终的后果
        t1.join();
        t2.join();
        System.out.println("number 最终后果:" + number);
    }
}

以上程序的执行后果如下图所示:

从上述执行后果能够看出,number 变量最终的后果并不是 0,和咱们预期的正确后果是不相符的,这就是多线程中的线程平安问题。

产生起因

导致线程平安问题的因素有以下 5 个:

  1. 多线程抢占式执行。
  2. 多线程同时批改同一个变量。
  3. 非原子性操作。
  4. 内存可见性。
  5. 指令重排序。

接下来咱们别离来看这 5 个因素的具体含意。

1. 多线程抢占式执行

导致线程平安问题的第一大因素就是多线程抢占式执行,设想一下,如果是单线程执行,或者是多线程有序执行,那就不会呈现凌乱的状况了,不呈现凌乱的状况,天然就不会呈现非线程平安的问题了。

2. 多线程同时批改同一个变量

如果是多线程同时批改不同的变量(每个线程只批改本人的变量),也是不会呈现非线程平安的问题了,比方以下代码,线程 1 批改 number1 变量,而线程 2 批改 number2 变量,最终两个线程执行完之后的后果如下:

public class ThreadSafe {
    // 全局变量
    private static int number = 0;
    // 循环次数(100W)
    private static final int COUNT = 1_000_000;
    // 线程 1 操作的变量 number1
    private static int number1 = 0;
    // 线程 2 操作的变量 number2
    private static int number2 = 0;

    public static void main(String[] args) throws InterruptedException {
        // 线程 1:执行 100W 次 number+1 操作
        Thread t1 = new Thread(() -> {for (int i = 0; i < COUNT; i++) {number1++;}
        });
        t1.start();

        // 线程 2:执行 100W 次 number-1 操作
        Thread t2 = new Thread(() -> {for (int i = 0; i < COUNT; i++) {number2--;}
        });
        t2.start();

        // 期待线程 1 和线程 2,执行完,打印 number 最终的后果
        t1.join();
        t2.join();
        number = number1 + number2;
        System.out.println("number=number1+number2 最终后果:" + number);
    }
}

以上程序的执行后果如下图所示:

从上述后果能够看出,多线程只有不是同时批改同一个变量,也不会呈现线程平安问题

3. 非原子性操作

原子性操作是指操作不能再被分隔就叫原子性操作。比方人类吸气或者是呼气这个动作,它是一瞬间一次性实现的,你不可能先吸一半(气),停下来玩会手机,再吸一半(气),这种操作就是原子性操作。而非原子性操作是我当初要去睡觉,但睡觉之前要先上床,再拉被子,再躺下、再入睡等一系列的操作综合在一起组成的,这就是非原子性操作。
非原子性操作是有能够被分隔和打断的,比方要上床之前,发现工夫还在,先刷个剧、刷会手机、再玩会游戏,甚至是再吃点小烧烤等等,所以非原子性操作有很多不确定性,而这些不确定性就会造成线程平安问题问题。
像 i++ 和 i– 这种操作就是非原子的,它在 +1 或 -1 之前,先要查问原变量的值,并不是一次性实现的,所以就会导致线程平安问题。
比方以下操作流程:

操作步骤 线程 1 线程 2
T1 读取到 number=1,筹备执行 number-1 的操作,但还没有执行,工夫片就用完了。
T2 读取到 number=1,并且执行 number+1 操作,将 number 批改成了 2。
T3 复原执行,因为之前曾经读取了 number=1,所以间接执行 -1 操作,将 number 变成了 0。

以上就是一个经典的谬误,number 本来等于 1,线程 1 进行 -1 操作,而线程 2 进行加 1,最终的后果 number 应该还等于 1 才对,但通过下面的执行,number 最终被批改成了 0,这就是非原子性导致的问题。

4. 内存可见性问题

在 Java 编程中内存分为两种类型:工作内存和主内存,而工作内存应用的是 CPU 寄存器实现的,而主内存是指电脑中的内存,咱们晓得 CPU 寄存器的操作速度是远大于内存的操作速度的,它们的性能差别如下图所示:

那这和线程平安有什么关系呢?
这是因为在 Java 语言中,为了进步程序的执行速度,所以在操作变量时,会将变量从主内存中复制一份到工作内存,而主内存是所有线程共用的,工作内存是每个线程公有的,这就会导致一个线程曾经把主内存中的公共变量批改了,而另一个线程不晓得,仍旧应用本人工作内存中的变量,这样就导致了问题的产生,也就导致了线程平安问题。

5. 指令重排序

指令重排序是指 Java 程序为了进步程序的执行速度,所以会对一下操作进行合并和优化的操作。
比如说,张三要去图书馆还书,舍友又让张三帮忙借书,那么程序的执行思维是,张三先去图书馆把本人的书还了,再去一趟图书馆帮舍友把书借回来。而指令重排序之后,把两次执行合并了,张三带着本人的书去图书馆把书先还了,再帮舍友把书借出来,整个流程就执行完了,这是失常状况下的指令重排序的益处。
然而指令重排序也有“副作用”,而“副作用”是产生在多线程执行中的,还是以张三借书和帮舍友还书为例,如果张三是一件事做完再做另一件事是没有问题的(也就是单线程执行是没有问题的),但如果是多线程执行,就是两件事由多集体混合着做,比方张三在图书馆遇到了本人的多个同学,于是就把工作分派给多集体一起执行,有人借了几本书、有人借了还了几本书、有人再借了几本书、有人再借了还了几本书,执行的很凌乱没有明确的指标,到最初喜剧就产生了,这就是在指令重排序带来的线程平安问题。

总结

线程平安是指某个办法或某段代码,在多线程中可能正确的执行,不会呈现数据不统一或数据净化的状况,反之则为线程平安问题。简略来说所谓的 非线程平安是指:在多线程中,程序的执行后果和预期的正确后果不统一的问题。而造成线程平安问题的因素有 5 个:多线程抢占式执行、多线程同时批改同一个变量、非原子性操作、内存可见性和指令重排序。

是非审之于己,毁誉听之于人,得失安之于数。

公众号:Java 面试真题解析

面试合集:https://gitee.com/mydb/interview

退出移动版