一、线程 ( Thread )

多进程:操作系统将 CPU 的时间片分配给每一个进程,给人并行处理的感觉。

多线程:一个程序同时执行多个任务,每一个任务称为一个线程( thread )。

多进程和多线程的本质区别在于每个进程拥有自己的一套变量,而线程则共享数据。

Java 程序运行时会有一到多个线程运行。每个线程都有专属的栈,用以追踪方法的执行(例:线程栈栈底为线程入口方法)。同一个 Java 程序的所有线程共享一个堆,用于保存对象或数组,为垃圾回收提供支持(无法被程序引用 -> 被垃圾回收器回收,无程序引用 ≠ 引用计数为零)。

1
2
3
4
5
6
7
8
9
10
11
// Normal Thread Mode
public void run() {
try {
while (/*has work to do*/) {
//work
//sleep(...) or wait(...)
}
} catch (InterruptedException e) {
//deal with the exception
}
}

1.1 线程状态与状态迁移

线程有如下六种状态:

  1. New(新创建):调用 start 方法后进入 Runnable 状态
  2. Runnable(可运行):可运行线程可能正在运行也可能没有运行
  3. Blocked(被阻塞):当线程试图获取某个被其他线程持有的Object lock ,进入 Blocked状态,当所有其他线程释放该 lock 并交给该线程时,退出 Blocked 状态
  4. Waiting(等待):线程主动进入等待状态,等待特定条件被唤醒
  5. Timed waiting(计时等待):Waiting + 超时参数(超时期满强制唤醒)
  6. Terminated(被终止):因 Run 方法正常退出而自然死亡 或 因一个未捕获的异常终止 Run 方法而意外死亡

State-change

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
2
3
4
5
6
7
8
9
10
synchronized(lock/*class or object*/){
// codeBlock
lock.notifyAll();// lock.notify();
}
synchronized class ClassName(){
// class
}
synchronized void method(){
// method
}

JVM 确保每个对象只有一个 lock ,只有拿到 lock 的线程才会执行。

尽量避免轮询——空转线程浪费 CPU 资源,该睡就睡(wait / sleep)。

sleep 和 wait 方法的区别:sleep 方法不会释放 lock ,但 wait 方法会释放 lock 。

ReentrantLock / ReentrantReadWriteLock

ReentrantLock(可重入锁):对同一线程重复获取同一锁的次数进行计数(构造器可选参数 boolean fair 表示可选用公平锁 / 非公平锁,默认使用非公平锁)。必须在 finally 块中释放锁!此外,使用 tryLock 可用轮询方式获取锁,如果锁可用,则获取锁并立即返回 true ,否则立即返回 false ,可避免死锁。

1
2
3
4
5
6
7
ReentrantLock lock = new ReentrantLock(); // an unfair lock
lock.lock();// if(lock.tryLock()){ try ... finally ... }
try {
// synchronized codeBlock
} finally {
lock.unlock();
}

Condition 在 Java 1.5 之后出现的一个接口,用于替代 Object 中的 wait - notify / notifyAll 以实现线程调度(对应 Condition 中的 await - signal /signAll)。使用 Condition 可以有选择地对线程进行调度(synchronized 相当于只有单一的 Condition)。

1
2
3
private ReentrantLock lock = new ReentrantLock();
public Condition conditionA = lock.newCondition();
public Condition conditionB = lock.newCondition();

相对于使用 synchronized ,使用 Condition 可以避免 notify / notifyAll 带来的线程唤醒顺序的不确定性。

ReentrantReadWriteLock:读写分离,允许所有对象共享读操作,但写操作与其他任何读写操作互斥。(详见后文设计模式中的 Read-Write Lock模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();

public double getTotalBalance(){
readLock.lock();
try {...}
finally { readLock.unlock(); }
}

public double transfer(...){
writeLock.lock();
try {...}
finally { writeLock.unlock(); }
}

二、多线程设计模式

2.1 Guarded Suspension 模式

Guarded-suspension

当线程执行 guardedMethod 方法时,若守护条件 guarded (守护条件成立与否还和 GuardedObject 的状态相关联)成立则可立即执行,否则就要进行等待。在 Java 中,guardedMethod 通过 while 和 wait 来实现,stateChaningMethod 通过 notify / notifyAll 来实现。

Guarded Suspension 模式在许多于并发相关的设计模式中都有应用,它针对处理对资源的互斥访问。

2.2 Producer - Consumer 模式

Producer-ConsumerModel

在 Producer-Consumer 模式中,将在不同线程中执行的输入与输出进行解耦,设置共享数据类 Channel 对数据进行专门管理(应在共享数据类中使用锁以保证线程安全)。此外,使用该模式还可借助缓冲区平衡 producer 与 consumer 生产 - 消耗速度间的差异。这种设计模式将多线程协调控制的 codeBlock 封装在 Channel 中,让 Producer 和 Consumer 专注于各自的线程。

Worker Thread (又称 Background Thread、Thread Pool)模式,是 Producer - Consumer 模式的变种。

2.3 Thread-Specific Storage 模式

Thread-Specific-StorageModel

TS 为 Thread-Specific 的缩写,上图中的 Client 类与 TSObject 类均有 n 个对象,它们一一对应。

其中 TSObjectCollection 以线程为键来获取和存储 TSObject 对象。

Thread-Specific Storage 模式为每个线程提供了独立的存储空间,这样的存储空间对其他线程不可见,自然也就不存在线程安全问题。其实,该模式的本质是将 互斥处理 提前到调度阶段执行,以维护工作线程彼此间的独立性。

2.4 Thread-Per-Message 模式

Thread-Per-Message

在 handle 操作非常耗时或需要等待输入 / 输出时(比启动新线程的时间代价更不可接受时),Thread-Per-Message 模式可以显著提高 Host 的响应性,降低延迟时间。但该设计模式仅适用于对操作顺序没有要求、不需要返回值时。

一言以蔽之:new Thread(getOneMessage());

2.5 Read-Write Lock 模式

Read-Write-LockModel

Read-Write Lock 模式利用了进行读取操作的线程彼此不会产生冲突的特性,对多个同时进行 read 操作的 Reader 不做互斥处理,使之可以并发执行,从而提高程序性能。这种设计模式适用于读取操作繁重,或读取频率高于写入频率的场景。

在该模式中最为关键的类是 ReadWriteLock,它要解决两类冲突:

  1. read-write conflict
  2. write-write conflict

具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public final class ReadWriteLock {
private int readingReaders = 0;
private int waitingWriters = 0;
private int writingWriters = 0;
private boolean preferWriter = true;

public synchronized void readLock() throws InterruptedException {
while (writingWriters > 0 || (preferWriter && waitingWriters > 0)) {
wait();
}
readingReaders++;
}

public synchronized void readUnlock() {
readingReaders--;
preferWriter = true;
notifyAll();
}

public synchronized void writeLock() throws InterruptedException {
waitingWriters++;
try {
while (readingReaders > 0 || writingWriters > 0) {
wait();
}
} finally {
waitingWriters++;
}
writingWriters++;
}

public synchronized void writeUnlock() {
writingWriters--;
preferWriter = false;
notifyAll();
}
}

引入 ReadWriteLock 既确保了 SharedResource 的安全性,还可以提高程序性能。

三、参考资料

  1. 图解 Java 多线程设计模式
  2. Java 核心技术卷 Ⅰ