本文已参与「新人创作礼」活动,一起开启掘金创作之路。
多线程问题分析
背景
- 线程安全是指某个方法或某段代码,在多线程中能够正确的执行,不会出现数据不一致或数据污染的情况,我们把这样的程序称之为线程安全的,反之则为非线程安全的。
- 举个例子来说,比如银行只有张三一个人来办理业务,这种情况在程序中就叫做单线程执行,而单线程执行是没有问题的,也就是线程安全的。但突然有一天来了很多人同时办理业务,这种情况就叫做多线程执行。如果所有人都一起争抢着办理业务,很有可能会导致错误,而这种错误就叫非线程安全。如果每个人都能有序排队办理业务,且工作人员不会操作失误,我们就把这种情况称之为线程安全的。
多线程时产生线程安全的原因
-
多线程抢占式执行
-
多线程同时修改同一个变量
-
非原子性操作
-
内存可见性
-
指令重排序
问题一:多线程抢占式执行 非常容易理解,如果不加以控制多线程在执行时可能不会按照理想状态的顺序进行执行,而是抢占式的执行,这样就会出现混乱的情况,造成多线程问题
问题二:多线程同时修改一个变量 如果是多线程同时修改不同的变量(每个线程只修改自己的变量),也是不会出现非线程安全的问题,例如线程 1 修改 number1 变量,而线程 2 修改 number2 变量,最终线程执行结束会得到正确的结果。多线程只要是不同时修改同一个变量,也不会出现线程安全问题
问题三:非原子性操作 原子性操作是指操作不能再被分隔就叫原子性操作。像 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,这就是非原子性导致的问题。
问题四:内存可见性 在 Java 编程中内存分为两种类型:工作内存和主内存,而工作内存使用的是 CPU 寄存器实现的,而主内存是指电脑中的内存,我们知道 CPU 寄存器的操作速度是远大于内存的操作速度。而在 Java 语言中,为了提高程序的执行速度,所以在操作变量时,会将变量从主内存中复制一份到工作内存,而主内存是所有线程共用的,工作内存是每个线程私有的,这就会导致一个线程已经把主内存中的公共变量修改了,而另一个线程不知道,依旧使用自己工作内存中的变量,这样就导致了问题的产生,也就导致了线程安全问题。
问题五:指令重排序 指令重排序是指 Java 程序为了提高程序的执行速度,所以会对操作进行合并和优化的操作。这样会造成多线程执行时,指令不按照顺序执行,造成混乱的情况。