最典型的例子就是生产者-消费者问题:有一个商品队列,生产者想队列中添加商品,消费者取出队列中的商品;显然,如果队列为空,消费者应该等待生产者产生商品才能消费;如果队列满了,生产者需要等待消费者消费之后才能生产商品。队列就是这个模型中的临界资源,当队列为空时,而消费者获得了该对象的锁,如果不释放,那么生产者无法获得对象锁,而消费者无法消费对象,就进入了死锁状态;反之队列满时,生产者不释放对象锁也会造成死锁。这是我们不希望看到的,所以就有了线程间协作来解决这个问题。
主要是为了复用和解耦,例如常见的消息框架(非常经典的一种生产者消费者模型的使用场景)ActiveMQ,其发送端和接收端用Topic进行关联。Java语言实现线程之间通信协作的方式是等待/通知机制,该机制有两种实现方式——synchronized+wait/notify模式和Lock+Condition模式,本文分别对这两种实现等待/通知机制的模式进行剖析,说明使用它们实现线程协作的方法。
一、线程通信引子
在下面的例子中,虽然两个线程实现了通信,但是凭借 线程B不断地通过 while语句轮询来检测某一个条件,这样会导致CPU的浪费。
//资源类
class MyList {
//临界资源
private volatile List<String> list = new ArrayList<String>();
public void add() {
list.add("abc");
}
public int size() {
return list.size();
}
}
// 线程A
class ThreadA extends Thread {
private MyList list;
public ThreadA(MyList list,String name) {
super(name);
this.list = list;
}
@Override
public void run() {
try {
for (int i = 0; i < 3; i++) {
list.add();
System.out.println("添加了" + (i + 1) + "个元素");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//线程B
class ThreadB extends Thread {
private MyList list;
public ThreadB(MyList list,String name) {
super(name);
this.list = list;
}
@Override
public void run() {
try {
while (true) { // while语句轮询
if (list.size() == 2) {
System.out.println("==2了,线程b要退出了!");
throw new InterruptedException();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//测试
public class Test {
public static void main(String[] args) {
MyList service = new MyList();
ThreadA a = new ThreadA(service,"A");
ThreadB b = new ThreadB(service,"B");
a.start();
b.start();
}
}
运行结果:
添加了1个元素
添加了2个元素
==2了,线程b要退出了!
java.lang.InterruptedException
at test.ThreadB.run(Test.java:57)
添加了3个元素
过于浪费CPU,需要一种机制来减少 CPU资源的浪费,而且还能实现多个线程之间的通信。
二、wait/notify实现等待/通知机制
均非Thread类中所声明的方法,而是Object类中声明的方法。原因是每个对象都拥有monitor(锁),所以让当前线程等待某个对象的锁,当然应该通过这个对象来操作,而不是用当前线程来操作,因为当前线程可能会等待多个对象的锁,如果通过线程来操作,就非常复杂了。wait()、notify() 和 notifyAll()有以下特点:
都必须在同步块或者同步方法中才能执行;
2、当前线程必须拥有该对象的锁,才能执行wait()方法,wait()方法会阻塞当前线程,并且释放对象锁;
3、notify()方法可以唤醒一个(1/N)正在等待这个资源锁的线程,使其从wait()方法返回,但是不保证被唤醒的线程一定可以获得这个对象锁。
4、notifyAll()方法可以唤醒所有正在等待这个资源锁的线程,然后让它们去竞争资源锁,具体哪个能拿到就不知道了。
5、调用wait()、notify()和notifyAll()方法时都必须先对调用对象加锁。
每个锁对象都有两个队列,一个是等待队列,一个是同步队列(也可以叫做“就绪”队列)。同步队列存储了已就绪(将要竞争锁)的线程(这些都是下一次获取锁的“候选人”),等待队列存储了正在等待(被阻塞)的线程(连“候选”资格都没有)。当一个等待队列中的线程被唤醒后,才会进入同步队列(成为“候选人”),进而等待CPU的调度;反之,当一个线程调用某对象的wait()方法后,就会进入等待队列,等待被唤醒。唤醒只意味着进入了同步队列,不意味着一定能获得资源(诸多“候选人”中挑一个)。
下面看如何使用上述的wait()和notify()方法来解决生产者/消费者问题:
Producer.java
public class Producer implements Runnable {
private PriorityQueue<Integer> queue = null;//临界资源
private int queueSize = 0;
public Producer(PriorityQueue<Integer> queue,int queueSize) {
this.queue = queue;
this.queueSize = queueSize;
}
public void product() {
while(true) {
synchronized (queue) {
System.out.println("当前队列中数据数量是:"+ queue.size());
while(queue.size() == queueSize) { //对于生产者来说需要判断的是队列是否满了,如果满了就等待
System.out.println("队列已满,等待消费者消费....");
try {
//调用wait()方法会释放锁
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
queue.notify(); //这里为什么加个notify呢?是为了防止死锁,线程出现问题时,也要释放对象锁。
}
}
//如果队列没满,那么就往队列中加入数据
queue.offer(1);
queue.notify();
try {
Thread.sleep(100); //为什么加个休眠?是为了让我们可以在控制台看到生产者和消费者交替执行
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("向队列中插入一个数据,队列中剩余空间是:"+ (queueSize - queue.size()));
}
}
}
@Override
public void run() {
this.product();
}
}
Consumer.java
public class Consumer implements Runnable {
private PriorityQueue<Integer> queue = null;//临界资源
public Consumer(PriorityQueue<Integer> queue){
this.queue = queue;
}
private void consume() {
while(true) {
synchronized (queue) { //首先锁定对象
//如果队列为空,那么消费者无法消费,必须等待生产者产生商品,所以需要释放对象锁,并让自己进入等待状态
System.out.println("当前队列中剩余数据个数:"+ queue.size());
while(queue.size() == 0) {
System.out.println("队列为空,等待数据......");
try {
queue.wait(); //使用wait()这个方法的时候,对象必须是获取锁的状态,调用了这个方法后,线程会释放该对象锁
} catch (InterruptedException e) {
e.printStackTrace();
queue.notify();//这里为什么加个notify呢?是为了防止死锁,线程出现问题时,也要释放对象锁。
}
}
//如果不为空,取出第一个对象
queue.poll();
//注意notify()方法就是通知一个在对象上等待的线程
queue.notify();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("消费一个数据后,队列中剩余数据个数:"+ queue.size());
}
}
}
@Override
public void run() {
this.consume();
}
}
Test.java
public class Test {
public static void main(String[] args) {
int queueSize = 20;
//这里可以回忆一下JVM中多线程共享内存的知识
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
Consumer consumer = new Consumer(queue);
Producer producer = new Producer(queue, queueSize);
new Thread(consumer).start();
new Thread(producer).start();
}
}
运行结果:
当前队列中剩余数据个数:0
队列为空,等待数据......
当前队列中数据数量是:0
向队列中插入一个数据,队列中剩余空间是:19
当前队列中数据数量是:1
向队列中插入一个数据,队列中剩余空间是:18
消费一个数据后,队列中剩余数据个数:1
当前队列中剩余数据个数:1
消费一个数据后,队列中剩余数据个数:0
当前队列中剩余数据个数:0
队列为空,等待数据......
当前队列中数据数量是:0
向队列中插入一个数据,队列中剩余空间是:19
当前队列中数据数量是:1
向队列中插入一个数据,队列中剩余空间是:18
当前队列中数据数量是:2
向队列中插入一个数据,队列中剩余空间是:17
消费一个数据后,队列中剩余数据个数:2
当前队列中剩余数据个数:2
消费一个数据后,队列中剩余数据个数:1
当前队列中剩余数据个数:1
消费一个数据后,队列中剩余数据个数:0
当前队列中数据数量是:0
……
下图展示了整个过程:ConsumerThread首先获取到了对象的锁,准备从队列中取元素时发现队列空,然后调用对象的wait()方法,从而释放了锁并进入等待队列WaitQueue中,进入等待状态。由于ConsumerThread释放了锁,ProducerThread随后获取了对象的锁,开始生产数据,并调用对象的notify()方法,从而释放了锁并进入等待队列WaitQueue中,同时也将
ConsumerThread从WaitQueue移到SynchronizedQueue中。当ProducerThread释放了锁之后,ConsumerThread再次获取锁
并从wait()方法处继续执行。
wait()/notifyall()场景测试:
//资源类
class ValueObject {
public static List<String> list = new ArrayList<String>();
}
//元素添加线程
class ThreadAdd extends Thread {
private String lock = null;
public ThreadAdd(String lock,String name) {
super(name);
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
ValueObject.list.add("anyString");
lock.notifyAll(); // 唤醒所有 wait线程
System.out.println("add anyString end ThreadName=" + Thread.currentThread().getName());
}
}
}
//元素删除线程
class ThreadSubtract extends Thread {
private String lock = null;
public ThreadSubtract(String lock,String name) {
super(name);
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
if (ValueObject.list.size() == 0) {
System.out.println("wait begin ThreadName=" + Thread.currentThread().getName());
lock.wait();
System.out.println("wait end ThreadName=" + Thread.currentThread().getName());
}
ValueObject.list.remove(0);
System.out.println("list size=" + ValueObject.list.size());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//测试类
public class WaitNotifyallTest {
public static void main(String[] args) throws InterruptedException {
//锁对象
String lock = new String("lock");
ThreadSubtract subtract1Thread = new ThreadSubtract(lock,"subtract1Thread");
subtract1Thread.start();
ThreadSubtract subtract2Thread = new ThreadSubtract(lock,"subtract2Thread");
subtract2Thread.start();
Thread.sleep(1000);
ThreadAdd addThread = new ThreadAdd(lock,"addThread");
addThread.start();
}
}
运行结果:
wait begin ThreadName=subtract1Thread
wait begin ThreadName=subtract2Thread
add anyString end ThreadName=addThread
wait end ThreadName=subtract2Thread
list size=0
wait end ThreadName=subtract1Thread
Exception in thread "subtract1Thread" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:653)
at java.util.ArrayList.remove(ArrayList.java:492)
at com.wait.notifyall.ThreadSubtract.run(WaitNotifyallTest.java:57)
当线程subtract1Thread 被唤醒后,将从wait处继续执行。但由于线程subtract2Thread 先获取到锁得到运行,已经将ValueObject.list中的元素删除,导致线程subtract1Thread 继续向下执行到ValueObject.list.remove(0)时产生异常。像这种有多个相同类型的线程场景,为防止wait的条件发生变化而导致的线程异常终止,我们在阻塞线程被唤醒的同时还必须对wait的条件进行额外的检查,如下所示:
//元素删除线程
class ThreadSubtract extends Thread {
private String lock = null;
public ThreadSubtract(String lock,String name) {
super(name);
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
//if (ValueObject.list.size() == 0) { //使用if还是while结果大不相同
while (ValueObject.list.size() == 0) {
System.out.println("wait begin ThreadName=" + Thread.currentThread().getName());
lock.wait();
System.out.println("wait end ThreadName=" + Thread.currentThread().getName());
}
ValueObject.list.remove(0);
System.out.println("list size=" + ValueObject.list.size());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
将线程类ThreadSubtract的 run()方法中的 if 条件改为 while 条件即可。
根据以上例子可以提炼出等待/通知机制的经典范式,该范式分为两部分,分别针对等待方(消费者)和通知方(生产者)。
等待方遵循以下原则:
1)获取对象锁
2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件
3)条件满足则执行对应的逻辑
对应的伪代码如下:
synchronized(对象) {
while(条件不满足) {
对象.wait();
}
对应的处理逻辑
}
通知方遵循以下原则:
1)获取对象锁
2)改变条件
3)通知所有等待在对象上的线程
对应的伪代码如下:
synchronized(对象) {
改变条件
对象.notifyAll();
}
三、Condition接口实现等待/通知机制
Condition是在Java 1.5中出现的,它用来替代传统的Object的wait()/notify()实现线程间的协作。相比使用Object的wait()/notify(),使用Condition的await()/signal()这种方式能够更加安全和高效地实现线程间协作。我们不经就要问:Condition相比较Object监视器的三个方法有什么差别呢?
Condition 的 await()/signal() 使用都必须在lock保护之内,也就是说,必须在lock.lock()和lock.unlock之间才可以使用。事实上,Conditon的await()/signal() 与 Object的wait()/notify() 有着天然的对应关系:
Conditon中的await()对应Object的wait();
Condition中的signal()对应Object的notify();
Condition中的signalAll()对应Object的notifyAll()。
Condition强大的地方在于它能够精确的控制多线程的休眠与唤醒(注意是唤醒,唤醒只意味着进入了同步队列,不意味着一定能获得资源),这个意思就是有A/B/C/D四个线程共享Z资源,如果A占用了Z,并且调用了b_condition.notify()就可以释放资源唤醒B线程,而Object的nofity就无法保证B/C/D中会被唤醒哪一个了。其实多数线程间协作使用上述两种方式都可以实现,但是Sun推荐使用Condition来实现...我认为具体看你喜欢了,以及使用的熟练程度,除非你特别希望精确控制哪个线程被唤醒。
使用Condition往往比使用传统的通知/等待机制即Object的wait()/notify()要更灵活、高效。
//线程 A
class ThreadA extends Thread {
private MyService service;
public ThreadA(MyService service) {
super();
this.service = service;
}
@Override
public void run() {
service.awaitA();
}
}
//线程 B
class ThreadB extends Thread {
private MyService service;
public ThreadB(MyService service) {
super();
this.service = service;
}
@Override
public void run() {
service.awaitB();
}
}
class MyService {
private Lock lock = new ReentrantLock();
// 使用多个Condition实现通知部分线程
public Condition conditionA = lock.newCondition();
public Condition conditionB = lock.newCondition();
public void awaitA() {
lock.lock();
try {
System.out.println("begin awaitA时间为" + System.currentTimeMillis()
+ " ThreadName=" + Thread.currentThread().getName());
conditionA.await();
System.out.println("end awaitA时间为" + System.currentTimeMillis()
+ " ThreadName=" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void awaitB() {
lock.lock();
try {
System.out.println("begin awaitB时间为" + System.currentTimeMillis()
+ " ThreadName=" + Thread.currentThread().getName());
conditionB.await();
System.out.println("end awaitB时间为" + System.currentTimeMillis()
+ " ThreadName=" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signalAll_A() {
try {
lock.lock();
System.out.println("signalAll_A时间为" + System.currentTimeMillis()
+ " ThreadName=" + Thread.currentThread().getName());
conditionA.signalAll();
} finally {
lock.unlock();
}
}
public void signalAll_B() {
try {
lock.lock();
System.out.println("signalAll_B时间为" + System.currentTimeMillis()
+ " ThreadName=" + Thread.currentThread().getName());
conditionB.signalAll();
} finally {
lock.unlock();
}
}
}
//测试
public class ConditionTest {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
Thread.sleep(3000);
service.signalAll_A();
}
}
运行结果:
begin awaitA时间为1490275194875 ThreadName=A
begin awaitB时间为1490275194877 ThreadName=B
signalAll_A时间为1490275197876 ThreadName=main
end awaitA时间为1490275197876 ThreadName=A
Condition 实现了一种分组机制,将所有对临界资源进行访问的线程进行分组,以便实现线程间更精细化的协作,例如通知部分线程。我们可以从上面例子的输出结果看出,只有conditionA范围内的线程A被唤醒,而conditionB范围内的线程B仍然阻塞。
下面通过一个有界队列的示例来深入了解Condition的使用方式。有界队列是一种特殊的队列,当队列为空时,队列的获取操作会阻塞线程,直到队列中有新增元素,当队列已满时,队列的插入操作会阻塞,直到队列出现“空位”,代码如下所示:
public class BoundedQueue<T> {
private Lock lock = new ReentrantLock();
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
private Object[] items = null;
private int putptr; /*写索引*/
private int takeptr;/*读索引*/
private int count;/*队列中存在的数据个数*/
public BoundedQueue(int size){
items = new Object[size];
}
public void put(T t) throws InterruptedException{
lock.lock();
try{
while(count == items.length)
notFull.await(); //阻塞写线程
items[putptr] = t;
if(++putptr == items.length)
putptr = 0; //如果写索引写到队列的最后一个位置了,那么置为0
++count; //个数++
notEmpty.signal(); //唤醒读线程
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException{
lock.lock();
try{
while(count == 0)
notEmpty.await(); //阻塞读线程
Object x = items[takeptr];
if(++takeptr == items.length)
takeptr = 0; //如果读索引读到队列的最后一个位置了,那么置为0
--count; //个数++
notFull.signal(); //唤醒读线程
return (T)x;
} finally {
lock.unlock();
}
}
}
这是一个处于多线程工作环境下的有界队列,有界队列提供了两个方法,put和take,put是存数据,take是取数据,内部有个缓存队列,具体变量和方法说明见代码,这个缓存区类实现的功能:有多个线程往里面存数据和从里面取数据,其缓存队列(先进先出后进后出)能缓存的最大数值是size,多个线程间是互斥的,当缓存队列中存储的值达到size时,将写线程阻塞,并唤醒读线程,当缓存队列中存储的值为0时,将读线程阻塞,并唤醒写线程,这也是ArrayBlockingQueue的内部实现。在添加和删除方法中使用while循环而非if判断,目的是防止过早或意外的通知,只有条件符合才能退出循环。
使用Condition机制实现生产者-消费者模型:
Producer.java
import java.util.PriorityQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class Producer implements Runnable {
private PriorityQueue<Integer> queue = null;
private int queueSize = 0;
private Lock lock = null;
private Condition consume=null;
private Condition produce=null;
public Producer(PriorityQueue<Integer> queue,int queueSize,Lock lock,Condition produce,Condition consume){
this.queue = queue;
this.queueSize = queueSize;
this.lock = lock;
this.consume = consume;
this.produce = produce;
}
public void product(){
while(true){
lock.lock();
try{
while(queue.size()==queueSize){
System.out.println("队列满了,等待消费者消费...");
try {
produce.await();
} catch (InterruptedException e) {
e.printStackTrace();
consume.signal();
}
}
queue.offer(1);
System.out.println("向队列中插入了一个对象,队列的剩余空间是:"+(queueSize-queue.size()));
consume.signal();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}finally{
lock.unlock();
}
}
}
@Override
public void run() {
this.product();
}
}
Consumer.java
import java.util.PriorityQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class Consumer implements Runnable {
private PriorityQueue<Integer> queue = null;
private Lock lock = null;
private Condition consume = null;
private Condition produce = null;
public Consumer(PriorityQueue<Integer> queue,Lock lock,Condition produce,Condition consume){
this.queue = queue;
this.lock = lock;
this.consume = consume;
this.produce = produce;
}
@Override
public void run() {
// TODO 自动生成的方法存根
while(true){
lock.lock();
try{
while(queue.size() == 0){
System.out.println("队列为空,等待数据...");
try{
consume.await();
} catch (InterruptedException e) {
e.printStackTrace();
produce.signal();
}
}
queue.poll();
System.out.println("从队列中取出一个元素,队列剩余数量是:"+queue.size());
produce.signal();
try{
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
}
}
Test.java
public class Test {
public static void main(String[] args) {
// TODO 自动生成的方法存根
int queueSize = 20;
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
Lock lock = new ReentrantLock();
Condition produce = lock.newCondition();
Condition consume = lock.newCondition();
Consumer con = new Consumer(queue, lock, produce, consume);
Producer pro = new Producer(queue, queueSize, lock, produce, consume);
new Thread(con).start();
new Thread(pro).start();
}
}
运行结果:
队列为空,等待数据...
向队列中插入了一个对象,队列的剩余空间是:19
向队列中插入了一个对象,队列的剩余空间是:18
向队列中插入了一个对象,队列的剩余空间是:17
向队列中插入了一个对象,队列的剩余空间是:16
向队列中插入了一个对象,队列的剩余空间是:15
从队列中取出一个元素,队列剩余数量是:4
从队列中取出一个元素,队列剩余数量是:3
从队列中取出一个元素,队列剩余数量是:2
从队列中取出一个元素,队列剩余数量是:1
从队列中取出一个元素,队列剩余数量是:0
队列为空,等待数据...
向队列中插入了一个对象,队列的剩余空间是:19
向队列中插入了一个对象,队列的剩余空间是:18
向队列中插入了一个对象,队列的剩余空间是:17
向队列中插入了一个对象,队列的剩余空间是:16
向队列中插入了一个对象,队列的剩余空间是:15
向队列中插入了一个对象,队列的剩余空间是:14
……
在上述代码的实现结果中,如果不加上Thread.sleep()来让线程睡眠,我们看到的结果就像是单线程一样,生产者填满队列,消费者清空队列。为什么会这样呢?我们注意到,在“同步块”中,如果不是队列的临界值(0、maxSize),仅仅是调用notify来唤醒另一个等待该资源的线程,那么这个线程本身在释放这个锁之后也会加入锁的竞争中,到底谁得到这个锁,其实也说不清楚,修改sleep的睡眠时间,可以看到从100毫秒到2000毫秒,设置不同的休眠时间,可以观察到生产者与消费者也不会出现交替进行,还是随机的。那么为什么要用Condition实现对确定线程的唤醒操作呢?唤醒了又不一定得到锁,这个需要使用到await()来让当前线程必须等到其他线程来唤醒才能控制生产者与消费者的交替执行。
在produce.signal()和consume.signal()后面分别加上:consume.await()和produce.await()即可实现生产者和消费者(多个线程也可以控制任意两个线程交替执行)的交替执行,这个使用Object监视器方法在多个线程的情况下是不可能实现的,但是仅仅2个线程还是可以的。上述列子中,如果有多个消费者,那么如何在生产者完成生产后就只唤醒消费者线程呢?同样,用Condition实现就非常简单了,如果使用Object监视器类也可以实现,但是相对复杂,编程过程中容易出现死锁。