购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.2 多线程带来的问题

多线程虽然通过并行处理提高了程序的执行效率,但也带来了数据不一致性、修改非原子性、执行乱序等问题。

3.2.1 CPU缓存导致的可见性问题

如图3-3所示,线程A运行在CPU1上,而线程B运行在CPU2上。假设内存中的变量V的初始状态为0,线程A将V修改为1,因为CPU缓存的关系,整个修改数据只存储在CPU1的缓存里。此时线程2读取变量V仍然是0,它看不到线程1的修改。这个时候线程B读取到的数据就不是最新的数据了。

图3-3 多线程共享变量访问

代码清单3-1是一个简单示例,用来演示多线程带来的数据修改一致性问题。ThreadTest内部定义了count变量,然后创建了两个线程,每个线程对count进行100000次加1操作。整个代码执行了200000次的自增操作,count的期望值是200000。

代码清单3-1 数据一致性问题示例

但是,实际执行的结果是一个100000~200000之间的随机数。因为线程A与线程B在不同的CPU上执行,count++的结果都是存储在CPU的缓存中的,CPU缓存的数据修改对其他CPU中的线程是不可见的。

3.2.2 线程上下文切换带来的原子性问题

在1.3节提到过,为了确保多个线程对CPU资源的合理使用,任务调度器会给每个线程分配一个执行时间,但执行时间到期后,会让当前线程结束执行,让出CPU资源供其他线程使用,这个过程称为被动线程上下文切换。例如,线程任务调度器给线程A分配了80ms的执行时间,过了80ms,操作系统就会重新选择一个线程B来执行。被动线程上下文切换的过程如图3-4所示。

图3-4 线程上下文切换

在执行时,线程还会主动放弃CPU的调度,即采用主动线程上下文切换。在Java线程中,诸如sleep、wait等方法以及I/O操作都会触发主动上下文切换。例如读取文件操作,线程发送完读取命令后可以把自己标记为等待状态,并出让CPU的使用权,待磁盘把文件读进内存,操作系统会把线程唤醒,唤醒后的线程就可以继续读取数据,这种模式能极大地提高CPU的利用率。

无论是主动线程上下文切换还是被动线程上下文切换都会带来操作的原子性问题。应用在执行时会把程序指令解释成多个CPU指令来执行。例如上面例子中的count++操作,至少需要3条CPU指令,如表3-2所示。

表3-2 count++指令分解

线程的上下文切换可能发生在任何一条CPU指令执行完之后。对于count++操作,假设count的值为0。线程A在指令1执行完成后,如果系统在进行线程上下文切换,则唤醒线程B执行。线程B从内存中读取到count值为0,然后对count进行加1操作,最终count值为1。线程B执行完成后,线程A被唤醒,CPU接着从寄存器中读取到count值为0,然后进行加1操作,最终写入到内存的count值为1。虽然两个线程都执行了count+=1的操作,但是得到的结果不是2,而是1。图3-5详细阐述了线程上下文切换破坏复合操作的原子性的过程。

图3-5 上下文切换破坏操作原子性

在工程师的心目中,count++操作是一个不可分割的整体,操作具备原子性。但在CPU的世界中,只有单条CPU指令才是原子的。而线程的上下文切换可以发生在任何两个CPU指令之间,会破坏高级语言层面操作的原子性。

3.2.3 优化带来的乱序问题

前面讲解了多线程内存可见性与操作原子性问题,接下来详细讲解一下线程执行的有序性问题。有序性是指程序按照代码编写的先后顺序执行。例如,在代码清单3-2中,先后对变量a、b进行赋值。

代码清单3-2 代码顺序

程序期望最终执行的顺序和代码编写的顺序是一致的。编译器为了提升程序的执行性能,有时候会改变程序中语句执行的先后顺序,优化后的结果如代码清单3-3所示。

代码清单3-3 编译器优化后的顺序

这种优化在单线程的环境下不会造成问题,但在多线程的情况下会导致意想不到的数据一致性问题。代码清单3-4是一个线程乱序执行的示例,首先定义了x、y、tempX、tempY这4个变量,然后创建了两个线程:线程A与线程B。线程A先将x赋值为1,再将y的值赋给tempY。线程B先将y赋值为1,再将x的值赋给tempX。

代码清单3-4 线程乱序执行的示例

代码在正常顺序执行的情况下,不管线程A和线程B是怎么执行的,tempX、tempY的结果应该只有如图3-6所示的3种情况:1,0;0,1;1,1。

图3-6 tempX与tempY逻辑值组合

但实际执行结果出现了第4种情况,即tempX=0、tempY=0,如图3-7所示,说明线程A与线程B的执行过程中发生了乱序的情况。

图3-7 编译器优化导致数据不一致

从上面例子中可以看到,编译器代码优化改变了线程的执行顺序,导致了程序数据不一致的情况。 lcQX7CZauf5zNrhZ6oxLv/S+76SLqGjs8Kh1Kk47Xzvem5kE+XMwQFVzJUtBOBrb

点击中间区域
呼出菜单
上一章
目录
下一章
×