现在计算机和智能手机都是多核处理器,为了更好地发挥设备的性能,提高应用程序的体验性,多线程是必不可少的技术。线程之间不是孤立的,它们共享进程的资源和数据,彼此之间还需要进行通信和协作,最典型的例子就是「生产者-消费者模型」。下面先介绍 wait/notify 机制和 Lock/Condition 机制,然后用两个线程交替打印奇偶数。
1. wait/notify
wait 和 notify 是 Object 类的两个方法,理解起来还是有些复杂的。它和多线程同步有关系,个人觉得放在 Object 类不太合理,可能是历史遗留问题吧。每个对象都有一把锁(monitor),在进入同步方法或代码块之前,当前线程需要先获取对象锁,然后才能执行同步块的代码,完成后释放对象锁。锁可以理解为唯一的凭证,有了它就能入场,而且独占所有的资源,立场就得交出去。
wait 方法的作用是使当前线程释放对象锁,并进入等待状态,不再往下执行。当其他线程调用对象的 notify/notifyAll 时,会唤醒等待的线程,等到其他线程释放锁后,被唤醒的现象将继续往下执行。notify 随机唤醒一个等待的线程,notifAll 唤醒所有等待的线程。注意:wait 和 notify 都需要在拿到对象锁的情况下调用。下面是 wait 的标准使用方法(来自 《Effective Java》一书):
synchronized (obj) {
while (condition does not hold) {
obj.wait(); // release lock and reacquire on wakeup
// perform action appropriate to condition
}
}
每个锁对象都有两个队列:就绪队列和阻塞队列。就绪队列存储了已经就绪(将要竞争锁)的线程,阻塞队列存储了被阻塞的线程。当阻塞线程被唤醒后,才会进入就绪队列,然后等待 CPU 的调度;反之,当一个线程被阻塞后,就会进入阻塞队列,等待被唤醒。
举个例子,线程 A 在执行任务,它等待线程 B 做完某个操作,才能往下执行,这就可以用 wait/notify 实现。
public void start() {
new Thread(new TaskA()).start();
new Thread(new TaskB()).start();
}
private final Object lock = new Object();
private boolean finished;
private class TaskA implements Runnable {
@Override
public void run() {
synchronized (lock) {
System.out.println("线程 A 拿到锁了,开始工作");
while (!finished) {
try {
System.out.println("线程 A 释放了锁,进入等待状态");
lock.wait();
System.out.println("线程 A 收到信号,继续工作");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("线程 A 释放了锁");
}
}
private class TaskB implements Runnable {
@Override
public void run() {
synchronized (lock) {
System.out.println("线程 B 拿到了锁,开始工作");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("-----------------------");
System.out.println("线程 B 发信号了,完成工作");
finished = true;
lock.notify();
}
System.out.println("线程 B 释放了锁");
}
}
/* 打印:
线程 A 拿到锁了,开始工作
线程 A 释放了锁,进入等待状态
线程 B 拿到了锁,开始工作
-----------------------
线程 B 发信号了,完成工作
线程 B 释放了锁
线程 A 收到信号,继续工作
线程 A 释放了锁
*/
2. Lock/Condition
Condition 可以看作 Object 的 wait/notify 的替代方案,同样用来实现线程间的协作。与使用 wait/notify 相比,Condition的 await/signal 更加灵活、安全和高效。Condition 是个接口,基本的方法就是 await() 和 signal()。Condition 依赖于 Lock 接口,生成一个 Condition 的代码是 lock.newCondition() 。 需要注意 Condition 的 await()/signal() 使用都必须在lock.lock() 和 lock.unlock() 之间才可以,Conditon 和 Object 的 wait/notify 有着天然的对应关系:
- Conditon 中的 await() 对应 Object 的 wait();
- Condition 中的 signal() 对应 Object 的 notify();
- Condition 中的 signalAll() 对应 Object 的 notifyAll();
举个例子,使用 Condition 实现和上面的功能。
public void start() {
new Thread(new TaskC()).start();
new Thread(new TaskD()).start();
}
private Lock reentrantLock = new ReentrantLock();
private Condition condition = reentrantLock.newCondition();
private class TaskC implements Runnable {
@Override
public void run() {
reentrantLock.lock();
System.out.println("线程 C 拿到了锁,开始工作");
try {
System.out.println("线程 C 释放了锁,进入等待状态");
condition.await();
System.out.println("线程 C 收到信号,继续工作");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("线程 C 释放了锁");
reentrantLock.unlock();
}
}
}
private class TaskD implements Runnable {
@Override
public void run() {
reentrantLock.lock();
System.out.println("线程 D 拿到了锁,开始工作");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("-----------------------");
try {
System.out.println("线程 D 发信号了,完成工作");
condition.signal();
} finally {
System.out.println("线程 D 释放了锁");
reentrantLock.unlock();
}
}
}
/*打印:
线程 C 拿到了锁,开始工作
线程 C 释放了锁,进入等待状态
线程 D 拿到了锁,开始工作
-----------------------
线程 D 发信号了,完成工作
线程 D 释放了锁
线程 C 收到信号,继续工作
线程 C 释放了锁
*/
相比 Object 的 wait/notify,Condition 有许多优点:
Condition 可以支持多个等待队列,因为一个 Lock 实例可以绑定多个 Condition
Condition 支持等待状态下不响应中断
Condition 支持当前线程进入等待状态,直到将来的某个时间
3. 两个线程交替打印奇偶数
使用 wait/notify:
public void printNumber() {
new Thread(new EvenTask()).start();
new Thread(new OddTask()).start();
}
private int number = 10;
private final Object numberLock = new Object();
private class EvenTask implements Runnable {
@Override
public void run() {
synchronized (numberLock) {
while (number >= 0 && (number & 1) == 0) {
System.out.println("偶数: " + (number--));
numberLock.notify();
try {
numberLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
private class OddTask implements Runnable {
@Override
public void run() {
synchronized (numberLock) {
while (number >= 0 && (number & 1) == 1) {
System.out.println("奇数: " + (number--));
numberLock.notify();
try {
numberLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
使用 Lock/Condition:
public void printNumber() {
new Thread(new EvenTask()).start();
new Thread(new OddTask()).start();
}
private int number = 10;
private Condition evenCondition = reentrantLock.newCondition();
private Condition oddCondition = reentrantLock.newCondition();
private class EvenTask implements Runnable {
@Override
public void run() {
reentrantLock.lock();
try {
while (number >= 0 && (number & 1) == 0) {
System.out.println("偶数: " + (number--));
oddCondition.signal();
evenCondition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
}
private class OddTask implements Runnable {
@Override
public void run() {
reentrantLock.lock();
try {
while (number >= 0 && (number & 1) == 1) {
System.out.println("奇数: " + (number--));
evenCondition.signal();
oddCondition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
}
运行后打印:
偶数: 10
奇数: 9
偶数: 8
奇数: 7
偶数: 6
奇数: 5
偶数: 4
奇数: 3
偶数: 2
奇数: 1
偶数: 0
最后,建议使用 Lock/Condition 代替 Object 的 wait/notify,因为前者是 java.util.concurrent 包下的接口,对于同步更简洁高效,多线程操作优先选用 JUC 包的类。
参考文章:
- Java 并发:线程间通信与协作