【重學Java】多執行緒基礎(三種創建方式,執行緒安全,生產者消費者)
實現多執行緒
簡單了解多執行緒【理解】
是指從軟體或者硬體上實現多個執行緒並發執行的技術。
具有多執行緒能力的電腦因有硬體支援而能夠在同一時間執行多個執行緒,提升性能。
並發和並行【理解】
- 並行:在同一時刻,有多個指令在多個CPU上同時執行。
- 並發:在同一時刻,有多個指令在單個CPU上交替執行。
進程和執行緒【理解】
-
進程:是正在運行的程式
獨立性:進程是一個能獨立運行的基本單位,同時也是系統分配資源和調度的獨立單位
動態性:進程的實質是程式的一次執行過程,進程是動態產生,動態消亡的
並發性:任何進程都可以同其他進程一起並發執行 -
執行緒:是進程中的單個順序控制流,是一條執行路徑,是一個進程中的執行場景/執行單元。
單執行緒:一個進程如果只有一條執行路徑,則稱為單執行緒程式
多執行緒:一個進程如果有多條執行路徑,則稱為多執行緒程式
實現多執行緒方式一:繼承Thread類【應用】
-
方法介紹
方法名 說明 void run() 在執行緒開啟後,此方法將被調用執行 void start() 使此執行緒開始執行,Java虛擬機會調用run方法() -
實現步驟
- 定義一個類MyThread繼承Thread類
- 在MyThread類中重寫run()方法
- 創建MyThread類的對象
- 啟動執行緒
-
程式碼演示
public class MyThread extends Thread { @Override public void run() { for(int i=0; i<100; i++) { System.out.println(i); } } } public class MyThreadDemo { public static void main(String[] args) { MyThread my1 = new MyThread(); MyThread my2 = new MyThread(); // my1.run(); // my2.run(); //void start() 導致此執行緒開始執行; Java虛擬機調用此執行緒的run方法 my1.start(); my2.start(); } }
-
兩個小問題
-
為什麼要重寫run()方法?
因為run()是用來封裝被執行緒執行的程式碼
-
run()方法和start()方法的區別?
run():封裝執行緒執行的程式碼,直接調用,相當於普通方法的調用,並沒有開啟執行緒
start():啟動執行緒;然後由JVM調用此執行緒的run()方法
-
實現多執行緒方式二:實現Runnable介面【應用】
-
Thread構造方法
方法名 說明 Thread(Runnable target) 分配一個新的Thread對象 Thread(Runnable target, String name) 分配一個新的Thread對象 -
實現步驟
- 定義一個類MyRunnable實現Runnable介面
- 在MyRunnable類中重寫run()方法
- 創建MyRunnable類的對象
- 創建Thread類的對象,把MyRunnable對象作為構造方法的參數
- 啟動執行緒
-
程式碼演示
public class MyRunnable implements Runnable { @Override public void run() { for(int i=0; i<100; i++) { System.out.println(Thread.currentThread().getName()+":"+i); } } } public class MyRunnableDemo { public static void main(String[] args) { //創建MyRunnable類的對象 MyRunnable my = new MyRunnable(); //創建Thread類的對象,把MyRunnable對象作為構造方法的參數 //Thread(Runnable target) // Thread t1 = new Thread(my); // Thread t2 = new Thread(my); //Thread(Runnable target, String name) Thread t1 = new Thread(my,"坦克"); Thread t2 = new Thread(my,"飛機"); //啟動執行緒 t1.start(); t2.start(); } }
實現多執行緒方式三: 實現Callable介面【應用】
-
方法介紹
方法名 說明 V call() 計算結果,如果無法計算結果,則拋出一個異常 FutureTask(Callable callable) 創建一個 FutureTask,一旦運行就執行給定的 Callable V get() 如有必要,等待計算完成,然後獲取其結果 -
理解:FutureTask 想一個中間類,可以利用它實現介面,創建Thread執行緒,也可以利用它獲取執行緒返回值
-
實現Callable介面
這種方式的優點:可以獲取到執行緒的執行結果。
這種方式的缺點:效率比較低,在獲取t執行緒執行結果的時候,當前執行緒受阻塞,效率較低。
java.util.concurrent.FutureTask;
JUC包下的,屬於java的並發包,老JDK中沒有這個包。新特性。 -
實現步驟
- 定義一個類MyCallable實現Callable介面
- 在MyCallable類中重寫call()方法
- 創建MyCallable類的對象
- 創建Future的實現類FutureTask對象,把MyCallable對象作為構造方法的參數
- 創建Thread類的對象,把FutureTask對象作為構造方法的參數
- 啟動執行緒
- 再調用get方法,就可以獲取執行緒結束之後的結果。
-
程式碼演示
public class MyCallable implements Callable<String> { @Override public String call() throws Exception { for (int i = 0; i < 100; i++) { System.out.println("跟女孩表白" + i); } //返回值就表示執行緒運行完畢之後的結果 return "答應"; } } public class Demo { public static void main(String[] args) throws ExecutionException, InterruptedException { //執行緒開啟之後需要執行裡面的call方法 MyCallable mc = new MyCallable(); //Thread t1 = new Thread(mc); //可以獲取執行緒執行完畢之後的結果.也可以作為參數傳遞給Thread對象 FutureTask<String> ft = new FutureTask<>(mc); //創建執行緒對象 Thread t1 = new Thread(ft); String s = ft.get(); //開啟執行緒 t1.start(); //String s = ft.get(); System.out.println(s); } }
-
三種實現方式的對比
- 實現Runnable、Callable介面
- 好處: 擴展性強,實現該介面的同時還可以繼承其他的類
- 缺點: 編程相對複雜,不能直接使用Thread類中的方法
- 繼承Thread類
- 好處: 編程比較簡單,可以直接使用Thread類中的方法
- 缺點: 可以擴展性較差,不能再繼承其他的類
- 實現Runnable、Callable介面
設置和獲取執行緒名稱【應用】
-
方法介紹
方法名 說明 void setName(String name) 將此執行緒的名稱更改為等於參數name String getName() 返回此執行緒的名稱 Thread currentThread() 返回對當前正在執行的執行緒對象的引用 -
程式碼演示
public class MyThread extends Thread { public MyThread() {} public MyThread(String name) { super(name); } @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(getName()+":"+i); } } } public class MyThreadDemo { public static void main(String[] args) { MyThread my1 = new MyThread(); MyThread my2 = new MyThread(); //執行緒有默認名字,格式:thread-編號 //void setName(String name):將此執行緒的名稱更改為等於參數 name my1.setName("高鐵"); my2.setName("飛機"); //Thread(String name) MyThread my1 = new MyThread("高鐵"); MyThread my2 = new MyThread("飛機"); //構造方法也可以給執行緒設置名字,但是要寫出執行緒的無參和有參構造函數 /*class MyThread extends Thread{ public MyThread(String name) { super(name); } public MyThread() { @Override public void run(){ //執行的程式碼 } } */ my1.start(); my2.start(); //static Thread currentThread() 返回對當前正在執行的執行緒對象的引用 System.out.println(Thread.currentThread().getName());//MyThread類內沒有getName()方法 } }
執行緒休眠【應用】
-
相關方法
方法名 說明 static void sleep(long millis) 使當前正在執行的執行緒停留(暫停執行)指定的毫秒數 -
程式碼演示
public class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 100; i++) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "---" + i); } } } public class Demo { public static void main(String[] args) throws InterruptedException { /*System.out.println("睡覺前"); Thread.sleep(3000); System.out.println("睡醒了");*/ MyRunnable mr = new MyRunnable(); Thread t1 = new Thread(mr); Thread t2 = new Thread(mr); t1.start(); t2.start(); } }
執行緒優先順序【應用】
-
執行緒調度
-
兩種調度方式
- 分時調度模型:所有執行緒輪流使用 CPU 的使用權,平均分配每個執行緒佔用 CPU 的時間片
- 搶佔式調度模型:優先讓優先順序高的執行緒使用 CPU,如果執行緒的優先順序相同,那麼會隨機選擇一個,優先順序高的執行緒獲取的 CPU 時間片相對多一些
-
Java使用的是搶佔式調度模型
-
隨機性
假如電腦只有一個 CPU,那麼 CPU 在某一個時刻只能執行一條指令,執行緒只有得到CPU時間片,也就是使用權,才可以執行指令。所以說多執行緒程式的執行是有隨機性,因為誰搶到CPU的使用權是不一定的
-
-
優先順序相關方法
方法名 說明 final int getPriority() 返回此執行緒的優先順序 final void setPriority(int newPriority) 更改此執行緒的優先順序執行緒默認優先順序是5;執行緒優先順序的範圍是:1-10 -
注意
執行緒優先順序的範圍是1~10
執行緒默認優先順序是5
優先順序只是提高執行緒搶佔CPU執行權的幾率,並不一定提高實際的搶佔率 -
程式碼演示
public class MyCallable implements Callable<String> { @Override public String call() throws Exception { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + "---" + i); } return "執行緒執行完畢了"; } } public class Demo { public static void main(String[] args) { //優先順序: 1 - 10 默認值:5 MyCallable mc = new MyCallable(); FutureTask<String> ft = new FutureTask<>(mc); Thread t1 = new Thread(ft); t1.setName("飛機"); t1.setPriority(10); //System.out.println(t1.getPriority());//5 t1.start(); MyCallable mc2 = new MyCallable(); FutureTask<String> ft2 = new FutureTask<>(mc2); Thread t2 = new Thread(ft2); t2.setName("坦克"); t2.setPriority(1); //System.out.println(t2.getPriority());//5 t2.start(); } }
守護執行緒【應用】
-
相關方法
方法名 說明 void setDaemon(boolean on) 將此執行緒標記為守護執行緒,當運行的執行緒都是守護執行緒時,Java虛擬機將退出 -
程式碼演示
public class MyThread1 extends Thread { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(getName() + "---" + i); } } } public class MyThread2 extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(getName() + "---" + i); } } } public class Demo { public static void main(String[] args) { MyThread1 t1 = new MyThread1(); MyThread2 t2 = new MyThread2(); t1.setName("女神"); t2.setName("備胎"); //把第二個執行緒設置為守護執行緒 //當普通執行緒執行完之後,那麼守護執行緒也沒有繼續運行下去的必要了. t2.setDaemon(true); t1.start(); t2.start(); } }
執行緒同步
賣票【應用】
-
案例需求
某電影院目前正在上映國產大片,共有100張票,而它有3個窗口賣票,請設計一個程式模擬該電影院賣票
-
實現步驟
-
定義一個類SellTicket實現Runnable介面,裡面定義一個成員變數:private int tickets = 100;
-
在SellTicket類中重寫run()方法實現賣票,程式碼步驟如下
-
判斷票數大於0,就賣票,並告知是哪個窗口賣的
-
賣了票之後,總票數要減1
-
票賣沒了,執行緒停止
-
定義一個測試類SellTicketDemo,裡面有main方法,程式碼步驟如下
-
創建SellTicket類的對象
-
創建三個Thread類的對象,把SellTicket對象作為構造方法的參數,並給出對應的窗口名稱
-
啟動執行緒
-
-
程式碼實現
public class SellTicket implements Runnable { private int tickets = 100; //在SellTicket類中重寫run()方法實現賣票,程式碼步驟如下 @Override public void run() { while (true) { if(ticket <= 0){ //賣完了 break; }else{ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } ticket--; System.out.println(Thread.currentThread().getName() + "在賣票,還剩下" + ticket + "張票"); } } } } public class SellTicketDemo { public static void main(String[] args) { //創建SellTicket類的對象 SellTicket st = new SellTicket(); //創建三個Thread類的對象,把SellTicket對象作為構造方法的參數,並給出對應的窗口名稱 Thread t1 = new Thread(st,"窗口1"); Thread t2 = new Thread(st,"窗口2"); Thread t3 = new Thread(st,"窗口3"); //啟動執行緒 t1.start(); t2.start(); t3.start(); } }
賣票案例的問題【理解】
-
賣票出現了問題
-
相同的票出現了多次
-
出現了負數的票
-
-
問題產生原因
執行緒執行的隨機性導致的,可能在賣票過程中丟失cpu的執行權,導致出現問題
同步程式碼塊解決數據安全問題【應用】
-
安全問題出現的條件
-
是多執行緒環境
-
有共享數據
-
有多條語句操作共享數據
-
-
如何解決多執行緒安全問題呢?
- 基本思想:讓程式沒有安全問題的環境
-
怎麼實現呢?
-
把多條語句操作共享數據的程式碼給鎖起來,讓任意時刻只能有一個執行緒執行即可
-
Java提供了同步程式碼塊的方式來解決
-
-
同步程式碼塊格式:
synchronized(任意對象) { 多條語句操作共享數據的程式碼 }
-
注意同步程式碼塊鎖住的對象必須是多個執行緒共享的對象,否則無效。
synchronized(任意對象):就相當於給程式碼加鎖了,任意對象就可以看成是一把鎖public class test33 { public static void main(String[] args) { MyThread thread1 = new MyThread(); MyThread thread2 = new MyThread(); MyThread thread3 = new MyThread(); thread1.setName("執行緒1"); thread2.setName("執行緒2"); thread3.setName("執行緒3"); thread1.start(); thread2.start(); thread3.start(); } } class MyThread extends Thread { private static int ticket = 100; public Object obj = new Object(); @Override public void run() { while (true) { synchronized (obj) { if (ticket <= 0) break; else { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } ticket--; System.out.println(Thread.currentThread().getName() + "執行了,還剩下" + ticket + "張票"); } } } } }
該案例中創建的三個執行緒,有三個不同的Object成員對象,不共享,相當於沒加鎖,無效。
需要改成public static Object obj = new Object();
讓obj成為共享的成員對象。
同樣,該案例中如果鎖為this及synchronized(this)
則依然無效,因為this所指的對象不是共享的。 -
同步的好處和弊端
-
好處:解決了多執行緒的數據安全問題
-
弊端:當執行緒很多時,因為每個執行緒都會去判斷同步上的鎖,這是很耗費資源的,無形中會降低程式的運行效率
-
-
程式碼演示
public class SellTicket implements Runnable { private int tickets = 100; private Object obj = new Object(); @Override public void run() { while (true) { synchronized (obj) { // 對可能有安全問題的程式碼加鎖,多個執行緒必須使用同一把鎖 //t1進來後,就會把這段程式碼給鎖起來 if (tickets > 0) { try { Thread.sleep(100); //t1休息100毫秒 } catch (InterruptedException e) { e.printStackTrace(); } //窗口1正在出售第100張票 System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "張票"); tickets--; //tickets = 99; } } //t1出來了,這段程式碼的鎖就被釋放了 } } } public class SellTicketDemo { public static void main(String[] args) { SellTicket st = new SellTicket(); Thread t1 = new Thread(st, "窗口1"); Thread t2 = new Thread(st, "窗口2"); Thread t3 = new Thread(st, "窗口3"); t1.start(); t2.start(); t3.start(); } }
同步方法解決數據安全問題【應用】
-
同步方法的格式
同步方法:就是把synchronized關鍵字加到方法上
修飾符 synchronized 返回值類型 方法名(方法參數) { 方法體; }
同步方法的鎖對象是什麼呢?
this
-
靜態同步方法
同步靜態方法:就是把synchronized關鍵字加到靜態方法上
修飾符 static synchronized 返回值類型 方法名(方法參數) { 方法體; }
同步靜態方法的鎖對象是什麼呢?
類名.class
-
程式碼演示
public class MyRunnable implements Runnable { private static int ticketCount = 100; @Override public void run() { while(true){ if("窗口一".equals(Thread.currentThread().getName())){ //同步方法 boolean result = synchronizedMthod(); if(result){ break; } } if("窗口二".equals(Thread.currentThread().getName())){ //同步程式碼塊 synchronized (MyRunnable.class){ if(ticketCount == 0){ break; }else{ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } ticketCount--; System.out.println(Thread.currentThread().getName() + "在賣票,還剩下" + ticketCount + "張票"); } } } } } private static synchronized boolean synchronizedMthod() { if(ticketCount == 0){ return true; }else{ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } ticketCount--; System.out.println(Thread.currentThread().getName() + "在賣票,還剩下" + ticketCount + "張票"); return false; } } }
測試類
public class Demo { public static void main(String[] args) { MyRunnable mr = new MyRunnable(); Thread t1 = new Thread(mr); Thread t2 = new Thread(mr); t1.setName("窗口一"); t2.setName("窗口二"); t1.start(); t2.start(); } }
Lock鎖【應用】
雖然我們可以理解同步程式碼塊和同步方法的鎖對象問題,但是我們並沒有直接看到在哪裡加上了鎖,在哪裡釋放了鎖,為了更清晰的表達如何加鎖和釋放鎖,JDK5以後提供了一個新的鎖對象Lock
Lock是介面不能直接實例化,這裡採用它的實現類ReentrantLock來實例化。默認鎖住當前對象
-
ReentrantLock構造方法
方法名 說明 ReentrantLock() 創建一個ReentrantLock的實例 -
加鎖解鎖方法
方法名 說明 void lock() 獲得鎖 void unlock() 釋放鎖 -
程式碼演示
public class Ticket implements Runnable { //票的數量 private int ticket = 100; private Object obj = new Object(); private ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true) { //synchronized (obj){//多個執行緒必須使用同一把鎖. try { lock.lock(); if (ticket <= 0) { //賣完了 break; } else { Thread.sleep(100); ticket--; System.out.println(Thread.currentThread().getName() + "在賣票,還剩下" + ticket + "張票"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } // } } } } public class Demo { public static void main(String[] args) { Ticket ticket = new Ticket(); Thread t1 = new Thread(ticket); Thread t2 = new Thread(ticket); Thread t3 = new Thread(ticket); t1.setName("窗口一"); t2.setName("窗口二"); t3.setName("窗口三"); t1.start(); t2.start(); t3.start(); } }
死鎖【理解】
-
概述
執行緒死鎖是指由於兩個或者多個執行緒互相持有對方所需要的資源,導致這些執行緒處於等待狀態,無法前往執行
-
什麼情況下會產生死鎖
- 資源有限
- 同步嵌套(鎖的嵌套)
-
程式碼演示
public class Demo { public static void main(String[] args) { Object objA = new Object(); Object objB = new Object(); new Thread(()->{ while(true){ synchronized (objA){ //執行緒一 try { Thread.sleep(100); //休眠為了使執行緒二獲得鎖objB } catch (InterruptedException e) { e.printStackTrace(); } synchronized (objB){ System.out.println("小康同學正在走路"); } } } }).start(); new Thread(()->{ while(true){ synchronized (objB){ //執行緒二 try { Thread.sleep(100); //休眠為了使執行緒一獲得鎖objA } catch (InterruptedException e) { e.printStackTrace(); } synchronized (objA){ System.out.println("小薇同學正在走路"); } } } }).start(); } }
生產者消費者
生產者和消費者模式概述【應用】
-
概述
生產者消費者模式是一個十分經典的多執行緒協作的模式,弄懂生產者消費者問題能夠讓我們對多執行緒編程的理解更加深刻。
所謂生產者消費者問題,實際上主要是包含了兩類執行緒:
一類是生產者執行緒用於生產數據
一類是消費者執行緒用於消費數據
為了解耦生產者和消費者的關係,通常會採用共享的數據區域,就像是一個倉庫
生產者生產數據之後直接放置在共享數據區中,並不需要關心消費者的行為
消費者只需要從共享數據區中去獲取數據,並不需要關心生產者的行為
-
Object類的等待和喚醒方法
方法名 說明 void wait() 導致當前執行緒等待,直到另一個執行緒調用該對象的 notify()方法或 notifyAll()方法 void notify() 喚醒正在等待對象監視器的單個執行緒 void notifyAll() 喚醒正在等待對象監視器的所有執行緒
- wait方法作用:o.wait()讓正在o對象上活動的執行緒t進入等待狀態,並且釋放掉t執行緒之前佔有的o對象的鎖。
- notify方法作用:o.notify()讓正在o對象上等待的執行緒喚醒,只是通知,不會釋放o對象上之前佔有的鎖。
生產者和消費者案例【應用】
-
案例需求
-
桌子類(Desk):定義表示包子數量的變數,定義鎖對象變數,定義標記桌子上有無包子的變數
-
生產者類(Cooker):實現Runnable介面,重寫run()方法,設置執行緒任務
1.判斷是否有包子,決定當前執行緒是否執行
2.如果有包子,就進入等待狀態,如果沒有包子,繼續執行,生產包子
3.生產包子之後,更新桌子上包子狀態,喚醒消費者消費包子
-
消費者類(Foodie):實現Runnable介面,重寫run()方法,設置執行緒任務
1.判斷是否有包子,決定當前執行緒是否執行
2.如果沒有包子,就進入等待狀態,如果有包子,就消費包子
3.消費包子後,更新桌子上包子狀態,喚醒生產者生產包子
-
測試類(Demo):裡面有main方法,main方法中的程式碼步驟如下
創建生產者執行緒和消費者執行緒對象
分別開啟兩個執行緒
-
-
程式碼實現
public class Desk { //定義一個標記 //true 就表示桌子上有漢堡包的,此時允許吃貨執行 //false 就表示桌子上沒有漢堡包的,此時允許廚師執行 public static boolean flag = false; //漢堡包的總數量 public static int count = 10; //鎖對象 public static final Object lock = new Object(); } public class Cooker extends Thread { // 生產者步驟: // 1,判斷桌子上是否有漢堡包 // 如果有就等待,如果沒有才生產。 // 2,把漢堡包放在桌子上。 // 3,叫醒等待的消費者開吃。 @Override public void run() { while(true){ synchronized (Desk.lock){ if(Desk.count == 0){ break; }else{ if(!Desk.flag){ //生產 System.out.println("廚師正在生產漢堡包"); Desk.flag = true; Desk.lock.notifyAll(); }else{ try { Desk.lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } } } public class Foodie extends Thread { @Override public void run() { // 1,判斷桌子上是否有漢堡包。 // 2,如果沒有就等待。 // 3,如果有就開吃 // 4,吃完之後,桌子上的漢堡包就沒有了 // 叫醒等待的生產者繼續生產 // 漢堡包的總數量減一 //套路: //1. while(true)死循環 //2. synchronized 鎖,鎖對象要唯一 //3. 判斷,共享數據是否結束. 結束 //4. 判斷,共享數據是否結束. 沒有結束 while(true){ synchronized (Desk.lock){ if(Desk.count == 0){ break; }else{ if(Desk.flag){ //有 System.out.println("吃貨在吃漢堡包"); Desk.flag = false; Desk.lock.notifyAll(); Desk.count--; }else{ //沒有就等待 //使用什麼對象當做鎖,那麼就必須用這個對象去調用等待和喚醒的方法. try { Desk.lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } } } public class Demo { public static void main(String[] args) { /*消費者步驟: 1,判斷桌子上是否有漢堡包。 2,如果沒有就等待。 3,如果有就開吃 4,吃完之後,桌子上的漢堡包就沒有了 叫醒等待的生產者繼續生產 漢堡包的總數量減一*/ /*生產者步驟: 1,判斷桌子上是否有漢堡包 如果有就等待,如果沒有才生產。 2,把漢堡包放在桌子上。 3,叫醒等待的消費者開吃。*/ Foodie f = new Foodie(); Cooker c = new Cooker(); f.start(); c.start(); } }
生產者和消費者案例優化【應用】
-
需求
- 將Desk類中的變數,採用面向對象的方式封裝起來
- 生產者和消費者類中構造方法接收Desk類對象,之後在run方法中進行使用
- 創建生產者和消費者執行緒對象,構造方法中傳入Desk類對象
- 開啟兩個執行緒
-
程式碼實現
public class Desk { //定義一個標記 //true 就表示桌子上有漢堡包的,此時允許吃貨執行 //false 就表示桌子上沒有漢堡包的,此時允許廚師執行 //public static boolean flag = false; private boolean flag; //漢堡包的總數量 //public static int count = 10; //以後我們在使用這種必須有默認值的變數 // private int count = 10; private int count; //鎖對象 //public static final Object lock = new Object(); private final Object lock = new Object(); public Desk() { this(false,10); // 在空參內部調用帶參,對成員變數進行賦值,之後就可以直接使用成員變數了 } public Desk(boolean flag, int count) { this.flag = flag; this.count = count; } public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } public int getCount() { return count; } public void setCount(int count) { this.count = count; } public Object getLock() { return lock; } @Override public String toString() { return "Desk{" + "flag=" + flag + ", count=" + count + ", lock=" + lock + '}'; } } public class Cooker extends Thread { private Desk desk; public Cooker(Desk desk) { this.desk = desk; } // 生產者步驟: // 1,判斷桌子上是否有漢堡包 // 如果有就等待,如果沒有才生產。 // 2,把漢堡包放在桌子上。 // 3,叫醒等待的消費者開吃。 @Override public void run() { while(true){ synchronized (desk.getLock()){ if(desk.getCount() == 0){ break; }else{ //System.out.println("驗證一下是否執行了"); if(!desk.isFlag()){ //生產 System.out.println("廚師正在生產漢堡包"); desk.setFlag(true); desk.getLock().notifyAll(); }else{ try { desk.getLock().wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } } } public class Foodie extends Thread { private Desk desk; public Foodie(Desk desk) { this.desk = desk; } @Override public void run() { // 1,判斷桌子上是否有漢堡包。 // 2,如果沒有就等待。 // 3,如果有就開吃 // 4,吃完之後,桌子上的漢堡包就沒有了 // 叫醒等待的生產者繼續生產 // 漢堡包的總數量減一 //套路: //1. while(true)死循環 //2. synchronized 鎖,鎖對象要唯一 //3. 判斷,共享數據是否結束. 結束 //4. 判斷,共享數據是否結束. 沒有結束 while(true){ synchronized (desk.getLock()){ if(desk.getCount() == 0){ break; }else{ //System.out.println("驗證一下是否執行了"); if(desk.isFlag()){ //有 System.out.println("吃貨在吃漢堡包"); desk.setFlag(false); desk.getLock().notifyAll(); desk.setCount(desk.getCount() - 1); }else{ //沒有就等待 //使用什麼對象當做鎖,那麼就必須用這個對象去調用等待和喚醒的方法. try { desk.getLock().wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } } } public class Demo { public static void main(String[] args) { /*消費者步驟: 1,判斷桌子上是否有漢堡包。 2,如果沒有就等待。 3,如果有就開吃 4,吃完之後,桌子上的漢堡包就沒有了 叫醒等待的生產者繼續生產 漢堡包的總數量減一*/ /*生產者步驟: 1,判斷桌子上是否有漢堡包 如果有就等待,如果沒有才生產。 2,把漢堡包放在桌子上。 3,叫醒等待的消費者開吃。*/ Desk desk = new Desk(); Foodie f = new Foodie(desk); Cooker c = new Cooker(desk); f.start(); c.start(); } }
阻塞隊列基本使用【理解】
- 阻塞隊列繼承結構
-
常見BlockingQueue:
ArrayBlockingQueue: 底層是數組,有界
LinkedBlockingQueue: 底層是鏈表,無界.但不是真正的無界,最大為int的最大值
-
BlockingQueue的核心方法:
put(anObject): 將參數放入隊列,如果放不進去會阻塞
take(): 取出第一個數據,取不到會阻塞
-
程式碼示例
public class Demo02 { public static void main(String[] args) throws Exception { // 創建阻塞隊列的對象,容量為 1 ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1); // 存儲元素 arrayBlockingQueue.put("漢堡包"); // 取元素 System.out.println(arrayBlockingQueue.take()); System.out.println(arrayBlockingQueue.take()); // 取不到會阻塞 System.out.println("程式結束了"); } }
阻塞隊列實現等待喚醒機制【理解】
-
案例需求
-
生產者類(Cooker):實現Runnable介面,重寫run()方法,設置執行緒任務
1.構造方法中接收一個阻塞隊列對象
2.在run方法中循環向阻塞隊列中添加包子
3.列印添加結果
-
消費者類(Foodie):實現Runnable介面,重寫run()方法,設置執行緒任務
1.構造方法中接收一個阻塞隊列對象
2.在run方法中循環獲取阻塞隊列中的包子
3.列印獲取結果
-
測試類(Demo):裡面有main方法,main方法中的程式碼步驟如下
創建阻塞隊列對象
創建生產者執行緒和消費者執行緒對象,構造方法中傳入阻塞隊列對象
分別開啟兩個執行緒
-
-
程式碼實現
public class Cooker extends Thread { private ArrayBlockingQueue<String> bd; public Cooker(ArrayBlockingQueue<String> bd) { this.bd = bd; } // 生產者步驟: // 1,判斷桌子上是否有漢堡包 // 如果有就等待,如果沒有才生產。 // 2,把漢堡包放在桌子上。 // 3,叫醒等待的消費者開吃。 @Override public void run() { while (true) { try { bd.put("漢堡包"); System.out.println("廚師放入一個漢堡包"); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class Foodie extends Thread { private ArrayBlockingQueue<String> bd; public Foodie(ArrayBlockingQueue<String> bd) { this.bd = bd; } @Override public void run() { // 1,判斷桌子上是否有漢堡包。 // 2,如果沒有就等待。 // 3,如果有就開吃 // 4,吃完之後,桌子上的漢堡包就沒有了 // 叫醒等待的生產者繼續生產 // 漢堡包的總數量減一 //套路: //1. while(true)死循環 //2. synchronized 鎖,鎖對象要唯一 //3. 判斷,共享數據是否結束. 結束 //4. 判斷,共享數據是否結束. 沒有結束 while (true) { try { String take = bd.take(); System.out.println("吃貨將" + take + "拿出來吃了"); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class Demo { public static void main(String[] args) { ArrayBlockingQueue<String> bd = new ArrayBlockingQueue<>(1); Foodie f = new Foodie(bd); Cooker c = new Cooker(bd); f.start(); c.start(); } }
注意:最終的列印結果可能回出現同樣的語句重複輸出,這不符合預期。
這是因為ArrayBlockingQueue的底層源程式碼是用ReentrantLock鎖了,實現執行緒同步,但是System.out語句是我們自己寫的,列印語句沒有實現執行緒同步,所以可能列印的時候出現問題,這只是個小問題。