Java线程通信与协作的解决方案——等待/通知机制

来源:互联网 发布:apk生成php源码 编辑:程序博客网 时间:2024/05/07 17:12

        线程与线程之间不是相互独立的个体,它们彼此之间需要相互通信和协作,最典型的例子就是生产者-消费者问题:有一个商品队列,生产者想队列中添加商品,消费者取出队列中的商品;显然,如果队列为空,消费者应该等待生产者产生商品才能消费;如果队列满了,生产者需要等待消费者消费之后才能生产商品。队列就是这个模型中的临界资源,当队列为空时,而消费者获得了该对象的锁,如果不释放,那么生产者无法获得对象锁,而消费者无法消费对象,就进入了死锁状态;反之队列满时,生产者不释放对象锁也会造成死锁。这是我们不希望看到的,所以就有了线程间协作来解决这个问题。

        其实说到生产者与消费者模型,我们不能简单的知道怎么实现,而是需要知这种模型的使用场景:主要是为了复用和解耦,例如常见的消息框架(非常经典的一种生产者消费者模型的使用场景)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();    }}// 线程Aclass 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();        }    }}//线程Bclass 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实现等待/通知机制


        wait/notify机制主要由 Object类中的三个方法保证:wait()、notify() 和 notifyAll()。这三个方法均非Thread类中所声明的方法,而是Object类中声明的方法。原因是每个对象都拥有monitor(锁),所以让当前线程等待某个对象的锁,当然应该通过这个对象来操作,而不是用当前线程来操作,因为当前线程可能会等待多个线程的锁,如果通过线程来操作,就非常复杂了。wait()、notify() 和 notifyAll()有以下特点:

        1、都是Object类中的本地方法,且为final,无法被重写;且这三个方都必须在同步块或者同步方法中才能执行
        2、当前线程必须拥有该对象的锁,才能执行wait()方法,wait()方法会阻塞当前线程,并且释放对象锁
        3、notify()方法可以唤醒一个(1/N)正在等待这个资源锁的线程,但是不保证被唤醒的线程一定可以获得这个对象锁。
        4、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 {                          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……
        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=subtract1Threadwait begin ThreadName=subtract2Threadadd anyString end ThreadName=addThreadwait  end ThreadName=subtract2Threadlist size=0wait  end ThreadName=subtract1ThreadException in thread "subtract1Thread" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0at 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 条件即可。


三、Condition接口实现等待/通知机制


        Condition是在Java 1.5中出现的,它用来替代传统的Object的wait()/notify()实现线程间的协作。相比使用Object的wait()/notify(),使用Condition的await()/signal()这种方式能够更加安全和高效地实现线程间协作。我们不经就要问:Condition相比较Object监视器的三个方法有什么差别呢?


        Condition是个接口,基本的方法就是await()和signal()方法。Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() 。 必须要注意的是,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时,使用Lock来替代Synchronized关键字来实现操作的原子性,实现对临界资源的加锁与解锁,同样的,Condition中提供的三个方法也需要在“同步块”中进行。相比较而言,Condition强大的地方在于它能够精确的控制多线程的休眠与唤醒(注意是唤醒,唤醒只意味着进入了就绪队列,不意味着一定能获得资源),这个意思就是有A/B/C/D四个线程共享Z资源,如果A占用了Z,并且调用了b_condition.notify()就可以释放资源唤醒B线程,而Object的nofity就无法保证B/C/D中会被唤醒哪一个了。其实多数线程间协作实用上述两种方式都可以实现,但是Sun推荐使用Condition来实现...我认为具体看你喜欢了,以及使用的熟练程度,除非你特别希望精确控制哪个线程被唤醒。

        使用Condition往往比使用传统的通知等待机制(Object的wait()/notify())要更灵活、高效。

//线程 Aclass ThreadA extends Thread {private MyService service;public ThreadA(MyService service) {     super();     this.service = service;}@Overridepublic void run() {     service.awaitA();}}//线程 Bclass ThreadB extends Thread {private MyService service;public ThreadB(MyService service) {     super();     this.service = service;}@Overridepublic 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=Abegin awaitB时间为1490275194877 ThreadName=BsignalAll_A时间为1490275197876 ThreadName=mainend awaitA时间为1490275197876 ThreadName=A
        可以看到只有线程A被唤醒,线程B仍然阻塞。实际上,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;      }   @Overridepublic 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监视器类也可以实现,但是相对复杂,编程过程中容易出现死锁。


参考资料:http://blog.csdn.net/zhshulin/article/details/50762465

           http://blog.csdn.net/ghsau/article/details/7481142

1 0
原创粉丝点击