halo,大家好,我是
方圆
,最近在整理之前写过的博客儿,把草率的和内容臃肿的进行了删除和重写,还是想让博客的内容质量高一些吧,这篇博客儿想写一写并发编程中的生产者消费者问题,之前面试被问过,我也给这个问题总结了一个很好理解的思路,“等待,业务,唤醒”,反正!看下去就行了!
1. 说在前头
生产者消费者问题最需要理解,一个线程需要另一个线程的配合
,就像是准备食材和做饭之间的关系,需要先准备好食材,才能做饭,这依靠的是线程之间的通信
来实现的。每个线程都有自己的等待执行条件
,所要执行的业务
,以及业务执行完成后对其他线程的唤醒
,可以简单的概括为《等待,业务,唤醒》
- synchronized 线程通信采用的是
wait()
和notifyall()
方法 - Lock 用的是 Condition 下的
await()
和signalAll()
方法
(注:
signal()
方法也有妙用,在多个Condition时,可用于定向的唤醒)
重要的要记住
:在对等待条件进行判断的时候,用的是while()而不是if(),
用while()来防止虚假唤醒问题,因为while()能一直检查条件,而if()只检查一次
2. 用synchronized实现0到9数字的交替打印
/**
* 用synchronized实现生产者消费者模式
* 用两个线程实现0到9数字的交替打印
*/
public class ProducerAndConsumer01 {
public static void main(String[] args) {
Task task = new Task();
// 创建打印偶数的线程A
new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
task.evenNumber();
}
} catch (Exception e) {
e.printStackTrace();
}
}, "A").start();
// 创建打印奇数的线程B
new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
task.oddNumber();
}
} catch (Exception e) {
e.printStackTrace();
}
}, "B").start();
}
}
class Task {
private int num = 0;
/**
* 打印奇数
*/
public synchronized void oddNumber() throws InterruptedException {
// 等待条件,注意这里使用的是while条件哦
while (num % 2 == 0) {
this.wait();
}
// 执行业务,数字 + 1并打印
System.out.println(Thread.currentThread().getName() + " -> " + num++);
// 唤醒打印偶数的线程
this.notifyAll();
}
/**
* 打印偶数
*/
public synchronized void evenNumber() throws InterruptedException {
// 等待条件
while (num % 2 == 1) {
this.wait();
}
// 执行业务,数字 + 1并打印
System.out.println(Thread.currentThread().getName() + " -> " + num++);
// 唤醒打印奇数的线程
this.notifyAll();
}
}
- 运行结果
3. 用ReentrantLock实现三个线程间的数字打印
- 用ReentrantLock的Condition来实现0到14数字的交替打印
注:await方法会将锁释放,三条线程会不停的抢锁,抢到锁之后判断执行条件,只有满足执行条件的时候才能去执行业务!!!
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerAndConsumer02 {
public static void main(String[] args) {
Task02 task = new Task02();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
task.printA();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
task.printB();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
task.printC();
}
}).start();
}
}
/**
* 三个线程,交替打印数字
*
* 注:await方法会将锁释放,三条线程会不停的抢锁,抢到锁之后判断执行条件,只有满足执行条件的时候才能执行业务!!!
*/
class Task02 {
// lock锁和三个不同的执行条件
private final Lock lock = new ReentrantLock();
private final Condition condition1 = lock.newCondition();
private final Condition condition2 = lock.newCondition();
private final Condition condition3 = lock.newCondition();
// 执行标志位,0,1,2分别为三条线程的执行条件
private int flag = 0;
private int num = 0;
public void printA() {
lock.lock();
try {
// 等待条件
while (flag != 0) {
condition1.await();
}
// 执行业务
System.out.println("大家好,我是" + Thread.currentThread().getName() + ",我要打印" + num++);
flag = 1;
// 唤醒
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
// 等待
while (flag != 1) {
condition2.await();
}
// 执行业务
System.out.println("大噶好啊,我是" + Thread.currentThread().getName() + ",我要打印" + num++);
flag = 2;
// 唤醒
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
// 等待
while (flag != 2) {
condition3.await();
}
// 执行业务
System.out.println("雷猴啊,我是" + Thread.currentThread().getName() + ",我要打印" + num++);
flag = 0;
// 唤醒
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
- 运行结果
3.1 执行流程分析
- 当
线程0拿到锁
的时候,线程1和线程2都会进入阻塞状态,当业务执行完成后,线程0释放锁
- 若此时
线程2拿到了锁
,那么根据等待条件,它会进入等待状态,此时线程2会释放锁
- 现在轮到
线程1拿到锁
,根据等待条件,它会执行业务,然后准备唤醒线程2
,为什么说是准备
唤醒呢?因为此时线程1还没有将锁释放
,直到线程1将锁释放之后
,线程2才能拿到锁
,被真正的唤醒,这样线程2才能执行它的业务
以上就展示了三个线程打印前3个数字的过程,大家多想一想,就能明白啦
4. 用volatile实现轻量级的交替数字打印
直接上代码吧!非常简单!
/**
* 用volatile实现生产者消费者
*/
public class ProducerAndConsumer03 {
public static void main(String[] args) {
Task03 task = new Task03();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
task.oddNumber();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
task.evenNumber();
}
}).start();
}
}
class Task03 {
/**
* 执行的标志位
*/
private volatile int flag = 0;
/**
* 打印的数字
*/
private int num = 1;
/**
* 打印奇数
*/
public void oddNumber() {
// 等待条件
while (flag != 0) {
}
// 业务
System.out.println(Thread.currentThread().getName() + " -> " + num++);
// 唤醒
flag = 1;
}
/**
* 打印偶数
*/
public void evenNumber() {
// 等待条件
while (flag != 1) {
}
// 业务
System.out.println(Thread.currentThread().getName() + " -> " + num++);
// 唤醒
flag = 0;
}
}
- 运行结果
执行流程分析
: 被标记为volatile的变量对各个线程都是可见的
,利用这一特性,定义一个标志位来控制线程的执行
收!