一.Condition 概述以及API
1.概述
在java1.5之前,線程之間的通信主要通過notify和wait。而Condition支持多路等待,就是定義多個Condition,每個Condition控制一個支路,典型問題生產者和消費者問題現在可以通過這個接口來進行優化。
2.API
1.await(),當前線程等待,同時釋放當前鎖,可以用signal()時或者signalAll()方法或者中斷跳出等待,線程會重新獲得鎖並繼續執行,和之前的Object的wait很像(await這個方法必須在lock和unlock之間調用)。
2.awaitUninterruptibly(),與await()方法基本相同,但這個方法不會在等待過程中響應中斷,也就是中斷不會跳出等待,繼續睡。
3.singal(),用於喚醒一個在等待中的線程,和notify類型
二.使用實例-生產者消費者模型
1.倉庫類Store的設計
這個倉庫類包含生產方法和消費方法,在里面設置鎖,以及條件來進行線程的通信。而生產者和消費者實例會持有這個倉庫類的實例,他們的生產消費方法是通過調用這個倉庫的相應的方法來實現的,來看看倉庫類把。
<1>倉庫具體設計
//設置鎖以及條件,並用構造函數初始化
private final static int MAX_NUM = 100; private Lock lock; private Condition p; private Condition c; public Store() { lock =new ReentrantLock(); c = lock.newCondition(); p = lock.newCondition(); }
//用一個鏈表作為倉庫,用LinkedList是因為業務主要是增刪,所以用它比較快 private LinkedList<Integer> list = new LinkedList<Integer>(); // get/set方法 public LinkedList<Integer> getList() { return list; } public void setList(LinkedList<Integer> list) { this.list = list; } public int getMAX_SIZE() { return MAX_NUM; }
<2>生產方法設計
//具體的生產方法 public void produce(int num) { lock.lock();//條件要放在lock.lock和lock.unlock之間 try{ // 倉庫容量不足 while (list.size() + num > MAX_NUM) { System.out.println("【要生產的產品數量】:" + num + " 【庫存量】:" + list.size() + "暫時不能執行生產任務!"); try { p.await();//當庫存量不足時,不能生產,所以設置生產者阻塞,用條件p。 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } // 倉庫容量足夠,生產num個產品 for (int i = 1; i <= num; ++i) { list.add(new Random().nextInt(100)); } System.out.println("【已經生產產品數】:" + num + " 【現倉儲量為】:" + list.size()); c.signalAll();//生產完之后喚醒消費者條件,這個時候c.await的線程會喚醒 TimeUnit.SECONDS.sleep(1); }catch(Exception e){ e.printStackTrace(); }finally{ lock.unlock(); } }
<3>消費方法設計
//消費方法
public void Consume(int num) { lock.lock(); // 倉庫容量不足 try{ while (list.size() < num) { System.out.println("【要消費的產品數量】:" + num + " 【庫存量】:" + list.size() + "/t暫時不能執行消費任務!"); try { c.await();//等到生產者那邊c.singalAll時在這里阻塞的線程會喚醒,繼續往下執行 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } // 倉庫容量足夠,消費num個產品 for (int i = 1; i <= num; ++i) { list.remove(); } System.out.println("【已經消費產品數】:" + num + " 【現倉儲量為】:" + list.size()); p.signalAll();//消費者消費完之后,會喚醒生產者繼續生產. TimeUnit.SECONDS.sleep(1); }catch(Exception e){ e.printStackTrace(); }finally{ lock.unlock(); } }
2.生產者消費者的設計
這部分很簡單,都是實現Runnable接口,通過構造函數初始化Store的實例,run方法調用Store的相應的生產消費方法,並傳入一個生產消費的數量,就基本穩了。
3.測試類
實例化Store,傳給生產者消費者,啟動線程把任務丟進去,跑起來就行啦。
三.Condition接口的原理
1.等待隊列
Condition持有多個FIFO的等待隊列,當前線程調用await方法時,那么這當前線程就會釋放鎖,進入這個等待隊列的尾部進入等待狀態。Condition擁有這個隊列的首節點(firstWaiter)和尾節點(lastWaiter).其實一個Lock(同步器)有一個同步隊列(准備去獲得鎖)以及多個等待隊列(本來已經有鎖,但是中途用了await方法),還記得之前討論的監視器么,和這個很像,監視器只有一個同步隊列和一個等待隊列。其實工作原理一樣的,沒有獲取鎖的線程在同步隊列等待鎖,獲得鎖后進入特殊的房間(資源),調用await之類的等待方法就把同步隊列的頭結點移到等待隊列的尾節點。當等待隊列里面的節點被喚醒后,進入同步隊列拿鎖進入同步狀態。
2.通知
調用Condition的singal方法,它將會先將在等待隊列中等待時間最長的節點(首節點)移到同步隊列(同步器的enq(Node node)方法,等待隊列中的頭節點線程安全地移動到同步隊列),然后喚醒它。進入鎖的競爭狀態(acquireQueued()方法)。不過注意的是當前線程或得鎖才會調用這個方法使等待隊列里面的首節點進入同步隊列。比如說:同步隊列里面有p1,p2,p3三個消費者線程.等待隊列里面有c1,c2,c3三個消費者線程。p1為頭結點,p1獲得鎖去生產,然后調用在singal方法時,p1必定是要先有鎖的,然后c1進入同步隊列,p2獲得鎖進行生產,然后就到c1進行消費。