一、线程 ( Thread )
多进程:操作系统将 CPU 的时间片分配给每一个进程,给人并行处理的感觉。
多线程:一个程序同时执行多个任务,每一个任务称为一个线程( thread )。
多进程和多线程的本质区别在于每个进程拥有自己的一套变量,而线程则共享数据。
Java 程序运行时会有一到多个线程运行。每个线程都有专属的栈,用以追踪方法的执行(例:线程栈栈底为线程入口方法)。同一个 Java 程序的所有线程共享一个堆,用于保存对象或数组,为垃圾回收提供支持(无法被程序引用 -> 被垃圾回收器回收,无程序引用 ≠ 引用计数为零)。
1 | // Normal Thread Mode |
1.1 线程状态与状态迁移
线程有如下六种状态:
- New(新创建):调用 start 方法后进入 Runnable 状态
- Runnable(可运行):可运行线程可能正在运行也可能没有运行
- Blocked(被阻塞):当线程试图获取某个被其他线程持有的Object lock ,进入 Blocked状态,当所有其他线程释放该 lock 并交给该线程时,退出 Blocked 状态
- Waiting(等待):线程主动进入等待状态,等待特定条件被唤醒
- Timed waiting(计时等待):Waiting + 超时参数(超时期满强制唤醒)
- Terminated(被终止):因 Run 方法正常退出而自然死亡 或 因一个未捕获的异常终止 Run 方法而意外死亡
1.2 多线程协同
线程安全
线程安全(thread-safe)是关于对象是否能被多个线程安全访问的特性。线程安全的对象,无论被以怎样的交叠次序进行访问,都将得到相同的结果。
- 线程安全的类对应的任意对象都是线程安全的
- 任何时候访问线程安全对象都无需做同步控制措施
引起线程安全问题的原因:某线程中的 数据关联 / 逻辑关联 codeBlock 被其他线程干扰。
解决方案:对具有关联性的 CodeBlock 进行封装(设计专属数据结构 / 使用同步控制块),保证 计算原子性。
线程优先级:一个线程继承它的父类线程的优先级,可用 setPriority 方法将线程优先级设置在 MIN_PRIORITY = 1 与 MAX_PRIOITY = 10 之间(线程的默认优先级为 NORM_PRIORITY )。然而,在 Oracle 为 Linux 提供的 Java 虚拟机中,线程的优先级被忽略——所有线程具有相同的优先级。设计时不要让线程安全问题 / 程序逻辑依赖于线程优先级!
计算原子性:与 Atomic 相关原子数据类相似。
锁
synchronized
关键字 synchronized(同步锁):对所有同步控制区域加上同步锁(同一线程对同一对象锁是可重入的,且同一线程可以多次获取同一把锁——支持多次重入)。通常在 synchronized 控制区域中配套使用 wait - notify / notifyAll 方法避免轮询。
1 | synchronized(lock/*class or object*/){ |
JVM 确保每个对象只有一个 lock ,只有拿到 lock 的线程才会执行。
尽量避免轮询——空转线程浪费 CPU 资源,该睡就睡(wait / sleep)。
sleep 和 wait 方法的区别:sleep 方法不会释放 lock ,但 wait 方法会释放 lock 。
ReentrantLock / ReentrantReadWriteLock
类 ReentrantLock(可重入锁):对同一线程重复获取同一锁的次数进行计数(构造器可选参数 boolean fair 表示可选用公平锁 / 非公平锁,默认使用非公平锁)。必须在 finally 块中释放锁!此外,使用 tryLock 可用轮询方式获取锁,如果锁可用,则获取锁并立即返回 true ,否则立即返回 false ,可避免死锁。
1 | ReentrantLock lock = new ReentrantLock(); // an unfair lock |
Condition 在 Java 1.5 之后出现的一个接口,用于替代 Object 中的 wait - notify / notifyAll 以实现线程调度(对应 Condition 中的 await - signal /signAll)。使用 Condition 可以有选择地对线程进行调度(synchronized 相当于只有单一的 Condition)。
1 | private ReentrantLock lock = new ReentrantLock(); |
相对于使用 synchronized ,使用 Condition 可以避免 notify / notifyAll 带来的线程唤醒顺序的不确定性。
类 ReentrantReadWriteLock:读写分离,允许所有对象共享读操作,但写操作与其他任何读写操作互斥。(详见后文设计模式中的 Read-Write Lock模式)
1 | private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); |
二、多线程设计模式
2.1 Guarded Suspension 模式
当线程执行 guardedMethod 方法时,若守护条件 guarded (守护条件成立与否还和 GuardedObject 的状态相关联)成立则可立即执行,否则就要进行等待。在 Java 中,guardedMethod 通过 while 和 wait 来实现,stateChaningMethod 通过 notify / notifyAll 来实现。
Guarded Suspension 模式在许多于并发相关的设计模式中都有应用,它针对处理对资源的互斥访问。
2.2 Producer - Consumer 模式
在 Producer-Consumer 模式中,将在不同线程中执行的输入与输出进行解耦,设置共享数据类 Channel 对数据进行专门管理(应在共享数据类中使用锁以保证线程安全)。此外,使用该模式还可借助缓冲区平衡 producer 与 consumer 生产 - 消耗速度间的差异。这种设计模式将多线程协调控制的 codeBlock 封装在 Channel 中,让 Producer 和 Consumer 专注于各自的线程。
Worker Thread (又称 Background Thread、Thread Pool)模式,是 Producer - Consumer 模式的变种。
2.3 Thread-Specific Storage 模式
TS 为 Thread-Specific 的缩写,上图中的 Client 类与 TSObject 类均有 n 个对象,它们一一对应。
其中 TSObjectCollection 以线程为键来获取和存储 TSObject 对象。
Thread-Specific Storage 模式为每个线程提供了独立的存储空间,这样的存储空间对其他线程不可见,自然也就不存在线程安全问题。其实,该模式的本质是将 互斥处理 提前到调度阶段执行,以维护工作线程彼此间的独立性。
2.4 Thread-Per-Message 模式
在 handle 操作非常耗时或需要等待输入 / 输出时(比启动新线程的时间代价更不可接受时),Thread-Per-Message 模式可以显著提高 Host 的响应性,降低延迟时间。但该设计模式仅适用于对操作顺序没有要求、不需要返回值时。
一言以蔽之:new Thread(getOneMessage());
2.5 Read-Write Lock 模式
Read-Write Lock 模式利用了进行读取操作的线程彼此不会产生冲突的特性,对多个同时进行 read 操作的 Reader 不做互斥处理,使之可以并发执行,从而提高程序性能。这种设计模式适用于读取操作繁重,或读取频率高于写入频率的场景。
在该模式中最为关键的类是 ReadWriteLock,它要解决两类冲突:
- read-write conflict
- write-write conflict
具体实现如下:
1 | public final class ReadWriteLock { |
引入 ReadWriteLock 既确保了 SharedResource 的安全性,还可以提高程序性能。
三、参考资料
- 图解 Java 多线程设计模式
- Java 核心技术卷 Ⅰ