CH02-线程与锁
虽然该模型稍显原始且难以驾驭、不够可靠还有点危险,但仍然是并发编程的首选项,也是其他并发模型的基石。
对硬件运行过程形式化
该模型其实是对底层硬件运行过程的形式化。这种形式化既是该模型的最大优点、也是其最大的缺点。
该模型非常简单直接,几乎所有编程语言都提供了对该模型的支持,且不对其使用方式加以限制。
互斥与内存模型
互斥:使用锁来保证某一时间仅有一个线程可以访问数据。它会带来竟态条件和死锁。
乱序执行的来源:
- 编译器和静态优化
- JVM 动态优化
- 底层硬件优化
从直觉上来说,编译器、JVM、硬件都不应该修改原有的代码逻辑,但是近几年的运行效率提升,尤其是共享内存架构的运行效率提升,均基于此类代码的优化。
Java 内存模型为这类优化提供了标准。
内存可见性
Java 内存模型定义了一个线程对内存的修改何时对另一个线程可见。基本原则是,如果读线程和写线程不进行同步,就不能保证可见性。
多把锁
很容易得出一个结论:让多线程代码安全运行的方法只能是让所有的代码都同步。但是这么做有两个缺点:
- 效率低下:如果每个方法都同步,大多数线程会频繁阻塞,也就失去了并发的意义。
- 死锁:哲学家就餐问题。
总结
- 对共享变量的所有访问都需要同步化。
- 读线程、写线程都需要同步化。
- 按照约定的全局顺序来获取多把锁。
- 当持有锁时避免调用外部方法(无法确保线程安全性)。
- 持有锁的时间尽可能的短。
更多同步机制
内置锁的限制
- 一个线程因为等待内置锁而进入阻塞之后,就无法中断该线程了。
- 尝试获得内置锁时无法设置超时。
- 必须通过 synchronized 块来获取内置锁。
可中断的锁
ReentrantLock 提供了显式的加解锁方法,可以在代码的不同位置来实现加解锁逻辑,这是 synchronized 块无法做到的。
同时,ReentrantLock 提供的 lockInterruptibly 方法可以用于终止死锁线程。
超时设置
ReentrantLock 还可以为获取锁的超时设置超时时间。
交替锁
设想我们要在链表插入一个节点,一种做法是用锁保护整个链表,但链表加锁时其他使用者无法访问该链表。而交替锁可以做到仅锁住链表的一部分,允许不涉及被锁部分的其他线程继续自由的访问链表。同样可以由 ReentrantLock 实现。
条件变量
并发编程经常需要等待某个事件发生。比如从队列删除元素前需要等待队列非空、向缓存添加数据前需要等待缓存拥有足够的空间。这时就需要条件变量 Condition。
一个条件变量需要与一把锁关联,线程在开始等待条件之前必须先获取这把锁。获取锁后,线程检查所有等待的条件是否为真。如果条件为真,线程将解锁并继续执行。
如果条件不为真,线程会调优 await 方法,它将原子的解锁并阻塞等待该条件。
当另一个线程调用了 signal 或 signalAll,意味着对应的条件可能已变为真,await 方法将原子的恢复运行并重新加锁。
原子变量
比如 AtomicInteger。与锁相比,原子变量有很多好处。首先,我们不会忘记在正确的时候获取锁;其次,由于没有锁的参与,对原子变量的操作不会引发死锁;最后,原子变量是无锁(lock-free)非阻塞(non-blocking)算法的基础,这种算法可以不使用锁和阻塞来达到同步的目的。
无锁代码比起有锁代码更加复杂,JUC 中的类都尽量使用了无锁代码。
总结
ReentrantLock 和 JUC.atomic 突破了使用内置锁的限制,可以利用它们做到:
- 在线程获取锁时将其中断。
- 设置线程获取锁时的超时时间。
- 按任意顺序获取和释放锁。
- 用条件变量来等待某个条件变为真。
- 使用原子变量来避免使用锁。
利用已有工具
线程池
比如,编写服务端应用时为每个连接请求创建一个线程,这样存在两个隐患:
- 创建线程是有代价的。
- 连接数的增长会使得线程数不断增长,而系统资源(如内存)是有限的。
可以使用线程池来对线程进行复用,JUC 提供了各种类型的线程池。
写时复制
比如 CopyOnWriteArrayList,它使用了保护性复制策略。它并非在遍历链表前进行复制,而是在链表被修改时复制,已经投入使用的迭代器会使用当时的旧副本。
其他概念
- 使用线程构建“生产者——消费者模型”。
- 毒丸(Poison Pill) 是一个特殊对象,告诉消费者“数据已取完,你可以退出了”。
- 使用线程构建“单生产者——多消费者模型”。
- 使用并发集合汇总多个消费者并发生成的结果。
- 使用线程池来优化线程的使用。
- 使用 ConcurrentHashMap 的分段锁优势,避免过多线程对单个资源的过度竞争。
- 为各个消费者提供各自的结构缓存,最后再汇总这些缓存,以避免没有必要的数据竞争。
总结
- 使用线程池,而不是直接创建线程。
- 使用写时复制让监听器先关的代码更简单高效。
- 使用同步队列构建生产者消费者模型。
- ConcurrentHashMap 提供了更好的并发访问。
本章总结
优点
- 适用面广,是许多其他技术的基础,更加接近于本质——近似对硬件工作方式的形式化,真确应用可以得到很高的效率。能够解决从小达到不同粒度的问题。
- 该模型可以被集成到大多数编程语言中。语言设计者可以轻易让一门指令式语言或 OO 语言支持该模型。
缺点
- 该模型没有为并行提供直接的支持。
- 该模型仅支持共享内存模型。如果要支持分布式内存模型则需要借助其他工具。
- 最大的缺点在于“无助”,应用开发者在编程语言层面没有得到足够的帮助。
隐性错误
应用多线程的难点不在编程,而在于难以测试。而测试中的一个大问题是难以复现。
随着项目的迭代和时间的流式,复杂的多线程代码会变得难以维护。
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.