(三)juc高級特性——虛假喚醒 / Condition / 按序交替 / ReadWriteLock / 線程八鎖
8. 生產者消費者案例-虛假喚醒
參考下面生產者消費者案例:
/* * 生產者和消費者案例 */ public class TestProductorAndConsumer { public static void main(String[] args) { Clerk clerk = new Clerk(); Productor pro = new Productor(clerk); Consumer cus = new Consumer(clerk);new Thread(pro, "生產者 A").start(); new Thread(cus, "消費者 B").start(); new Thread(pro, "生產者 C").start(); new Thread(cus, "消費者 D").start(); } } //店員 class Clerk{ private int product = 0; //進貨 public synchronized void get(){//循環次數:0 if(product >= 1){ System.out.println("產品已滿!"); try { this.wait(); } catch (InterruptedException e) { } } System.out.println(Thread.currentThread().getName() + " : " + ++product);//為避免線程不能正常關閉(一直處在wait狀態未喚醒),notifyAll()方法不要放在if...else...中 this.notifyAll(); } //賣貨 public synchronized void sale(){//product = 0; 循環次數:0 if(product <= 0){ System.out.println("缺貨!"); try { this.wait(); } catch (InterruptedException e) { } } System.out.println(Thread.currentThread().getName() + " : " + --product); this.notifyAll(); } } //生產者 class Productor implements Runnable{ private Clerk clerk; public Productor(Clerk clerk) { this.clerk = clerk; } @Override public void run() { for (int i = 0; i < 20; i++) { try { Thread.sleep(200); } catch (InterruptedException e) { } clerk.get(); } } } //消費者 class Consumer implements Runnable{ private Clerk clerk; public Consumer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { for (int i = 0; i < 20; i++) { clerk.sale(); } } }
當多個生產者、消費者同時響應資源時,程序輸出如下(商品數出現負數):
原因如下,即產生了虛假喚醒:
解決方法在jdk的wait()方法裏已經聲明,即需要把wait()方法放在循環裏(生產者方法也同下)
//賣貨 public synchronized void sale(){//product = 0; 循環次數:0 while(product <= 0){//為了避免虛假喚醒問題,應該總是使用在循環中 System.out.println("缺貨!"); try { this.wait();//中斷和虛假喚醒都可能發生,所以需要將該方法放在while循環裏 } catch (InterruptedException e) { } } System.out.println(Thread.currentThread().getName() + " : " + --product); this.notifyAll(); }
9. Condition 線程通信
Condition 接口描述了可能會與鎖有關聯的條件變量。這些變量在用法上與使用 Object.wait 訪問的隱式監視器類似,但提供了更強大的功能。需要特別指出的是,單個 Lock 可能與多個 Condition 對象關聯。為了避免兼容性問題,Condition 方法的名稱與對應的 Object 版本中的不同。
在 Condition 對象中,與 wait、notify 和 notifyAll 方法對應的分別是await、signal 和 signalAll。
Condition 實例實質上被綁定到一個鎖上。要為特定 Lock 實例獲得Condition 實例,請使用其 newCondition() 方法。
使用lock和Condition對生產者消費者案例進行改造
class Clerk { private int product = 0; private Lock lock = new ReentrantLock(); //使用其 newCondition() 方法,為特定 Lock 實例獲得Condition 實例 private Condition condition = lock.newCondition(); // 進貨 public void get() { //開啟lock lock.lock(); try { while (product >= 1) { // 為了避免虛假喚醒,應該總是使用在循環中。 System.out.println("產品已滿!"); try { //this.wait(); condition.await(); } catch (InterruptedException e) { } } System.out.println(Thread.currentThread().getName() + " : " + ++product); //this.notifyAll(); condition.signalAll(); } finally {//執行關lock lock.unlock(); } } // 賣貨 public void sale() { lock.lock(); try { while (product <= 0) { System.out.println("缺貨!"); try { condition.await(); } catch (InterruptedException e) { } } System.out.println(Thread.currentThread().getName() + " : " + --product); condition.signalAll(); } finally { lock.unlock(); } } }
10. 線程按序交替
/* * 編寫一個程序,開啟 3 個線程,這三個線程的 ID 分別為 A、B、C,每個線程將自己的 ID 在屏幕上打印 n 遍,要求輸出的結果必須按順序顯示。 * 如:ABBCCCABBCCCABBCCC…… 依次遞歸 * 這裏按照ABBCCC...的順序打印20次 */ public class TestABCAlternate { public static void main(String[] args) { AlternateDemo ad = new AlternateDemo(); new Thread(new Runnable() { @Override public void run() { for (int i = 1; i <= 20; i++) { ad.loopA(i); } } }, "A").start(); new Thread(new Runnable() { @Override public void run() { for (int i = 1; i <= 20; i++) { ad.loopB(i); } } }, "B").start(); new Thread(new Runnable() { @Override public void run() { for (int i = 1; i <= 20; i++) { ad.loopC(i); System.out.println("-----------------------------------"); } } }, "C").start(); } } class AlternateDemo{ private int number = 1; //當前正在執行線程的標記 private Lock lock = new ReentrantLock(); private Condition condition1 = lock.newCondition(); private Condition condition2 = lock.newCondition(); private Condition condition3 = lock.newCondition(); /** * @param totalLoop : 循環第幾輪 */ public void loopA(int totalLoop){ lock.lock(); try { //1. 判斷 if(number != 1){ condition1.await(); } //2. 打印 for (int i = 1; i <= 1; i++) { System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop); } //3. 喚醒 number = 2; condition2.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void loopB(int totalLoop){ lock.lock(); try { //1. 判斷 if(number != 2){ condition2.await(); } //2. 打印 for (int i = 1; i <= 2; i++) { System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop); } //3. 喚醒 number = 3; condition3.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void loopC(int totalLoop){ lock.lock(); try { //1. 判斷 if(number != 3){ condition3.await(); } //2. 打印 for (int i = 1; i <= 3; i++) { System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop); } //3. 喚醒 number = 1; condition1.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
實現如下:
11. ReadWriteLock 讀寫鎖
ReadWriteLock 維護了一對相關的鎖,一個用於只讀操作,另一個用於寫入操作。只要沒有 writer,讀取鎖可以由多個 reader 線程同時保持。寫入鎖是獨占的。
ReadWriteLock 讀取操作通常不會改變共享資源,但執行寫入操作時,必須獨占方式來獲取鎖。對於讀取操作占多數的數據結構。 ReadWriteLock 能提供比獨占鎖更高的並發性。而對於只讀的數據結構,其中包含的不變性可以完全不需要考慮加鎖操作。
/* * ReadWriteLock : 讀寫鎖 * * 寫寫/讀寫 需要“互斥” * 讀讀 不需要互斥 * */ public class TestReadWriteLock { public static void main(String[] args) { ReadWriteLockDemo rw = new ReadWriteLockDemo(); //1個線程對數據進行寫操作 new Thread(new Runnable() { @Override public void run() { rw.set((int)(Math.random() * 101)); } }, "Write:").start(); //100個線程對數據進行讀操作 for (int i = 0; i < 100; i++) { new Thread(new Runnable() { @Override public void run() { rw.get(); } }).start(); } } } class ReadWriteLockDemo{ private int number = 0; //創建ReadWriteLock讀寫鎖對象 private ReadWriteLock lock = new ReentrantReadWriteLock(); //讀 public void get(){ lock.readLock().lock(); //上鎖 try{ System.out.println(Thread.currentThread().getName() + " : " + number); }finally{ lock.readLock().unlock(); //釋放鎖 } } //寫 public void set(int number){ lock.writeLock().lock(); try{ System.out.println(Thread.currentThread().getName()); this.number = number; }finally{ lock.writeLock().unlock(); } } }
12. 線程八鎖
/* * 題目:判斷打印的 "one" or "two" ? * * 1. 兩個普通同步方法,兩個線程,標準打印, 打印? //one two * 2. 新增 Thread.sleep() 給 getOne() ,打印? //one two * 3. 新增普通方法 getThree() , 打印? //three one two * 4. 兩個普通同步方法,兩個 Number 對象,打印? //two one * 5. 修改 getOne() 為靜態同步方法,打印? //two one * 6. 修改兩個方法均為靜態同步方法,一個 Number 對象? //one two * 7. 一個靜態同步方法,一個非靜態同步方法,兩個 Number 對象? //two one * 8. 兩個靜態同步方法,兩個 Number 對象? //one two * * 線程八鎖的關鍵: * ①非靜態方法的鎖默認為 this, 靜態方法的鎖為 對應的 Class 實例 * ②某一個時刻內,只能有一個線程持有鎖,無論幾個方法。 */ public class TestThread8Monitor { public static void main(String[] args) { Number number = new Number(); Number number2 = new Number(); new Thread(new Runnable() { @Override public void run() { number.getOne(); } }).start(); new Thread(new Runnable() { @Override public void run() { // number.getTwo(); number2.getTwo(); } }).start(); /*new Thread(new Runnable() { @Override public void run() { number.getThree(); } }).start();*/ } } class Number{ public static synchronized void getOne(){//Number.class try { Thread.sleep(3000); } catch (InterruptedException e) { } System.out.println("one"); } public synchronized void getTwo(){//this System.out.println("two"); } public void getThree(){ System.out.println("three"); } }
總結:
①一個對象裏面如果有多個synchronized方法,某一個時刻內,只要一個線程去調用其中的一個synchronized方法了,其他的線程都只能等待,換句話說,某一時刻內,只能有唯一一個線程去訪問這些synchronized方法。
②鎖的是當前對象this,被鎖定後,其他線程都不能進入到當前對象的其他的synchronized方法。
③加個普通方法後發現和同步鎖無關。
④換成靜態同步方法後,情況又變化
⑤所有的非靜態同步方法用的都是同一把鎖 -- 實例對象本身,也就是說如果一個實例對象的非靜態同步方法獲取鎖後,該實例對象的其他非靜態同步方法必須等待獲取鎖的方法釋放鎖後才能獲取鎖,可是別的實例對象的非靜態同步方法因為跟該實例對象的非靜態同步方法用的是不同的鎖,所以毋須等待該實例對象已經取鎖的非靜態同步方法釋放鎖就可以獲取他們自己的鎖。
⑥所有的靜態同步方法用的也是同一把鎖 -- 類對象本身,這兩把鎖是兩個不同的對象,所以靜態同步方法與非靜態同步方法之間不會有競爭條件。但是一旦一個靜態同步方法獲取鎖後,其他的靜態同步方法都必須等待該方法釋放鎖後才能獲取鎖,而不管是同一個實例對象的靜態同步方法之間,還是不同的實例對象的靜態同步方法之間,只要它們是同一個實例對象。
(三)juc高級特性——虛假喚醒 / Condition / 按序交替 / ReadWriteLock / 線程八鎖