生產者與消費者問題是多線程同步的一個經典問題。生產者和消費者同時使用一塊緩衝區,生產者生產商品放入緩衝區,消費者從緩衝區中取出商品。我們需要保證的是,當緩衝區滿時,生產者不可生產商品;當緩衝區爲空時,消費者不可取出商品。
下面介紹java中幾種解決同步問題的方式
-
wait()與notify()方法
-
Lock與Condition機制
-
BlockingQueue阻塞隊列
【1】wait()與notify()方法
這兩個方法是object類中的方法
wait()用在以下場合:
(1)當緩衝區滿時,緩衝區調用wait()方法,使得生產者釋放鎖,當前線程阻塞,其他線程可以獲得鎖。
(2)當緩衝區空時,緩衝區調用wait()方法,使得消費者釋放鎖,當前線程阻塞,其他線程可以獲得鎖。
notify()用在以下場合:
(1)當緩衝區未滿時,生產者生產商品放入緩衝區,然後緩衝區調用notify()方法,通知上一個因wait()方法釋放鎖的線程現在可以去獲得鎖了,同步塊代碼執行完成後,釋放對象鎖,此處的對象鎖,鎖住的是緩衝區。
(2)當緩衝區不爲空時,消費者從緩衝區中取出商品,然後緩衝區調用notify()方法,通知上一個因wait()方法釋放鎖的線程現在可以去獲得鎖了,同步塊代碼執行完成後,釋放對象鎖。
import java.util.LinkedList; /** * 生產者消費者問題 */ public class ProAndCon { //最大容量 public static final int MAX_SIZE = 2; //存儲媒介 public static LinkedList<Integer> list = new LinkedList<>(); class Producer implements Runnable { @Override public void run() { synchronized (list) { //倉庫容量已經達到最大值 while (list.size() == MAX_SIZE) { System.out.println("倉庫已滿,生產者" + Thread.currentThread().getName() + "不可生產."); try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } list.add(1); System.out.println("生產者" + Thread.currentThread().getName() + "生產, 倉庫容量爲" + list.size()); list.notify(); } } } class Consumer implements Runnable { @Override public void run() { synchronized (list) { while (list.size() == 0) { System.out.println("倉庫爲空,消費者" + Thread.currentThread().getName() + "不可消費."); try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } list.removeFirst(); System.out.println("消費者" + Thread.currentThread().getName() + "消費,倉庫容量爲" + list.size()); list.notify(); } } } public static void main(String[] args) { ProAndCon proAndCon = new ProAndCon(); Producer producer = proAndCon.new Producer(); Consumer consumer = proAndCon.new Consumer(); for (int i = 0; i < 2; i++) { Thread pro = new Thread(producer); pro.start(); Thread con = new Thread(consumer); con.start(); } } } |
【2】Lock與Condition機制
在JDK5.0之後,Java提供了Lock與Condition機制。Condition接口的await()和signal()是用來做同步的兩種方法,它們的功能基本上和Object的wait()、nofity()相同,或者說可以取代它們,但是它們和Lock機制是直接掛鉤的。通過在Lock對象上調用newCondition()方法,將條件變量和一個鎖對象進行綁定,進而控制併發程序訪問競爭資源的安全。
import java.util.LinkedList; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ProAndCon2 { public static final int MAX_SIZE = 2; public static LinkedList<Integer> list = new LinkedList<>(); public static Lock lock = new ReentrantLock(); //倉庫滿的條件變量 public static Condition full = lock.newCondition(); //倉庫空的條件變量 public static Condition empty = lock.newCondition(); class Producer implements Runnable { @Override public void run() { lock.lock(); while (list.size() == MAX_SIZE) { try { System.out.println("倉庫已滿,生產者" + Thread.currentThread().getName() + "不可生產."); full.await(); } catch (InterruptedException e) { e.printStackTrace(); } } list.add(1); System.out.println("生產者" + Thread.currentThread().getName() + "生產, 倉庫容量爲" + list.size()); //喚醒其他生產者與消費者線程 full.signal(); empty.signal(); lock.unlock(); } } class Consumer implements Runnable { @Override public void run() { lock.lock(); while (list.size() == 0) { try { System.out.println("倉庫爲空,消費者" + Thread.currentThread().getName() + "不可消費."); empty.await(); } catch (InterruptedException e) { e.printStackTrace(); } } list.removeFirst(); System.out.println("消費者" + Thread.currentThread().getName() + "消費,倉庫容量爲" + list.size()); //喚醒其他生產者與消費者線程 full.signal(); empty.signal(); lock.unlock(); } } public static void main(String[] args) { ProAndCon2 proAndCon = new ProAndCon2(); Producer producer = proAndCon.new Producer(); Consumer consumer = proAndCon.new Consumer(); for (int i = 0; i < 2; i++) { Thread pro = new Thread(producer); pro.start(); Thread con = new Thread(consumer); con.start(); } } } |
【3】使用BlockingQueue阻塞隊列
什麼是阻塞隊列?
如果向一個已經滿了的隊列中添加元素或者從空隊列中移除元素,都將會導致線程阻塞,線程一直等待到有舊元素被移除或新元素被添加的時候,才能繼續執行。符合這種情況的隊列,稱爲阻塞隊列。
JDK 1.5 以後新增BlockingQueue接口,我們採用它實現類的其中兩個類,ArrayBlockingQueue或者是LinkedBlockingQueue。
怎麼使用LinkedBlockingQueue?
這裏我們用LinkedBlockingQueue來解決生產者與消費者問題,主要用到它的兩個方法,即put()與take()
put():向阻塞隊列中添加一個元素,隊列滿時,自動阻塞。
take():從阻塞隊列中取出一個元素,隊列空時,自動阻塞。
其實LinkedBlockingQueue底層使用的仍然是Lock與Condition機制,我們從源碼就可以看出來
//..............用到了Lock與Condition機制 /** Lock held by take, poll, etc */ private final ReentrantLock takeLock = new ReentrantLock(); /** Wait queue for waiting takes */ private final Condition notEmpty = takeLock.newCondition(); /** Lock held by put, offer, etc */ private final ReentrantLock putLock = new ReentrantLock(); /** Wait queue for waiting puts */ private final Condition notFull = putLock.newCondition(); //...........put方法 /** * Inserts the specified element at the tail of this queue, waiting if * necessary for space to become available. * * @throws InterruptedException {@inheritDoc} * @throws NullPointerException {@inheritDoc} */ public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); // Note: convention in all put/take/etc is to preset local var // holding count negative to indicate failure unless set. int c = -1; Node<E> node = new Node<E>(e); final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; putLock.lockInterruptibly(); try { /* * Note that count is used in wait guard even though it is * not protected by lock. This works because count can * only decrease at this point (all other puts are shut * out by lock), and we (or some other waiting put) are * signalled if it ever changes from capacity. Similarly * for all other uses of count in other wait guards. */ while (count.get() == capacity) { notFull.await(); } enqueue(node); c = count.getAndIncrement(); if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } if (c == 0) signalNotEmpty(); } //...........take方法 public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); try { while (count.get() == 0) { notEmpty.await(); } x = dequeue(); c = count.getAndDecrement(); if (c > 1) notEmpty.signal(); } finally { takeLock.unlock(); } if (c == capacity) signalNotFull(); return x; } |
看得出來,LinkedBlockingQueue底層已經解決好了同步問題,我們可以很方便的使用它。
代碼演示:
import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; /** * 解決生產者與消費者問題 * 採用阻塞隊列BlockingQueue */ public class ProAndCon3 { public static final int MAX_SIZE = 2; public static BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(MAX_SIZE); class Producer implements Runnable { @Override public void run() { if (queue.size() == MAX_SIZE) { System.out.println("倉庫已滿,生產者" + Thread.currentThread().getName() + "不可生產."); } try { queue.put(1); System.out.println("生產者" + Thread.currentThread().getName() + "生產, 倉庫容量爲" + queue.size()); } catch (InterruptedException e) { e.printStackTrace(); } } } class Consumer implements Runnable { @Override public void run() { if (queue.size() == 0) { System.out.println("倉庫爲空,消費者" + Thread.currentThread().getName() + "不可消費."); } try { queue.take(); System.out.println("消費者" + Thread.currentThread().getName() + "消費,倉庫容量爲" + queue.size()); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { ProAndCon3 proAndCon = new ProAndCon3(); Producer producer = proAndCon.new Producer(); Consumer consumer = proAndCon.new Consumer(); for (int i = 0; i < 2; i++) { Thread pro = new Thread(producer); pro.start(); Thread con = new Thread(consumer); con.start(); } } } |