文章目录
- 一、交替输出1A2B3C4D5E...
- 1.1 synchronized/wait/notify
- 1.2 Condition/await/signal
- 二、生产者–消费者问题
- 2.1 synchronized/wait/notify
- 2.2 Condition/await/signal
- 三、手写一个简单的阻塞队列
一、交替输出1A2B3C4D5E…
用两个线程,一个输出数字,一个输出字母,最后的结果是交替输出1A2B3C4D5E…
1.1 synchronized/wait/notify
说到线程的运用,最先想到的可能就是“synchronized/wait/notify”的组合:
- 用synchronized锁住一个对象,达到同一时间只能有一个线程输出的目的;
- 用wait和notify进行线程间的通信,达到交替输出的目的。
上面的思路总体上来说没有问题,不过两个线程同时启动时,获得CPU时间片的次序是不固定的,因此需要用一个变量来控制先输出数字。示例:
public class ThreadTest {
private static volatile boolean t2Started = false;
final Object lock = new Object();
char[] arrI = "1234567".toCharArray();
char[] arrC = "ABCDEFG".toCharArray();
public static void main(String[] args) {
new ThreadTest().testSyncWaitNofiy();
}
public void testSyncWaitNofiy() {
new Thread(() -> {
synchronized (lock) {
while (!t2Started) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (char c : arrC) {
System.out.print(c);
try {
lock.notify();
lock.wait();//让出锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();//必须,否则无法终止程序
}
}, "t1").start();
new Thread(() -> {
synchronized (lock) {
for (char c : arrI) {
System.out.print(c);
t2Started = true;
try {
lock.notify();
lock.wait();//让出锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();//必须,否则无法终止程序
}
}, "t2").start();
}
}
以上是经典的交替输出写法,以上代码有两种执行顺序:
- 1、t1先执行
假如t1先获得CPU时间片,会走入以下逻辑:
接着t1线程被暂停,t2线程获得锁,将t2Started变量置为true,然后打印数字,唤醒t1,t2线程自身被暂停。
这时t1线程获得锁,开始打印字母,然后唤醒t2,自己暂停 ------> t2线程获得锁,开始打印数组,然后唤醒t1,自己暂停…
最后,t2输出数字"7",然后唤醒t1,自己暂停。t1输出字母"G",然后唤醒t2,自己暂停。这时t1线程中第二个notify的作用就体现出来了。假如t1中没有第二个notify,在t1获得锁后,什么也不做,然后释放锁。但是由于没有唤醒其他被暂停的线程,t2将会一直被暂停!
所以即便t1线程的数字数组arrI已经不打印任何字母了,还是需要将暂停的t2线程唤醒,达到两个线程都停止的目的。 - 2、t2先执行
如果t2线程先获得时间片,相当于直接进入打印数字的逻辑,t1中while循环相关的代码不会被执行。
1.2 Condition/await/signal
用Lock和synchronized的逻辑,总体上是一样的,不过是将隐式锁换成了显式锁而已。示例:
public class ThreadTest {
private static volatile boolean t2Started = false;
char[] arrI = "1234567".toCharArray();
char[] arrC = "ABCDEFG".toCharArray();
public static void main(String[] args) {
new ThreadTest().testLockCondition();
}
public void testLockCondition() {
Lock lock = new ReentrantLock();
Condition conditionT1 = lock.newCondition();
Condition conditionT2 = lock.newCondition();
new Thread(() -> {
try {
lock.lock();
while (!t2Started) {
try {
conditionT1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (char c : arrI) {
System.out.print(c);
conditionT2.signal();
conditionT1.await();
}
conditionT2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1").start();
new Thread(() -> {
try {
lock.lock();
for (char c : arrC) {
System.out.print(c);
t2Started = true;
conditionT1.signal();
conditionT2.await();
}
conditionT1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t2").start();
}
}
二、生产者–消费者问题
生产者–消费者问题在之前的文章章已经介绍过,这里再写这个例子,是为了可以将这个例子和交替输出字符串的例子放在一起对比看:交替输出字符串的问题要达到交替输出(两个线程按顺序获取锁)的目的,所以在各自的线程中在暂停自身前先唤醒对方;而生产者消费者问题不需要两个线程交替执行,所以没有在暂停自身前唤醒对方的操作
。
在生产者—消费者问题中,生产者的主要职责是生产产品,产品既可以是数据,也可以是任务。消费者的主要职责是消费生产者生产的产品,这里的消费包括对产品所代表的数据进行加工处理或执行产品所代表的任务。
生产者–消费者问题中,实际上主要是包含了两类线程,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据。
为了解耦生产者和消费者的关系,通常会采用共享数据区的方式,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;消费者只需要从共享数据区中去获取数据,不再需要关心生产者的行为。这个共享数据区域中应该具备线程间并发协作的功能:
- 如果共享数据区已满的话,阻塞生产者继续放入数据;
- 如果共享数据区为空的话,阻塞消费者继续消费数据。
在实现生产者消费者问题时,可以采用三种方式:
- 使用Object的wait/notify的消息通知机制;
- 使用Condition的await/signal的消息通知机制;
- 使用BlockingQueue实现(BlockingQueue在后续文章详细介绍)。
通常,生产者和消费者的处理能力是不同的,即生产者生产产品的速率和消费者消费产品的速率是不同的,较为常见的情形是生产者的处理能力比消费者的处理能力大。这种情况下,传输通道所起的作用不仅仅作为生产者和消费者之间传递数据的中介,它在一定程度上还起到一个平衡生产者和消费者处理能力的作用。
按照生产者数量和消费者数量的组合来划分,可以分为以下几种:
类别 | 生产者线程数量 | 消费者线程数量 |
单生产者-单消费者 | 1 | 1 |
单生产者-多消费者 | 1 | N(N>=2) |
多生产者-多消费者 | N(N>=2) | N(N>=2) |
多生产者-单消费者 | N(N>=2) | 1 |
2.1 synchronized/wait/notify
此种方式指的是:通过配合调用Object对象的wait方法和notify方法或notifyAll方法来实现线程间的通信,简单来说可以分为两个步骤:
- 在线程中调用wait方法,将阻塞当前线程;
- 其他线程调用了调用notify方法或notifyAll方法进行通知之后,当前线程才能从wait方法返回,继续执行下面的操作。
假设有一个箱子,最多只能装10个苹果,箱子里没苹果时不能消费(简单理解为从箱子里往外取苹果),箱子里苹果的数量为10时不能生产(简单理解为向箱子里放苹果)。要实现这一效果,至少有三个点需要关注:
- 要达到
阻塞生产者和消费者
两种效果,需要用不同的条件判断+wait实现
;- 要达到
通知生产者和消费者正常运行下去
的效果,需要用notify/notifyAll实现
(具体用哪种通知方法看需求,notify是随机通知一个,notifyAll是通知所有);- 要
存放共享数据
,需要选择合适的集合类
(该例子比较简单,并没有用集合类,而是用了一个变量来实现)。
示例:
//箱子,存量苹果的容器
public class Box {
//箱子中目前苹果的数量
int num;
public synchronized void put() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//10是箱子中能存放苹果的最大数量
while (num == 10) {
try {
System.out.println("箱子满了,生产者暂停");
//等待消费者消费一个才能继续生产,所以要让出锁
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.println("箱子未装满,开始生产,生产后箱子中的苹果数量:"+num);
//唤醒可能因为没苹果而等待的消费者
this.notify();
}
public synchronized void take() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (num == 0) {
try {
System.out.println("箱子空了,消费者暂停");
//等待生产者生产一个才能继续消费,所以要让出锁
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num--;
System.out.println("箱子中有了苹果,开始消费,消费后箱子中的苹果数量:"+num);
//唤醒可能因为苹果满了而等待的生产者
this.notify();
}
}
//消费者
public class Consumer implements Runnable {
private Box box;
public Consumer(Box box) {
this.box= box;
}
@Override
public void run() {
while (true){
box.take();
}
}
}
//生产者
public class Producer implements Runnable {
private Box box;
public Producer(Box box) {
this.box= box;
}
@Override
public void run() {
while (true){
box.put();
}
}
}
public class ConsumerAndProducer {
public static void main(String[] args) {
Box box = new Box();
//生产者线程
Producer p1 = new Producer(box);
//消费者线程
Consumer c1 = new Consumer(box);
new Thread(p1).start();
new Thread(c1).start();
}
}
结果示例:
2.2 Condition/await/signal
该种方式实现生产者消费者,和之前一种方式原理是一样的,不过是将隐式锁换成了显式锁而已。以上个小节的功能为例,修改Box.java代码即可,示例:
//箱子,存量苹果的容器
public class Box {
//箱子中目前苹果的数量
int num;
Lock lock = new ReentrantLock();
Condition full = lock.newCondition();
Condition empty = lock.newCondition();
public void put() {
lock.lock();
try {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//10是箱子中能存放苹果的最大数量
while (num == 10) {
try {
System.out.println("箱子满了,生产者暂停");
//等待消费者消费一个才能继续生产,所以要让出锁
full.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.println("箱子未装满,开始生产,生产后箱子中的苹果数量:"+num);
//唤醒可能因为没苹果而等待的消费者
empty.signal();
}finally {
lock.unlock();
}
}
public synchronized void take() {
lock.lock();
try {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (num == 0) {
try {
System.out.println("箱子空了,消费者暂停");
//等待生产者生产一个才能继续消费,所以要让出锁
empty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num--;
System.out.println("箱子中有了苹果,开始消费,消费后箱子中的苹果数量:"+num);
//唤醒可能因为苹果满了而等待的生产者
full.signal();
}finally {
lock.unlock();
}
}
}
结果示例:
三、手写一个简单的阻塞队列
阻塞队列是一种线程安全的队列,当队列为空时,消费者线程将被阻塞直到队列中有元素可供消费;当队列已满时,生产者线程将被阻塞直到队列有空闲位置可供插入元素。
可以使用 Java 提供的 Lock 和 Condition 接口来实现阻塞队列。示例:
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BlockingQueue<T> {
private LinkedList<T> queue;
private int capacity;
private Lock lock;
private Condition notFull;
private Condition notEmpty;
public BlockingQueue(int capacity) {
this.capacity = capacity;
queue = new LinkedList<>();
lock = new ReentrantLock();
notFull = lock.newCondition();
notEmpty = lock.newCondition();
}
public void put(T element) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await();
}
queue.add(element);
notEmpty.signal();
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
T element = queue.remove();
notFull.signal();
return element;
} finally {
lock.unlock();
}
}
}
在上述代码中,我们使用了 LinkedList 作为队列的底层数据结构,使用 Lock 和 Condition 接口来实现阻塞队列,其中:
lock:用于同步队列的插入和删除操作;
notFull:用于在队列已满时阻塞生产者线程;
notEmpty:用于在队列为空时阻塞消费者线程。
- 阻塞队列的使用
使用阻塞队列时,我们需要先创建一个阻塞队列对象,指定队列的容量,然后在生产者线程中调用 put 方法向队列中插入元素,在消费者线程中调用 take 方法从队列中获取元素。示例:
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> queue = new BlockingQueue<>(10);
new Thread(() -> {
for (int i = 0; i < 20; i++) {
try {
queue.put(i);
System.out.println("Producer put: " + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 20; i++) {
try {
Integer element = queue.take();
System.out.println("Consumer take: " + element);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
在上述代码中,我们创建了一个容量为 10 的阻塞队列,启动了一个生产者线程和一个消费者线程,生产者线程向队列中插入了 20 个元素,消费者线程从队列中取出了 20 个元素。