Java 多线程——生产者消费者模型 - 演义

1,184 阅读18分钟

这里主要解读(乱侃)生产者消费者模型的工作方式以及JDK5.0新增实现和传统同步关键字实现方式的区别。

在JDK5.0新增了Lock、Condition这两个接口来处理多线程安全问题。

Lock:可替代synchronized实现的同步函数或同步代码块。

Condition:封装 Object 监视器方法(wait、notify 和 notifyAll)成为专用的监视器对象, Lock 可以绑定多组监视器对象.

这俩兄弟的出世带来了什么利好呢?

Lock可以绑定多组Condition最重要的好处应该是,通过对Condition对象的分组使用(一组监视消费者,一组监视生产者)可以达到只唤醒对方线程的目的,而不必使用

notifyAll不分青红皂白唤醒所有线程。

API里给出了一个很好的示例(在这里

  1. class BoundedBuffer {  
  2.    final Lock lock = new ReentrantLock();  
  3.    final Condition notFull  = lock.newCondition();   
  4.    final Condition notEmpty = lock.newCondition();   
  5.   
  6.    final Object[] items = new Object[100];  
  7.    int putptr, takeptr, count;  
  8.   
  9.    public void put(Object x) throws InterruptedException {  
  10.      lock.lock();  
  11.      try {  
  12.        while (count == items.length)  
  13.          notFull.await();  
  14.        items[putptr] = x;  
  15.        if (++putptr == items.length) putptr = 0;  
  16.        ++count;  
  17.        notEmpty.signal();  
  18.      } finally {  
  19.        lock.unlock();  
  20.      }  
  21.    }  
  22.   
  23.    public Object take() throws InterruptedException {  
  24.      lock.lock();  
  25.      try {  
  26.        while (count == 0)  
  27.          notEmpty.await();  
  28.        Object x = items[takeptr];  
  29.        if (++takeptr == items.length) takeptr = 0;  
  30.        --count;  
  31.        notFull.signal();  
  32.        return x;  
  33.      } finally {  
  34.        lock.unlock();  
  35.      }  
  36.    }  
  37.  }  
class BoundedBuffer {
   final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition(); 
   final Condition notEmpty = lock.newCondition(); 

   final Object[] items = new Object[100];
   int putptr, takeptr, count;

   public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
       while (count == items.length)
         notFull.await();
       items[putptr] = x;
       if (++putptr == items.length) putptr = 0;
       ++count;
       notEmpty.signal();
     } finally {
       lock.unlock();
     }
   }

   public Object take() throws InterruptedException {
     lock.lock();
     try {
       while (count == 0)
         notEmpty.await();
       Object x = items[takeptr];
       if (++takeptr == items.length) takeptr = 0;
       --count;
       notFull.signal();
       return x;
     } finally {
       lock.unlock();
     }
   }
 }

这里实现了一个线程安全的缓冲区,下面对它进行一番演义。

  1. /** 
  2.  * BoundedBuffer2 诸葛亮率领的蜀汉军队的粮仓(2.0版本) 
  3.  */  
  4. public class BoundedBuffer2 {  
  5.         // lock-粮草官 :主要职责 督查筹粮分配粮食;主要装备:高音大喇叭  
  6.         final Lock lock = new ReentrantLock();  
  7.   
  8.         /* 
  9.          * Condition:诸葛武侯引进的黑科技:两路信号手动切换多功能高音大喇叭 
  10.          *  
  11.          * notFull (筹粮队信号):粮草官发现粮仓没装满时 
  12.          * 拿起他的大喇叭打开通向筹粮兵这路信号将响起(只有负责筹粮的士兵能接收到此路信号,所以只唤醒负责筹粮的士兵): 
  13.          * “丫的粮仓还没装满!去弄粮食,接丞相令是要渭水分兵屯田的,屯田不行去找刘禅要呀,要不来去曹魏那抢呀!” 
  14.          *  
  15.          * notEmpty(火头军信号): 
  16.          * 粮草官发现粮仓已经有粮食的时候(不是空的了)拿起他的大喇叭向火头军喊话(只有他们能接收到):“喂,喂!有粮食了”。 
  17.          */  
  18.         final Condition notFull = lock.newCondition();  
  19.         final Condition notEmpty = lock.newCondition();  
  20.   
  21.         final Object[] items = new Object[100]; // 粮仓总共能装100万石粮食,够蜀汉十万大军吃十个月了。  
  22.         int putptr, takeptr, count;  
  23.   
  24.         public void put(Object x) throws InterruptedException { // 筹粮的具体运作方式  
  25.             lock.lock();// 首先粮官就位,开始监工(入库)嘛!  
  26.             try {  
  27.                 while (count == items.length)  
  28.                     // 一旦粮仓装满  
  29.                     notFull.await();// 粮官大喇叭响起(通向正在筹粮兵信号  如果有多个筹粮队伍(生产者线程)也只是通知正在工作的这一个  ):  
  30.                                                                         //"正在筹粮士兵全线 立刻 就地 停止工作 等待通知!"。  
  31.                 // 没满的话就筹粮嘛,putptr大粮仓内带有编号的小粮仓一百个,从0到99按顺序往里装嘛  
  32.                 items[putptr] = x;  
  33.                 System.out.println(Thread.currentThread().getName()+"---"+putptr+"号小粮仓  运---进 粮食,现存粮食共计:"+(count+ 1)+"小仓!");  
  34.                 if (++putptr == items.length)// 装到99号小粮仓了咋办?  
  35.                     // 从0号开始继续装嘛。列为看官可能有问题了 0号不是有粮食吗?  
  36.                     // 没有了,筹粮的同时被火头军运走做饭消耗了。(循环队列)  
  37.                     putptr = 0;  
  38.                 ++count;// 计数,新增了一小粮仓粮食  
  39.                 notEmpty.signal();// 粮官大喇叭响起(通向已经休息的火头军中的一支):粮仓已经有粮食了,可以来取了!  
  40.             } finally {  
  41.                 lock.unlock();// 这一小仓入库完毕 ,粮官休息去了!  
  42.             }  
  43.         }  
  44.   
  45.         public Object take() throws InterruptedException {  
  46.             lock.lock();// 首先粮官就位,开始监工(出库)嘛!  
  47.             try {  
  48.                 while (count == 0)  
  49.                     // 一旦粮仓一颗粮食都没有  
  50.                     notEmpty.await();// 粮官大喇叭响起(通向来取粮的火头军):粮仓没有粮食了,不要来取了!  
  51.                 // 粮仓没空的话就取出粮食,takeptr 大粮仓内带有编号的小粮仓一百个,从0到99按顺序往外取出粮食  
  52.                 Object x = items[takeptr];  
  53.                 System.out.println(Thread.currentThread().getName()+"---"+takeptr+"号小粮仓  运出  粮食,现存粮食共计:"+(count- 1)+"小仓!");  
  54.                 if (++takeptr == items.length)// 取到99号小粮仓了  
  55.                     takeptr = 0;// 从0号开始继续取  
  56.                 --count;// 计数,减少了一小粮仓粮食  
  57.                 notFull.signal();// 粮官大喇叭响起(通向已休息的筹粮兵中的一支):喂!喂!筹粮士兵开工,粮仓不满了,有空位了,渣渣!  
  58.                 return x;// 这一小仓运出库  
  59.             } finally {  
  60.                 lock.unlock();// 这一小出入库完毕 ,粮官休息去了!  
  61.             }  
  62.         }  
  63.     }  
/**
 * BoundedBuffer2 诸葛亮率领的蜀汉军队的粮仓(2.0版本)
 */
public class BoundedBuffer2 {
		// lock-粮草官 :主要职责 督查筹粮分配粮食;主要装备:高音大喇叭
		final Lock lock = new ReentrantLock();

		/*
		 * Condition:诸葛武侯引进的黑科技:两路信号手动切换多功能高音大喇叭
		 * 
		 * notFull (筹粮队信号):粮草官发现粮仓没装满时
		 * 拿起他的大喇叭打开通向筹粮兵这路信号将响起(只有负责筹粮的士兵能接收到此路信号,所以只唤醒负责筹粮的士兵):
		 * “丫的粮仓还没装满!去弄粮食,接丞相令是要渭水分兵屯田的,屯田不行去找刘禅要呀,要不来去曹魏那抢呀!”
		 * 
		 * notEmpty(火头军信号):
		 * 粮草官发现粮仓已经有粮食的时候(不是空的了)拿起他的大喇叭向火头军喊话(只有他们能接收到):“喂,喂!有粮食了”。
		 */
		final Condition notFull = lock.newCondition();
		final Condition notEmpty = lock.newCondition();

		final Object[] items = new Object[100];// 粮仓总共能装100万石粮食,够蜀汉十万大军吃十个月了。
		int putptr, takeptr, count;

		public void put(Object x) throws InterruptedException {// 筹粮的具体运作方式
			lock.lock();// 首先粮官就位,开始监工(入库)嘛!
			try {
				while (count == items.length)
					// 一旦粮仓装满
					notFull.await();// 粮官大喇叭响起(通向正在筹粮兵信号  如果有多个筹粮队伍(生产者线程)也只是通知正在工作的这一个  ):
																		//"正在筹粮士兵全线 立刻 就地 停止工作 等待通知!"。
				// 没满的话就筹粮嘛,putptr大粮仓内带有编号的小粮仓一百个,从0到99按顺序往里装嘛
				items[putptr] = x;
				System.out.println(Thread.currentThread().getName()+"---"+putptr+"号小粮仓  运---进 粮食,现存粮食共计:"+(count+1)+"小仓!");
				if (++putptr == items.length)// 装到99号小粮仓了咋办?
					// 从0号开始继续装嘛。列为看官可能有问题了 0号不是有粮食吗?
					// 没有了,筹粮的同时被火头军运走做饭消耗了。(循环队列)
					putptr = 0;
				++count;// 计数,新增了一小粮仓粮食
				notEmpty.signal();// 粮官大喇叭响起(通向已经休息的火头军中的一支):粮仓已经有粮食了,可以来取了!
			} finally {
				lock.unlock();// 这一小仓入库完毕 ,粮官休息去了!
			}
		}

		public Object take() throws InterruptedException {
			lock.lock();// 首先粮官就位,开始监工(出库)嘛!
			try {
				while (count == 0)
					// 一旦粮仓一颗粮食都没有
					notEmpty.await();// 粮官大喇叭响起(通向来取粮的火头军):粮仓没有粮食了,不要来取了!
				// 粮仓没空的话就取出粮食,takeptr 大粮仓内带有编号的小粮仓一百个,从0到99按顺序往外取出粮食
				Object x = items[takeptr];
				System.out.println(Thread.currentThread().getName()+"---"+takeptr+"号小粮仓  运出  粮食,现存粮食共计:"+(count-1)+"小仓!");
				if (++takeptr == items.length)// 取到99号小粮仓了
					takeptr = 0;// 从0号开始继续取
				--count;// 计数,减少了一小粮仓粮食
				notFull.signal();// 粮官大喇叭响起(通向已休息的筹粮兵中的一支):喂!喂!筹粮士兵开工,粮仓不满了,有空位了,渣渣!
				return x;// 这一小仓运出库
			} finally {
				lock.unlock();// 这一小出入库完毕 ,粮官休息去了!
			}
		}
	}

这里缓冲区是一个阻塞的循环队列,存取粮食的过程如下:


测试一下:

  1. /** 
  2.  * 封装生产者任务:负责筹粮 
  3.  *  
  4.  */  
  5. class Producer implements Runnable {  
  6.     private BoundedBuffer2 buffer;  
  7.   
  8.     public Producer(BoundedBuffer2 buffer) {  
  9.         this.buffer = buffer;  
  10.   
  11.     }  
  12.   
  13.     public void run() {  
  14.         int i = 0;  
  15.   
  16.         try {  
  17.             while (true) {  
  18.                 // Thread.sleep(1);  
  19.                 buffer.put(new Object());  
  20.   
  21.             }  
  22.         } catch (InterruptedException e) {  
  23.             e.printStackTrace();  
  24.         }  
  25.     }  
  26.   
  27. }  
  28.   
  29. /** 
  30.  * 封装消费者任务:负责取粮 
  31.  *  
  32.  */  
  33. class Consumer implements Runnable {  
  34.     private BoundedBuffer2 buffer;  
  35.   
  36.     public Consumer(BoundedBuffer2 buffer) {  
  37.         this.buffer = buffer;  
  38.     }  
  39.   
  40.     public void run() {  
  41.         int i = 0;  
  42.   
  43.         try {  
  44.             while (true) {  
  45.                 // Thread.sleep(1);  
  46.                 buffer.take();  
  47.             }  
  48.         } catch (InterruptedException e) {  
  49.             e.printStackTrace();  
  50.         }  
  51.     }  
  52.   
  53. }  
  54.   
  55. public class MultithreadTest {  
  56.     public static void main(String[] args) {  
  57.         BoundedBuffer2 buffer = new BoundedBuffer2();  
  58.         Producer pdc = new Producer(buffer);  
  59.         Consumer csm = new Consumer(buffer);  
  60.         Thread pThread1 = new Thread(pdc);  
  61.         Thread pThread2 = new Thread(pdc);  
  62.         Thread cThread1 = new Thread(csm);  
  63.         Thread cThread2 = new Thread(csm);  
  64.         pThread1.start();  
  65.         pThread2.start();  
  66.         cThread1.start();  
  67.         cThread2.start();  
  68.     }  
  69. }  
/**
 * 封装生产者任务:负责筹粮
 * 
 */
class Producer implements Runnable {
	private BoundedBuffer2 buffer;

	public Producer(BoundedBuffer2 buffer) {
		this.buffer = buffer;

	}

	public void run() {
		int i = 0;

		try {
			while (true) {
				// Thread.sleep(1);
				buffer.put(new Object());

			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

}

/**
 * 封装消费者任务:负责取粮
 * 
 */
class Consumer implements Runnable {
	private BoundedBuffer2 buffer;

	public Consumer(BoundedBuffer2 buffer) {
		this.buffer = buffer;
	}

	public void run() {
		int i = 0;

		try {
			while (true) {
				// Thread.sleep(1);
				buffer.take();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

}

public class MultithreadTest {
	public static void main(String[] args) {
		BoundedBuffer2 buffer = new BoundedBuffer2();
		Producer pdc = new Producer(buffer);
		Consumer csm = new Consumer(buffer);
		Thread pThread1 = new Thread(pdc);
		Thread pThread2 = new Thread(pdc);
		Thread cThread1 = new Thread(csm);
		Thread cThread2 = new Thread(csm);
		pThread1.start();
		pThread2.start();
		cThread1.start();
		cThread2.start();
	}
}

运行现象:



以上是JDK5.0后的Lock和Condition的实现方式,下面看一下synchronized 实现主要注释不同之处,这里用同步方法,用同步代码块也是可以。

  1. /** 
  2.  * BoundedBuffer 诸葛亮率领的蜀汉军队的粮仓(1.0版本) 
  3.  */  
  4. public class BoundedBuffer {  
  5.     // this-粮草官 :主要职责 督查筹粮分配粮食;主要装备:无,靠吼!  
  6.     final Object lock = new Object();  
  7.     final Object[] items = new Object[100]; // 粮仓总共能装100万石粮食,够蜀汉十万大军吃十个月了。  
  8.     int putptr, takeptr, count;  
  9.   
  10.     public synchronized void put(Object x)  throws InterruptedException {  
  11.         while (count == items.length)  
  12.             this.wait();// 粮官扯着嗓子吼起(来到粮草的兵,不分什么兵都会接收到):正在筹粮士兵全线 立刻 就地 停止工作 等待通知  
  13.         items[putptr] = x;  
  14.         System.out.println(Thread.currentThread().getName() + "---" + putptr  
  15.                 + "号小粮仓  运---进 粮食,现存粮食共计:" + (count +  1) + "小仓!");  
  16.         if (++putptr == items.length)  
  17.             putptr = 0;  
  18.         ++count;  
  19.         this.notifyAll();// 粮官吼起(通向所有已经休息的小部队):粮仓已经有粮食了,可以来取了!  
  20.     }  
  21.   
  22.     public synchronized Object take() throws InterruptedException {  
  23.         while (count == 0)  
  24.             this.wait();// 粮官扯着嗓子吼起(来到粮草的兵,不分什么兵都会接收到):粮仓没有粮食了,不要来取了!  
  25.         Object x = items[takeptr];  
  26.         System.out.println(Thread.currentThread().getName() + "---" + takeptr  
  27.                 + "号小粮仓  运出  粮食,现存粮食共计:" + (count -  1) + "小仓!");  
  28.         if (++takeptr == items.length)  
  29.             takeptr = 0;  
  30.         --count;  
  31.         this.notifyAll();// 粮官大喇叭响起(通向筹粮兵):喂!喂!筹粮士兵全线开工,粮仓不满了,有空位了,渣渣!  
  32.         return x;  
  33.     }  
  34. }  
/**
 * BoundedBuffer 诸葛亮率领的蜀汉军队的粮仓(1.0版本)
 */
public class BoundedBuffer {
	// this-粮草官 :主要职责 督查筹粮分配粮食;主要装备:无,靠吼!
	final Object lock = new Object();
	final Object[] items = new Object[100];// 粮仓总共能装100万石粮食,够蜀汉十万大军吃十个月了。
	int putptr, takeptr, count;

	public synchronized void put(Object x) throws InterruptedException {
		while (count == items.length)
			this.wait();// 粮官扯着嗓子吼起(来到粮草的兵,不分什么兵都会接收到):正在筹粮士兵全线 立刻 就地 停止工作 等待通知
		items[putptr] = x;
		System.out.println(Thread.currentThread().getName() + "---" + putptr
				+ "号小粮仓  运---进 粮食,现存粮食共计:" + (count + 1) + "小仓!");
		if (++putptr == items.length)
			putptr = 0;
		++count;
		this.notifyAll();// 粮官吼起(通向所有已经休息的小部队):粮仓已经有粮食了,可以来取了!
	}

	public synchronized Object take() throws InterruptedException {
		while (count == 0)
			this.wait();// 粮官扯着嗓子吼起(来到粮草的兵,不分什么兵都会接收到):粮仓没有粮食了,不要来取了!
		Object x = items[takeptr];
		System.out.println(Thread.currentThread().getName() + "---" + takeptr
				+ "号小粮仓  运出  粮食,现存粮食共计:" + (count - 1) + "小仓!");
		if (++takeptr == items.length)
			takeptr = 0;
		--count;
		this.notifyAll();// 粮官大喇叭响起(通向筹粮兵):喂!喂!筹粮士兵全线开工,粮仓不满了,有空位了,渣渣!
		return x;
	}
}

小结:通过2.0版本的粮草官持有高科技喇叭可以指定唤醒筹粮队或者是火头军,而不像1.0版本那样不能区分军种在需要筹粮时把火头军也唤醒了。

这就是Lock、Condition这对组合相较与synchronized 实现方式的优势。另外ArrayBlockingQueue这个类提供了线程安全有界缓存区功能,有需要可以使用这个类,本篇中2.0版本就是其核心实现方式。