线程间的通信问题,用一个最经典的例子来讲就是生产者和消费者问题:
假设我们现在有两条线程P、C,它们操作着同一个变量num,每次当num=0时,P线程就对num进行+1操作,每次num=1时C就对num进行-1操作。这就好比一个商家生产东西,一个客户买东西,商家生产一个,客户就买一个。然而线程之间是相互隔离的,我们要让两条线程能够做到对同一个资源进行操作,就必须要进行线程通信。
public class ProductorAndCustomer {
public static void main(String[] args) {
//资源
Data data = new Data();
//生产者线程
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data.increment();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"Productor").start();
//消费者线程
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"Customer").start();
}
}
//资源
class Data{
private int num = 0;
//生产
public synchronized void increment() throws InterruptedException {
if (num != 0){
//说明生产的产品未被消费者消费,因此等待被消费
this.wait();
}
//说明生产的产品被消费者消费了,因此再进行一次生产
num++;
System.out.println(Thread.currentThread().getName()+"->生产了产品:num = "+num);
//生产完毕后需要通知消费者进行消费
this.notify();
}
//消费
public synchronized void decrement() throws InterruptedException {
if (num == 0){
//说明生产者未生产产品,因此等待生产者生产产品
this.wait();
}
//说明生产已经生产了一个产品,因此再进行一次消费
num--;
System.out.println(Thread.currentThread().getName()+"->消费了产品:num = "+num);
//消费完毕后需要通知生产者进行生产
this.notify();
}
}
运行结果
可以看到,每当生产者进行一次生产,消费者就会进行一次对应的消费,如果消费者先进行消费,这时生产的产品为0,消费者就会进入等待状态(执行消费者的wait()方法 ),等待生产者进行生产,如果生产者生产了一个产品,又被执行了一次生产,那么这时产品数不为0,因此进入等待状态( 执行生产者的wait()方法 )等待消费者进行消费。
但是这样的生产者和消费者模式会存在一个问题:
一般来讲,我们的生产者和消费者可能会有多个。假设现在新来了一个消费者和生产者,我们再观察一下运行结果
//生产者线程1
new Thread(()->{
try {
for (int i = 0; i < 30; i++) {
data.increment();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"Productor1").start();
//生产者线程2
new Thread(()->{
try {
for (int i = 0; i < 30; i++) {
data.increment();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"Productor2").start();
//消费者线程1
new Thread(()->{
try {
for (int i = 0; i < 30; i++) {
data.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"Customer1").start();
//消费者线程2
new Thread(()->{
try {
for (int i = 0; i < 30; i++) {
data.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"Customer2").start();
运行结果
可以看到,出现了问题:生产者多生产了几个产品。这就是多线程中的虚假唤醒问题。
我们查看一下官方api对这一块的解释:
所谓的虚假唤醒,就是用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码,而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。
我们将资源类中的if
替换成while再来进行尝试:
//生产
public synchronized void increment() throws InterruptedException {
while (num != 0){
//说明生产的产品未被消费者消费,因此等待被消费
this.wait();
}
//说明生产的产品被消费者消费了,因此再进行一次生产
num++;
System.out.println(Thread.currentThread().getName()+"->生产了产品:num = "+num);
//生产完毕后需要通知消费者进行消费
this.notifyAll();
}
//消费
public synchronized void decrement() throws InterruptedException {
while (num == 0){
//说明生产者未生产产品,因此等待生产者生产产品
this.wait();
}
//说明生产已经生产了一个产品,因此再进行一次消费
num--;
System.out.println(Thread.currentThread().getName()+"->消费了产品:num = "+num);
//消费完毕后需要通知生产者进行生产
this.notifyAll();
}
Lock(JUC)版的生产者消费者问题
我们在使用synchronized来描绘生产者和消费者问题时,使用的是
wait()
方法和notifyAll()
方法来进行线程通信,在JUC中,我们有没有同样的方式呢?来看一下api吧:
可以看到,官方文档告诉我们使用锁对象来进行
.newCondition()
来创建一个条件对象(同步监视器),对应Object类中wait
和notifyAll
的作用,用来进行线程通信。
在JUC中,我们使用Condition实例中的.await()
方法来进行线程等待,使用.signal()
方法来进行线程唤醒。对应synchronized
中的wait()
和notifyAll()
方法。如下图:
代码实现:
//资源
class Data{
private int num = 0; //资源
Lock lock = new ReentrantLock(); //创建锁
Condition condition = lock.newCondition(); //通过锁对象来创建同步监视器
//生产
public void increment() throws InterruptedException {
lock.lock(); //加锁
try {
//业务代码
while (num != 0){
//说明生产的产品未被消费者消费,因此等待被消费
condition.await();
}
//说明生产的产品被消费者消费了,因此再进行一次生产
num++;
System.out.println(Thread.currentThread().getName()+"->生产了产品:num = "+num);
//生产完毕后需要通知消费者进行消费
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); //释放锁
}
}
//消费
public void decrement() throws InterruptedException {
lock.lock(); //加锁
try {
//业务代码
while (num == 0){
//说明生产者未生产产品,因此等待生产者生产产品
condition.await();
}
//说明生产已经生产了一个产品,因此再进行一次消费
num--;
System.out.println(Thread.currentThread().getName()+"->消费了产品:num = "+num);
//消费完毕后需要通知生产者进行生产
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); //释放锁
}
}
}
运行结果
显然,我们同样也能使用JUC来实现生产者和消费者问题。但是这种新的方式只是和原来
synchronized
的方式功能一样,没有什么新的功能吗?要知道,任何一个新的技术,绝对不仅仅只是覆盖了原来的技术。
- 我们来增加一个需求,现在我们要求:
生产者1生产完毕后通知消费者1来消费,
消费者1消费完毕后通知生产者2生产,
生产者2生产完毕后通知消费者2来消费,
消费者2消费完毕后通知生产者1来生产从而达到一种线程间的精准通信的方式。(这就是JUC方式特有的新功能)
public class ProductorAndCustomer {
public static void main(String[] args) {
//资源
Data data = new Data();
new Thread(()->{
try {
for (int i = 0; i < 5; i++) {
data.printA();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
for (int i = 0; i < 5; i++) {
data.printB();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
for (int i = 0; i < 5; i++) {
data.printC();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
//资源
class Data{
private int num = 1;
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
//A执行完调用B,B执行完调用C,C执行完调用A
public void printA() throws InterruptedException {
lock.lock();
try {
while (num!=1){
condition1.await(); //A等待
}
num=2; //A执行
System.out.println(Thread.currentThread().getName()+"->A执行,调用B");
condition2.signal(); //唤醒B
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() throws InterruptedException {
lock.lock();
try {
while (num!=2){
condition2.await();
}
num=3;
System.out.println(Thread.currentThread().getName()+"->B执行,调用C");
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() throws InterruptedException {
lock.lock();
try {
while (num!=3){
condition3.await();
}
num=1;
System.out.println(Thread.currentThread().getName()+"->C执行,调用A");
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
运行结果
由此,我们使用同步监视器完成了线程之间的精准通信。