0%

线程通信

线程通信:从 synchronized 到 Lock 的演进与实践

线程通信是多线程编程中的核心问题,用于协调多个线程间的执行顺序和资源共享。Java 提供了两种主要的线程通信机制:基于synchronizedwait/notify和基于LockCondition接口。本文将深入对比这两种机制,并解析其适用场景。

synchronized 与 Object 类的线程通信方法

核心方法

Java 的每个对象都继承自Object类,提供了三个关键方法用于线程间通信:

  • wait():使当前线程释放对象锁并进入等待状态,直到被其他线程唤醒;
  • notify():随机唤醒一个在该对象上等待的线程;
  • notifyAll():唤醒所有在该对象上等待的线程。

使用规则

这些方法必须在synchronized修饰的代码块或方法中调用,否则会抛出IllegalMonitorStateException

示例:交替打印 1-100

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
38
39
40
public class AlternatePrint {  
private static final Object LOCK = new Object();
private static int count = 1;

public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
synchronized (LOCK) {
// 检查条件,避免虚假唤醒
while (count > 100) break;
System.out.println(Thread.currentThread().getName() + ": " + count++);
LOCK.notify(); // 唤醒其他线程
try {
if (count <= 100) LOCK.wait(); // 释放锁并等待
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}, "线程1");

Thread t2 = new Thread(() -> {
while (true) {
synchronized (LOCK) {
while (count > 100) break;
System.out.println(Thread.currentThread().getName() + ": " + count++);
LOCK.notify();
try {
if (count <= 100) LOCK.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}, "线程2");

t1.start();
t2.start();
}
}

虚假唤醒(Spurious Wakeup)

Java 规范允许wait()在没有被显式唤醒的情况下返回,称为 “虚假唤醒”。因此,正确的使用方式是将wait()放在while循环中,确保条件满足:

1
2
3
4
5
6
synchronized (obj) {  
while (!condition) {
obj.wait();
}
// 执行操作
}

Lock 与 Condition 接口的线程通信

Condition 接口简介

JDK 1.5 引入了java.util.concurrent.locks包,提供了比synchronized更灵活的锁机制。Condition接口与Lock配合使用,替代了Objectwait/notify方法,提供更细粒度的线程控制:

  • await():等价于wait()
  • signal():等价于notify()
  • signalAll():等价于notifyAll()

Condition 的优势

  • 多个等待队列:一个Lock可以创建多个Condition对象,每个对象管理独立的等待队列,实现更精准的线程唤醒;
  • 可中断等待:支持带超时的等待和可中断的等待;
  • 公平锁支持:配合公平锁实现线程按顺序获取锁。

示例:基于 Condition 的交替打印

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
38
39
40
41
42
43
44
45
46
import java.util.concurrent.locks.Condition;  
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class AlternatePrintWithCondition {
private static final Lock LOCK = new ReentrantLock();
private static final Condition CONDITION = LOCK.newCondition();
private static int count = 1;

public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
LOCK.lock();
try {
while (count > 100) break;
System.out.println(Thread.currentThread().getName() + ": " + count++);
CONDITION.signal(); // 唤醒其他线程
if (count <= 100) CONDITION.await(); // 释放锁并等待
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
LOCK.unlock();
}
}
}, "线程1");

Thread t2 = new Thread(() -> {
while (true) {
LOCK.lock();
try {
while (count > 100) break;
System.out.println(Thread.currentThread().getName() + ": " + count++);
CONDITION.signal();
if (count <= 100) CONDITION.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
LOCK.unlock();
}
}
}, "线程2");

t1.start();
t2.start();
}
}

多条件唤醒示例:生产者 - 消费者模型

使用 synchronized 实现

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
38
39
40
41
42
43
44
45
46
47
48
49
public class ProducerConsumer {  
private static final Object LOCK = new Object();
private static final Queue<Integer> QUEUE = new LinkedList<>();
private static final int MAX_SIZE = 5;

public static void main(String[] args) {
// 生产者线程
Thread producer = new Thread(() -> {
for (int i = 0; i < 10; i++) {
synchronized (LOCK) {
// 队列满时等待
while (QUEUE.size() == MAX_SIZE) {
try {
LOCK.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
QUEUE.offer(i);
System.out.println("生产者生产: " + i);
LOCK.notifyAll(); // 唤醒消费者
}
}
});

// 消费者线程
Thread consumer = new Thread(() -> {
while (true) {
synchronized (LOCK) {
// 队列空时等待
while (QUEUE.isEmpty()) {
try {
LOCK.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
Integer item = QUEUE.poll();
System.out.println("消费者消费: " + item);
if (item == 9) break; // 退出条件
LOCK.notifyAll(); // 唤醒生产者
}
}
});

producer.start();
consumer.start();
}
}

使用 Condition 实现(更清晰的等待 / 唤醒逻辑)

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import java.util.concurrent.locks.Condition;  
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ProducerConsumerWithCondition {
private static final Lock LOCK = new ReentrantLock();
private static final Condition NOT_FULL = LOCK.newCondition();
private static final Condition NOT_EMPTY = LOCK.newCondition();
private static final Queue<Integer> QUEUE = new LinkedList<>();
private static final int MAX_SIZE = 5;

public static void main(String[] args) {
// 生产者线程
Thread producer = new Thread(() -> {
for (int i = 0; i < 10; i++) {
LOCK.lock();
try {
while (QUEUE.size() == MAX_SIZE) {
NOT_FULL.await(); // 队列满时等待
}
QUEUE.offer(i);
System.out.println("生产者生产: " + i);
NOT_EMPTY.signal(); // 通知消费者队列非空
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
LOCK.unlock();
}
}
});

// 消费者线程
Thread consumer = new Thread(() -> {
while (true) {
LOCK.lock();
try {
while (QUEUE.isEmpty()) {
NOT_EMPTY.await(); // 队列空时等待
}
Integer item = QUEUE.poll();
System.out.println("消费者消费: " + item);
if (item == 9) break; // 退出条件
NOT_FULL.signal(); // 通知生产者队列非满
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
LOCK.unlock();
}
}
});

producer.start();
consumer.start();
}
}

synchronized 与 Condition 的对比

特性 synchronized + wait/notify Lock + Condition
锁机制 内置锁,自动释放 显式锁,需手动释放(finally 块)
等待队列数量 单一队列,无法区分唤醒类型 可创建多个 Condition,精准唤醒
公平性 非公平锁 支持公平锁
中断响应 不支持中断等待 支持可中断等待
超时机制 仅支持带超时的 wait 支持灵活的超时设置
锁状态查询 无法查询 可查询锁状态
代码复杂度 较低,语法简洁 较高,需手动管理锁

最佳实践建议

1. 选择原则

  • 优先使用 synchronized:简单场景、锁竞争不激烈时,语法简洁,JVM 对其有优化;
  • 使用 Lock+Condition:需要灵活的锁控制(如多条件唤醒、可中断锁、公平锁)。

2. 注意事项

  • 避免死锁:确保线程按相同顺序获取锁,并在 finally 块中释放锁;
  • 防范虚假唤醒:始终使用while循环检查条件;
  • 性能考量:高并发场景下,ReentrantLock可能提供更好的性能,但需根据实际情况测试。

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10