文章目录
- 5. 线程间通信
- 5.1 synchronized
- 5.2 Lock +Condition
- 5.3 BlockingQueue
5. 线程间通信
当线程在运行时,在系统的调度中具有一定的透明性,虽然程序无法精准的控制线程,但是我们可以通过一定的手段,达到协调线程运行。
上一篇我们介绍了线程同步,确保不同的线程操作的数据一致性。同样的 我们也可以用锁的方式,协调线程。下面我们用一道经典的力扣多线程题目多线程交替打印 foobar来简单说下线程间通信。
5.1 synchronized
之前我们讲到 synchronized 代码块和 synchronized 方法,都是获取监视器来控制线程的状态,那么同样的在不同的线程中,如果我们用的监视器是同一个,就可以做到根据条件切换不同线程运行。话不多说先上代码
public class PrintAB{
private static final Object LOCK = new Object();
private static boolean printA = true;
public static void main(String[] args){
new Thread(new PrintA()).start();
new Thread(new PrintB()).start();
}
static class PrintA implements Runnable {
@Override
public void run() {
synchronized (LOCK) {
for (int i = 0; i < 10; i++) {
try {
if (!printA) { //注释 1
LOCK.wait(); // 注释 2
}
System.out.println("A"); //注释 3
printA = false; // 注释 4
LOCK.notifyAll(); // 注释 5
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
static class PrintB implements Runnable {
@Override
public void run() {
synchronized (LOCK) {
for (int i = 0; i < 10; i++) {
try {
if (printA) { //注释 6
LOCK.wait(); //注释 7
}
System.out.println("B"); //注释 8
printA = true; //注释 9
LOCK.notifyAll(); //注释 10
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
这里我们首先定义两个成员变量,一个是锁 LOCK,一个是 用来标记是否打印 A 的标记printA。接着 main 方法中启动了两个线程,一个线程是用来打印 A,一个线程是用来打印 B,到这里都很好理解。
下面我们来看看如何通过监视器,协调线程的:
- 当程序运行到 注释 1 处,回去判断是否要阻塞当前线程,由于第一次进来的时候printA = true,所以这里不阻塞,并且运行了输出
- 解咒走到了注释 4,此处将 printA 赋值 flase,因为要交替输出 AB,A 已经输出,所以到 B 的轮次
- 然后走到注释 5,这里唤醒了 其他的锁,当再次循环到注释 1 的时候,由于 printA 为 false,所以此时 Lock 是 wait 状态,线程阻塞,从而走到线程 B,注释 6 处获取的 printA 为 false,所以线程 B 不阻塞,输出了 B,然后有将 printA 设置为 true,并且通知其他线程,如此循环 做到了两个输出 交替执行。
5.2 Lock +Condition
上面的方法是通过 synchronized 来保证线程同步,通过控制监视器的 wait,notify 方法协调线程。如果不通过 synchronized 仅使用 Lock 对象确保同步,也是可以的,这就是我们要学习的第二个方式 Lock+Condition
在这种方式下,Lock 代替了synchronized, Condition 代替了监视器。先看代码:
public class Main {
private static boolean printA = true;
private static final Lock LOCK = new ReentrantLock();
private static final Condition CONDITION = LOCK.newCondition();
public static void main(String[] args) throws InterruptedException {
new Thread(new PrintA()).start();
new Thread(new PrintB()).start();
}
static class PrintA implements Runnable {
@Override
public void run() {
LOCK.lock();
try {
for (int i = 0; i < 10; i++) {
if (!printA) {
CONDITION.await();
}
System.out.println("A");
printA = false;
CONDITION.signalAll();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
LOCK.unlock();
}
}
}
static class PrintB implements Runnable {
@Override
public void run() {
LOCK.lock();
try {
for (int i = 0; i < 10; i++) {
if (printA) {
CONDITION.await();
}
System.out.println("B");
printA = true;
CONDITION.signalAll();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
LOCK.unlock();
}
}
}
}
从代码上看,和方法一的写法起始很像,只不过将 synchronized 代码块通过 lock()方法加锁,而监视器的 wait 方法通过 condition 的 await 方法代替,监视器的 notifyAll方法被 condition 的 signalAll 方法代替。这里需要注意的是 lock 一定要在 finally 中释放锁。同样介绍一下 condition 方法:
- await(): 和 Object 类的 wait 类似,也是用于当线程等待,直到其他线程通过 signal 或者 signalAll 唤醒
- signal():唤醒在此Lock 对象上的等待的单个线程,如果有多个线程在等待,则回选择唤醒其中一个。只有当前线程放弃对该 Lock 对象的锁定,才能执行被唤醒的线程。
- signalAll():唤醒所有等待的线程,只有当前线程放弃对该 Lock 对象的锁定后,才能执行被唤醒的线程
5.3 BlockingQueue
从字面意思可以看出 这就是一个阻塞队列,从源码看 BlockingQueue 也是继承 Queue,所以他也有队列的特性。BlockingQueue 作为阻塞队列,当队列已经满的情况下,下一个通过 put 方法加入队列的会被阻塞,当队列有空闲才会继续添加,同理在使用 take 方法 取队列里面的元素的时候,如果队列为空,那么此时取操作也会阻塞,等到队列内有元素的时候,才会读取。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Main {
static BlockingQueue<String> queueA = new LinkedBlockingQueue<>();
static BlockingQueue<String> queueB = new LinkedBlockingQueue<>();
public static void main(String[] args) {
queueA.add("A");
new Thread(new PrintA()).start();
new Thread(new PrintB()).start();
}
static class PrintA implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
System.out.println(queueA.take());
queueB.add("B");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
static class PrintB implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
System.out.println(queueB.take());
queueA.add("A");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
这里我们引入了两个队列,一个队列控制输出 A,一个队列控制输出 B,因为在 BlockingQueue 的顺序不一定可靠,所以使用两个队列确保每一个队列只做一件事情,但是在不同线程控制 队列 A,B 的数据,这样就能做到线程同步,交替输出。值得注意的是,在main 方法的一开始,就给 A 队列加入了一个元素,因为如果不加入元素,在 printA 中,走到 queueA.take()的时候,队列为空,此时队列 B 也为空,互相阻塞造成了死锁。
上面通过力扣题目,讲解三种方式的线程通信,起始还有很多并发类可以控制,总结我们可以发现,在多线程通信中,如果想要控制线程,逃不过三板斧 加锁 -> 操作 -> 释放锁
。