- 单线程程序所进行的计算本质上是串行。多线程编程的目标是将原本串行的计算改为并发乃至并行。
- 竞态(Race Condition)是指计算的正确性依赖于相对世界顺序(Relative Timing)或者线程的交错。竞态表现为计算的结果时而正确时而错误,它并不意味着计算的结果一定是错误的,其往往伴随着读脏数据、丢失更新的问题。竞态是访问(读取、更新)同一组共享变量的多个线程所执行的操作相互交错而导致的干扰(读脏数据)或者冲突(丢失更新)的结果。二维表分析法是分析和解释竞态的有效和常用工具。一个类能够导致竞态,那么它就不是线程安全的。线程安全意味着不存在竞态,但是不存在竞态却未必意味着线程安全。
- 线程安全问题表现为原子性、可见性和有序性这三个方面。这几个方面既相互区别,又相互联系。原子性的保障能够消除竞态。可见性描述了一个线程对共享变量的更新对于另一个线程而言是否可见,或者说什么情况下可见的问题。原子性和可见性一同得以保障了一个线程能够共享变量的相对新值,而不是一个半成品的“值"。有序性描述了一个处理器上运行的一个线程对共享变量所作的更新,在另外一个处理器上运行的其他线程看来,这些线程是以什么样的顺序观察到这些更新的问题。可见性是有序性的基础,而有序性又可能影响可见性。
- 原子操作是“不可分割”的操作。所谓“不可分割”包含两层含义:其一,访问(读、写)某个共享变量的操作从其执行线程以外的任何线程来看,该操作要么已经执行结束,要么尚未发生,即其他线程不会”看到“该操作执行了部分的中间效果;其二,访问一组共享变量的原子操作是不能够被交错的,这通常意味着互斥,即对于访问同一组共享变量的多个原子操作,一个线程执行其中一个操作的时候其他线程无法访问这组共享变量中的任意一个变量。将read-modify-write的操作和check-then-act转换为原子操作能够消除竞态。在Java语言中,对long/double型以外的任何变量的写操作都是原子的。volatile关键字修饰的long/double型写操作也具有原子性。针对任何变量的读操作都是原子操作。
- 可见性问题不是必然出现的,而一旦出现则可能导致灾难性后果。导致可见性问题的因素既有软件因素(JIT编译器)也有硬件因素(处理器和内存等存储设备)。可见性的保障仅仅意味着一个线程能够读取到共享变量的相对新值,而不能保障该线程能够读取到相应变量的最新值。父线程在启动子线程前对共享变量所作的更新对这个子线程可见,子线程执行期间对共享变量所作的更新对该线程的join()执行线程可见(从join返回处开始才可见的)。
- 编译器、处理器、存储子系统(写缓冲器和高速缓存等)和运行时(JIT编译器)都可能导致重排序。重排序是出于性能的需要并满足”貌似串行语义“的前提下进行的,它可能导致线程安全问题。与可见性问题类似,重排序也不是必然出现的。有序性的保障是通过部分地从逻辑上禁止重排序实现的。可见性是有序性的基础,而有序性反过来又可能影响可见性。
| 重排序类型 | 重排序表现 | 重排序来源(主体) |
|---|---|---|
| 指令重排序 | 程序顺序与源代码顺序不一致 | 编译器 |
| 指令重排序 | 执行顺序与程序顺序不一致 | JIT编译器、处理器 |
| 存储子系统重排序 | 源代码顺序、程序顺序和执行顺序这三者保持一致,但是感知顺序与执行顺序不一致 | 高速缓存、写缓冲器 |
- 上下文切换可以看作多线程编程的必然产物,一方面它使得充分利用及其有限的处理器资源成为可能;另一方面它也增加了系统的开销。因此,多线程编程并未必比单线程的计算效率要高。程序运行过程中发生的上下文切换既有自发性上下文切换,也有非自发性上下文切换。Linux内核提供的perf命令可以帮助我们测量程序运行过程中发生的上下文切换的次数和频率。
- 多线程程序可能由于资源稀缺性或者程序自身的错误和缺陷而一直处于非RUNNABLE状态,或是即使是处于RUNNABLE状态,但是其要执行的任务一直无法进展,即产生了活性故障。
- 非公平调度策略是我们多数情况下的首选资源调度策略。其优点是吞吐率较大;缺点是资源申请者申请资源所需的时间偏差可能较大,并可能导致饥饿现象。公平调度策略适合在资源的持有线程占用资源的时间相对较长或者资源的平均申请时间间隔相对长的情况下,或者对资源申请所需的时间偏差有要求的情况下使用。其优点是线程申请资源所需的时间偏差较小,并且不会导致饥饿现象;其缺点是吞吐率较小。
探讨多线程编程中竞态条件、原子性、可见性和有序性等核心概念,解析线程安全问题及解决方案。
555

被折叠的 条评论
为什么被折叠?



