目錄
- 一、多執行緒概述
- 1.1、行程和執行緒的概念
- 1.1.1、行程
- 1.1.2、執行緒
- 1.2、執行緒創建的方法
- 1.2.1、繼承Thread類
- 1.2.2、重寫Runnable介面
- 1.2.3、重寫callable介面
- 1.3、執行緒的狀態機
- 1.3.1、執行緒的狀態
- 1.3.2、執行緒的狀態機
- 1.1、行程和執行緒的概念
- 二、多執行緒實作
- 2.1、繼承Thread類
- 2.1.1、Thread的基本用法
- 2.1.2、多執行緒下載網圖
- 2.2、實作Runnable介面
- 2.2.1、Runnable基本用法
- 2.2.2、模擬龜兔賽跑
- 2.3、實作Callable介面
- 2.4、并發問題
- 2.5、補充Lambda運算式
- 2.5.1、Lambda的簡介
- 2.5.2、Lambda運算式用法引入
- 2.5.3、Lambda深入理解
- 2.1、繼承Thread類
- 三、執行緒狀態轉換
- 3.1、執行緒停止
- 3.2、執行緒休眠
- 3.3、執行緒禮讓
- 3.4、執行緒加入
- 3.5、執行緒狀態檢測
- 3.6、執行緒優先級
- 3.7、守護執行緒
- 四、執行緒同步
- 4.1、執行緒同步的概念
- 4.2、Java執行緒同步方法
- 4.2.1、同步方法或者同步塊
- ①、synchronized修飾方法
- ②、synchronized同步塊
- 4.2.2、Lock鎖
- ①、一般寫法
- ②、案例說明
- 4.2.1、同步方法或者同步塊
- 五、執行緒通信
- 2.1、執行緒通信的概念
- 2.1.1、為什么需要執行緒通信
- 2.1.2、執行緒的通信的方式
- 2.2、管程法
- 2.3、信號量法
- 2.4、執行緒池法
- 2.1、執行緒通信的概念
一、多執行緒概述
1.1、行程和執行緒的概念
1.1.1、行程
- 行程是執行程式的一次執行程序,是一個動態的程序,是一個活動的物體,是系統資源分配的單位
- 一個應用程式的運行就可以被看做是一個行程
1.1.2、執行緒
- 執行緒,是運行中的實際的任務執行者,一般的,一個行程中包含了多個可以同時運行的執行緒
- 執行緒就是獨立的執行路徑,是cpu調度和執行的單位,是序執行流中最小執行單位,是行程中實際運行單位
- 多執行緒的調度由CPU決定,無法人為干預
- 在java中,一定存在著兩個執行緒,一個是main方法,即主執行緒,一個是gc執行緒,用于jvm的垃圾回收
1.2、執行緒創建的方法
1.2.1、繼承Thread類
1.2.2、重寫Runnable介面
1.2.3、重寫callable介面
1.3、執行緒的狀態機
1.3.1、執行緒的狀態
java中執行緒共有五種狀態
- 新建狀態
- 就緒狀態
- 阻塞狀態
- 運行狀態
- 死亡狀態
1.3.2、執行緒的狀態機
-
通用執行緒狀態機
-
對應java中的實作手段
二、多執行緒實作
2.1、繼承Thread類
用法上述內容講到,直接上代碼
2.1.1、Thread的基本用法
-
代碼實作
package com.kuang.class1; /** * 1、繼承Thread類來實作多執行緒的demo */ public class TestThreadDemo extends Thread { // 繼承Thread類 @Override public void run() { // 重寫run方法 for (int i = 0; i < 20; i++) { System.out.println("子執行緒--" + i); } } public static void main(String[] args) { // 創建子執行緒物件,并呼叫start方法開啟子執行緒 new TestThreadDemo().start(); // 這是主執行緒代碼 for (int i = 0; i < 20; i++) { System.out.println("主執行緒--" + i); } } } -
效果展示
可以看見子執行緒和主執行緒交替執行,但同一時間只能由一個執行,即宏觀并發,微觀交替

2.1.2、多執行緒下載網圖
- 需要用到common-io包
- 實作
WebDownLoader類,里面實作一個DownLoader()方法,主要用到FileUtils.copyURLToFile()方法,將URL資源轉為圖片,即實作下載 - 主類繼承Thread類,
run()方法呼叫DownLoader()來實作多執行緒下載 - main方法創建物件,呼叫
start()方法開啟執行緒
-
代碼實作
package com.kuang.class1; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.net.URL; /** * 使用多執行緒下載網路圖片的demo * 需要用到common-io.jar */ public class TestThreadToDownload extends Thread { private final String url; private final String name; public TestThreadToDownload(String url, String name) { this.url = url; this.name = name; } @Override public void run() { // 呼叫下載器實作多執行緒下載 WebDownLoader downLoader = new WebDownLoader(); downLoader.DownLoader(url,name); System.out.println("下載了檔案名為 :" + name); } public static void main(String[] args) { // 創建多執行緒物件 TestThreadToDownload t1 = new TestThreadToDownload("https://img1.baidu.com/it/u=3726701668,178087506&fm=253&fmt=auto&app=138&f=JPEG?w=353&h=499","./src/com/kuang/class1/image/1.jpg"); TestThreadToDownload t2 = new TestThreadToDownload("https://img1.baidu.com/it/u=3726701668,178087506&fm=253&fmt=auto&app=138&f=JPEG?w=353&h=450","./src/com/kuang/class1/image/2.jpg"); TestThreadToDownload t3 = new TestThreadToDownload("https://img1.baidu.com/it/u=3726701668,178087506&fm=253&fmt=auto&app=138&f=JPEG?w=353&h=500","./src/com/kuang/class1/image/3.jpg"); // 開啟執行緒 t1.start(); t2.start(); t3.start(); } } /** * 下載器實作 */ class WebDownLoader { public void DownLoader(String url, String name) { try{ // 主要用到該方法,將傳入的URL下載成本地檔案 FileUtils.copyURLToFile(new URL(url),new File(name)); } catch(IOException e){ e.printStackTrace(); System.out.println("IO例外,DownLoader下載出問題"); } } } -
效果展示
輸出了下載的檔案名
對應路徑存在下載的圖片檔案,則下載成功
2.2、實作Runnable介面
2.2.1、Runnable基本用法
- 實作
Runnable介面并且必須重寫其中的run()方法 - Runnable創建多執行緒使用靜態代理模式,首先必須創建實作了該介面的類的物件
- 然后將物件傳入Thread的構造器,創建一個Thread物件
- 最后通過Thread類的物件呼叫
start()方法開啟執行緒
Runnable介面實作多執行緒比較特殊,但最終都是通過Thread類來運行的;推薦使用Runnable介面而非Thread類,因為java只能單繼承,但可以實作多個介面
-
代碼實作
package com.kuang.class1; /** * 使用Runnable介面來實作多執行緒 */ public class TestRunnableDemo implements Runnable { @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println("子執行緒--" +i); } } public static void main(String[] args) { // 靜態代理模式 // 先創建實作了Runnable介面的物件 // 再將這個物件傳入Thread的構造器,創建一個Thread類物件 // 最后呼叫start方法開啟 new Thread(new TestRunnableDemo()).start(); // 主執行緒代碼 for (int i = 0; i < 20; i++) { System.out.println("主執行緒--" +i); } } } -
效果展示
主執行緒和子執行緒交替運行
2.2.2、模擬龜兔賽跑
- 設定一個靜態類變數
winner模擬勝出者 - 設定一個裁判方法,如果已經存在勝出者,則執行緒停止;如果步數大于等于100,將該執行緒名賦給
winner,否則執行緒繼續回圈
-
代碼實作
package com.kuang.class1; /** * 多執行緒實作龜兔賽跑demo, */ public class TestRaceDemo implements Runnable { public static String winner; // 獲勝者變數 @Override public void run() { for (int i = 1; i <= 100; i++) { boolean flag = gameOver(i); if (flag) { break; } System.out.println(Thread.currentThread().getName() + "--->跑了" + i + "步"); // 兔子在中間開始休息 if (i % 45 == 0 && Thread.currentThread().getName().equals("兔子")) { try { Thread.sleep(300); } catch (InterruptedException e) { throw new RuntimeException(e); } } // 模擬烏龜跑的慢 if(Thread.currentThread().getName().equals("烏龜")) { try { Thread.sleep(5); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } // 裁判方法 public boolean gameOver(int steps) { if(winner != null) { return true; } if (steps >= 100) { winner = Thread.currentThread().getName(); System.out.println("勝出者是" + winner); return true; } return false; } public static void main(String[] args) { TestRaceDemo raceDemo = new TestRaceDemo(); new Thread(raceDemo,"兔子").start(); new Thread(raceDemo,"烏龜").start(); } } -
效果展示
兔子中途休息
烏龜奮起直追,最后獲勝
2.3、實作Callable介面
- 實作
Callable介面,并且重寫call()方法,該方法具有回傳值,回傳值型別同implements定義Callable<[回傳值型別]>時的型別
主方法開啟多執行緒由兩種方法:
方法1:分為創建服務、提交執行、獲取結果、關閉服務四個固定步驟
- 第0步,先創建實作了Callable介面的類的物件
- 通過
ExecutorService service = Executors.newFixedThreadPool(<執行緒池容量>)創建一個自定義執行緒數的執行緒池,并開啟服務 Future<[call方法回傳型別]> r1 = service.submit(<第0步物件>)將執行緒提交執行r1.get()獲取執行緒執行完畢后的回傳值service.shutdownNow()關閉服務
方法2:通過Thread代理模式開啟執行緒
- 先創建實作了Callable介面的類的物件
FutureTask<String> task = new FutureTask<>(call)創建FutureTask物件new Thread(task,"小明").start()開啟執行緒
FutureTask泛型間接實作了Runnable介面,相當于轉換了一下
-
代碼實作
package com.kuang.class1; import java.util.concurrent.*; /** * 實作callable介面來創建多執行緒 */ public class TestCallableDemo implements Callable<String> { boolean flag = false; @Override // 此處回傳值與上方泛型一致 // 該方法相當于run方法,但帶有回傳值 public String call() { if(!flag) { for (int i = 1; i <= 10; i++) { if (i == 10) { flag = true; } System.out.println(Thread.currentThread().getName() + "正在干飯-->" + i); } } return Thread.currentThread().getName() + "干完飯啦!"; } public static void main(String[] args) throws ExecutionException, InterruptedException { // 創建目標物件 TestCallableDemo call = new TestCallableDemo(); // 創建服務,這里創建了一個容納三個執行緒的執行緒池 ExecutorService service = Executors.newFixedThreadPool(3); // 提交執行 Future<String> r1 = service.submit(call); Future<String> r2 = service.submit(call); Future<String> r3 = service.submit(call); System.out.println(r1.get()); System.out.println(r2.get()); System.out.println(r3.get()); service.shutdownNow(); // 第二種實作多執行緒的方法 /*FutureTask<String> task = new FutureTask<>(call); new Thread(task,"小明").start();*/ } } -
效果展示
2.4、并發問題
在不控制并發的前提下實作買票系統,多執行緒進行買票,觀察票的情況
- 創建票數的變數
- 實作
Runnable介面,在run()方法中模擬買票,每執行一次(買一張票),票就減少一張 - 主執行緒創建多個子執行緒來模擬并發
-
代碼實作
package com.kuang.class1; /** * 多執行緒買票,不控制并發 */ public class TestBuyTicketDemo implements Runnable { private static int ticketNum = 10; // 票的數量 @Override public void run() { while (ticketNum > 0) { // 模擬延時 try { Thread.sleep(50); } catch (InterruptedException e) { throw new RuntimeException(e); } // 買票程式 System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNum-- + "票"); } } public static void main(String[] args) { TestBuyTicketDemo t = new TestBuyTicketDemo(); // 多執行緒買票 new Thread(t,"小明").start(); new Thread(t,"張三").start(); new Thread(t,"黃牛黨").start(); } } -
效果展示
多次執行可能發現,不同人可能買到同一張票

-
**問題發現 **
多個人買到了同一張票,導致資料不唯一
2.5、補充Lambda運算式
2.5.1、Lambda的簡介
- 為什么要用?
- 避免匿名內部類定義過多,可以讓代碼簡潔緊湊,留下核心的邏輯
- Lambda運算式能夠讓程式員的編程更加高效
- 何時使用?
- 前提:必須存在一個函式式介面(即介面中只定義了一個抽象方法),這樣我們就可以使用Lambda運算式來創建該介面的物件
- 語法格式
(parameters) -> expression[運算式][(parameters) -> statements[陳述句][(parameters) ->{ statements; }
2.5.2、Lambda運算式用法引入
將介面的實作類、靜態內部類、區域內部類、匿名內部類、Lambda式進行對比,可以發現Lambda運算式例外簡潔
-
代碼實作
package com.kuang.class2; /** * lambda運算式的測驗類 * 該運算式使用的前提是,有有函式型介面 */ // 定義函式式介面:只存在一個抽象方法的介面 interface ILike { void lambda(); } public class TestLambda1 { // 靜態內部類實作介面 static class Like2 implements ILike { @Override public void lambda() { System.out.println("靜態內部類的lambda方法"); } } public static void main(String[] args) { // 定義區域內部類 class Like3 implements ILike { @Override public void lambda() { System.out.println("區域內部類的lambda方法"); } } // 定義匿名內部類 ILike like4 = new ILike() { @Override public void lambda() { System.out.println("匿名內部類的lambda方法"); } }; // lambda運算式寫法 ILike like5 = () -> System.out.println("真正的lambda運算式"); new Like1().lambda(); new Like2().lambda(); new Like3().lambda(); like4.lambda(); like5.lambda(); } } // 定義介面實作類,傳統用法 class Like1 implements ILike { @Override public void lambda() { System.out.println("介面實作類的lambda方法"); } } -
效果展示
定義介面物件
like5,使用Lambda運算式,只關心核心邏輯的寫法;使用時,直接通過該物件呼叫介面中的方法即可

2.5.3、Lambda深入理解
上面式函式式介面中的抽象方法無引數、無回傳值,這里定義有引數及回傳值的抽象方法;
詳細請看Java Lambda 運算式 | 菜鳥教程 (runoob.com)
-
代碼實作
package com.kuang.class2; /** * Lambda運算式測驗2 * 介面方法有引數及回傳值 */ public class TestLambda2 { public static void main(String[] args) { // 這里使用lambda運算式,實作了介面中的抽象方法,將兩數運算具體化,這里定義加法運算 MathOperation add = Integer::sum; // 減法運算 MathOperation sub = (int a, int b) -> a - b; // 乘法運算 MathOperation multi = (a,b) -> a * b; System.out.println(add.operation(1,2)); System.out.println(sub.operation(3,5)); System.out.println(multi.operation(8,7)); } // 定義函式式介面, interface MathOperation { // 將兩數運算抽象出來 int operation(int a, int b); } } -
效果展示
三、執行緒狀態轉換
執行緒的五大狀態在概述中已經講到,這里主要講解讓執行緒狀態轉換的方法
3.1、執行緒停止
- 不推薦使用jdk中的內置方法,如
stop(),因為這是使執行緒強制停止的方法,可能會導致多執行緒的一些問題 - 推薦自定義標志位,并加上邏輯判斷,讓執行緒在某一條件下自動停止運行
-
代碼實作
package com.kuang.class3; /** * 執行緒狀態之:執行緒停止 * 建議使用標志位加條件判斷,讓執行緒自行停止 * 不建議使用jdk的內置方法,如stop() */ public class TestThreadStop implements Runnable { @Override public void run() { int i = 0; while (i != 15) { // 設定標志位,讓執行緒主動停止 System.out.println("子執行緒---run" + i++); } System.out.println("子執行緒停止!!!"); } public static void main(String[] args) { new Thread(new TestThreadStop()).start(); for (int i = 0; i < 20; i++) { System.out.println("main---run" + i); } } } -
效果展示
當子執行緒回圈變數
i為15時,子執行緒自動跳出回圈,結束運行
3.2、執行緒休眠
- 主要用到
Sleep()方法讓執行緒從運行狀態轉變為阻塞狀態 - 每一個物件都有鎖,
Sleep()不會釋放鎖
-
代碼實作
實作回圈顯示當前時間的案例,一秒鐘回圈一次
package com.kuang.class3; import java.text.SimpleDateFormat; import java.util.Date; /** * 執行緒休眠Sleep */ public class TestThreadSleep { public static void main(String[] args) { Date date = new Date(System.currentTimeMillis()); // 獲得當前時間 while(true) { try { Thread.sleep(1000); // 一秒休眠一次 System.out.println(new SimpleDateFormat("HH:mm:ss").format(date)); date = new Date(System.currentTimeMillis()); // 更新時間 } catch (InterruptedException e) { e.printStackTrace(); } } } } -
效果展示
3.3、執行緒禮讓
主要用到yield()方法,在多執行緒情況下,讓正處在運行態的執行緒轉換為就緒態,重新競爭CPU調度(但是禮讓不一定成功,再次調度還是看CPU心情)
-
代碼實作
package com.kuang.class3; /** * 執行緒禮讓的demo */ public class TestYield implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { if (i % 2 == 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } Thread.yield(); // 執行緒禮讓 } System.out.println(Thread.currentThread().getName() + "執行緒執行" + i + "次"); } System.out.println(Thread.currentThread().getName() + "執行緒運行結束"); } public static void main(String[] args) { TestYield yield = new TestYield(); new Thread(yield,"a").start(); new Thread(yield,"b").start(); } } -
效果展示
可以看出ab執行緒交替執行
3.4、執行緒加入
主要用到jion()方法,執行緒加入相當于該執行緒插隊,插隊后該執行緒強制執行到結束,其他執行緒在此期間阻塞
-
代碼實作
package com.kuang.class3; /** * 執行緒加入的案例,執行緒加入相當于讓這個執行緒強制執行完,其他執行緒在此期間是阻塞的 */ public class TestThreadJoin implements Runnable{ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("VIP執行緒" + i); } } public static void main(String[] args) throws InterruptedException { TestThreadJoin join = new TestThreadJoin(); Thread thread = new Thread(join); thread.start(); for (int i = 0; i < 10; i++) { if(i == 5) { thread.join(); // 執行緒插隊,并且強制執行 } System.out.println("main執行緒" + i); } } } -
效果展示
主執行緒執行五次后,子執行緒加入,并且執行完畢
3.5、執行緒狀態檢測
用到Thread.State中的幾個類變數,分別對應執行緒不同時期的狀態
-
代碼實作
package com.kuang.class3; /** * 觀測執行緒狀態 */ public class TestThreadState { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { for (int i = 0; i < 5; i++) { if (i % 2 == 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println("執行緒運行中,,,"); } System.out.println("/////////"); }); Thread.State state = thread.getState(); System.out.println(state); // NEW狀態 thread.start(); state = thread.getState(); System.out.println(state); while(state != Thread.State.TERMINATED) { state = thread.getState(); System.out.println(state); Thread.sleep(500); // 每0.5s檢測一次狀態 } } } -
效果展示
3.6、執行緒優先級
- 執行緒的優先級從低到高分為了10個等級,分別對應數字1到10
- 一條執行緒可以使用
setPriority()方法自定義設定優先級,可以使用getPriority()查看優先級 - 高優先級并非優先調度,只是被調度的概率提高了,具體調度還是得看CPU
-
代碼實作
package com.kuang.class3; public class TestThreadPriority implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + "的優先級是" + Thread.currentThread().getPriority()); } public static void main(String[] args) { TestThreadPriority test = new TestThreadPriority(); Thread t1 = new Thread(test,"t1"); Thread t2 = new Thread(test,"t2"); Thread t3 = new Thread(test,"t3"); Thread t4 = new Thread(test,"t4"); Thread t5 = new Thread(test,"t5"); // 執行緒自定義優先級 t1.setPriority(Thread.MAX_PRIORITY); t2.setPriority(Thread.MIN_PRIORITY); t3.setPriority(8); t4.setPriority(3); t5.setPriority(Thread.NORM_PRIORITY); // 默認優先級 t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); } } -
效果展示
可見高優先級并非優先調度
3.7、守護執行緒
- java提供了兩種執行緒:守護執行緒和用戶執行緒
- 守護執行緒,是指在程式運行時 在后臺提供一種通用服務的執行緒,這種執行緒并不屬于程式中不可或缺的部分
- 用戶執行緒可以理解為被守護執行緒,JVM會等待所有用戶執行緒,當用戶執行緒執行完畢后,只剩守護執行緒存在,這時JVM會關閉,而不會等待守護執行緒再執行完畢,會殺死所有的守護執行緒(因為守護執行緒守護著用戶執行緒,當用戶執行緒執行完畢后,守護執行緒就無事可做了,當然沒必要再往下繼續執行了)
- 守護執行緒的優先級一般較低,用戶可以使用
setDaemon()方法主動設定執行緒為守護執行緒
-
代碼實作
package com.kuang.class3; /** * 主要測驗守護執行緒的設定 */ public class TestDaemon implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()+"執行緒啟動"); // 這里延時是為了阻塞守護執行緒,讓用戶執行緒先執行完畢 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 這句話永遠不會輸出,因為被守護執行緒關閉后,jvm就關閉了,jvm不會等守護執行緒結束 System.out.println(Thread.currentThread().getName()+"執行緒關閉"); } public static void main(String[] args){ System.out.println("main執行緒啟動"); TestDaemon test = new TestDaemon(); Thread thread = new Thread(test,"A"); thread.setDaemon(true); thread.start(); System.out.println("main執行緒關閉"); } } -
效果展示
可以發現JVM不會等待守護執行緒執行完畢,在用戶執行緒執行完后會立即殺死守護執行緒
四、執行緒同步
? 第二節講到不控制執行緒并發的一些問題,比如買票是搶到同一張票(資料不唯一),嚴重的可能會導致死鎖現象,即執行緒的永久等待;本節針對控制多執行緒并發的一系列問題,提出相應的解決方法,
4.1、執行緒同步的概念
-
執行緒不同步的后果
? 當多執行緒在操作共享資源時,如果不控制執行緒的并發,就可能會導致共享區資料不唯一,或者執行緒間的死鎖現象;尤其針對高并發環境下,比如12306買票,或者微信搶紅包;如果不控制并發,很可能會出現比如有人在群里發了一個一百塊紅包,一百人同時開搶,可能這一百人都會搶到一百塊錢,給平臺造成損失,
-
何時需要執行緒同步
? 當有一個執行緒在對記憶體進行操作時,其他執行緒都不可以對這個記憶體地址進行操作,直到該執行緒完成操作, 其他執行緒才能對該記憶體地址進行操作,而其他執行緒又處于等待狀態,實作執行緒同步的方法有很多,臨界區物件(共享資源)就是其中一種
執行緒同步的底層涉及到一些演算法問題,深入理解的話就需要學習《作業系統》
4.2、Java執行緒同步方法
4.2.1、同步方法或者同步塊
主要學習synchronized關鍵字的使用
①、synchronized修飾方法
? 使方法轉變為同步方法,相當于給方法加鎖;當方法中有執行緒正在操作,該方法就會被加鎖,其他執行緒就必須排隊;直到該執行緒操作完畢后主動解鎖,其他執行緒再競爭進入該方法;之后依舊進行加鎖和解鎖操作,
-
代碼實作
重現搶票程式,觀察使用
synchronized修飾前和修飾后,輸出結果的變化package com.kuang.class4.synchronizedDemo; /** * 不安全的買票案例,體驗同步修飾符的用法 */ public class UnsafeBuyTicket { public static void main(String[] args) { BuyTicket station = new BuyTicket(); new Thread(station,"小明").start(); new Thread(station,"黃牛黨").start(); } } class BuyTicket implements Runnable { private int ticketNum = 10; // 票的數量 boolean flag = true; @Override public void run() { while(flag) { try { buy(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } // 此處將synchronized去掉再加上,觀察輸出變化 private synchronized void buy() throws InterruptedException { if (ticketNum <= 0) { flag = false; return; } System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNum-- + "張票"); Thread.sleep(100); } }給
buy()方法加鎖,觀察變化 -
前后對比
為加鎖前:可能會出現兩個人拿到同一張票的情況
加鎖后:一張票只能被一個人拿,執行緒安全
②、synchronized同步塊
-
用法:
synchronized(boj) {}此處
obj稱為同步監視器,obj可以是任何物件,但一般選用共享區資源作為監視器,例如上述買票程式中的票的數量ticketNum就是共享區資源
-
代碼實作
多執行緒操作不安全的泛型集合
ArrayList,觀察加鎖前后變化package com.kuang.class4.synchronizedDemo; import java.util.ArrayList; import java.util.List; /** * 執行緒不安全的泛型集合,體驗同步塊的用法 */ public class UnsafeList { public static void main(String[] args) { List<String> list = new ArrayList<>(); for (int i = 0; i < 100000; i++) { new Thread(() ->{ synchronized (list) { list.add(Thread.currentThread().getName());} }).start(); } // 這里的sleep是為了讓主執行緒休眠,否則主執行緒可能會提前輸出list的size try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(list.size()); } }多執行緒給
list添加1000000條資料,最后輸出list的大小 -
前后對比
未使用同步塊之前,缺少資料

使用同步塊后,
list的大小準確

4.2.2、Lock鎖
取自JUC包下的鎖機制,只能對代碼塊進行加鎖,但效率比synchronized更高
①、一般寫法
private final ReentrantLock lock = new ReentrantLock(); // 定義鎖
public void run() {
try {
lock.lock();
// 這里寫需要同步的代碼塊
}
finally {
lock.unlock(); // 如同步代碼塊有例外,則將解鎖寫在此處
// 但一般都在此處解鎖
}
}
②、案例說明
繼續以買票案例為例
-
代碼實作
package com.kuang.class4.lockDemo; import java.util.concurrent.locks.ReentrantLock; /** * 鎖機制的測驗案例 */ public class TestLock { public static void main(String[] args) { BuyTicket2 ticket2 = new BuyTicket2(); new Thread(ticket2,"小明").start(); new Thread(ticket2,"小紅").start(); new Thread(ticket2,"黃牛黨").start(); } } class BuyTicket2 implements Runnable { private int ticketNum = 10; boolean flag = true; private final ReentrantLock lock = new ReentrantLock(); // 定義可重入鎖 @Override public void run() { while(flag) { try { buy(); } catch (InterruptedException e) { e.printStackTrace(); } } } private void buy() throws InterruptedException { try { lock.lock(); // 在臨界區加鎖 if (ticketNum <= 0) { flag = false; return; } System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNum-- + "張票"); } finally { lock.unlock(); // 退出臨界區后解鎖 } Thread.sleep(1000); // 本執行緒阻塞,其他執行緒開始競爭 } } -
效果展示
執行緒安全,無死鎖和不安全資料
五、執行緒通信
2.1、執行緒通信的概念
2.1.1、為什么需要執行緒通信
執行緒是作業系統調度的最小單位,有自己的堆疊空間,可以按照既定的代碼逐步的執行,但是如果每個執行緒間都孤立的運行,那就會造資源浪費,所以在現實中,我們需要這些執行緒間可以按照指定的規則共同完成一件任務,所以這些執行緒之間就需要互相協調,這個程序被稱為執行緒的通信
因此執行緒通信可以概括為:當多個執行緒共同操作共享的資源時,互相告知自己的狀態以避免資源爭奪,并且互相協調,完成同一件任務
2.1.2、執行緒的通信的方式
- 共享記憶體
- 管程
- 信號量
- 管道
- 其他
2.2、管程法
實作生產者、消費者模型,該模型大致定義如下:
- 存在緩沖區,生產者在緩沖區放入東西,消費者從緩沖區取出東西進行消費
- 生產者,當緩沖區不滿時,生產者就生產東西放入緩沖區;當緩沖區滿時,生產者就停止生產進入阻塞,并通知消費者取出東西
- 消費者,當緩沖區存在東西時,就不斷取出;當緩沖區為空時,消費者停止消費進入阻塞,并通知生產了生產東西
-
代碼實作
package com.kuang.class5; /** * 測驗生產者和消費者的案例 */ public class TestProducerAndCustomer { public static void main(String[] args) { SynContainer container = new SynContainer(); new Producer(container).start(); // 啟動生產者執行緒 new Customer(container).start(); // 啟動消費者執行緒 } } // 生產者類 class Producer extends Thread { SynContainer container; public Producer(SynContainer container) { this.container = container; } @Override public void run() { for (int i = 1; i < 101; i++) { container.push(new Goods(i)); // 給緩沖區放入東西 System.out.println("生產了第" + i + "個商品"); } } } class Customer extends Thread { SynContainer container; public Customer(SynContainer container) { this.container = container; } @Override public void run() { for (int i = 1; i < 101; i++) { System.out.println("消費了第" + container.pop().id + "個商品"); // 從緩沖區取出東西 } } } // 商品類 class Goods { int id; // 產品編號 public Goods(int id) { this.id = id; } } // 緩沖區類 class SynContainer { Goods[] goods = new Goods[10]; // 定義一個容量為10的緩沖區 int count = 0; // 緩沖區商品計數器 // 放入緩沖區 public synchronized void push(Goods g) { if (count == goods.length) { // 緩沖區滿了,生產者休息 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } goods[count] = g; count++; // 通知消費者消費 this.notifyAll(); } // 從緩沖區取出 public synchronized Goods pop() { if (count == 0) { try { // 緩沖區為空,消費者阻塞 this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } count--; // 通知生產者生產 this.notifyAll(); return goods[count]; } } -
效果展示
![https://img2022.cnblogs.com/blog/2875618/202209/2875618-20220912215157733-2044630329.png)
2.3、信號量法
是在多執行緒環境下使用的一種設施,是可以用來保證兩個或多個關鍵代碼段不被并發呼叫,在進入一個關鍵代碼段之前,執行緒必須獲取一個信號量;一旦該關鍵代碼段完成了,那么該執行緒必須釋放信號量,其它想進入該關鍵代碼段的執行緒必須等待直到第一個執行緒釋放信號量,
-
代碼實作
代碼實作生產者消費者模型的吃水果案例,媽媽往盤子中放水果,我從盤子中拿水果吃,盤子只有一個;當盤子中有水果,就不能再放了,并通知我吃水果;當盤子中沒了水果,我就不能吃,并通知媽媽放水果,
package com.kuang.class5; /** * 吃水果的生產者消費者模型 * 媽媽往盤子放水果,我從盤子中取水果吃 */ public class TestEatFruit { public static void main(String[] args) { Plate plate = new Plate(); new Me(plate).start(); new Mom(plate).start(); } } class Mom extends Thread { Plate plate; public Mom(Plate plate) { this.plate = plate; } @Override public void run() { for (int i = 0; i < 20; i++) { if (i % 2 == 0) { plate.put("蘋果"); } else if (i % 5 == 0) { plate.put("香蕉"); } else { plate.put("橘子"); } } } } class Me extends Thread { Plate plate; public Me(Plate plate) { this.plate = plate; } @Override public void run() { for (int i = 0; i < 20; i++) { plate.eat(); } } } class Plate { String fruitName; // 水果名 boolean mutex = true; // 定義互斥信號量 public synchronized void put(String name) { if (!mutex) { try { this.wait(); // 當盤子中有水果就必須阻塞 } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("媽咪給了一個" + name); this.notifyAll(); // 放了一個水果,通知我吃水果 this.fruitName = name; this.mutex = !mutex; } public synchronized void eat() { if (mutex) { try { this.wait(); // 當盤子中沒有水果,就阻塞 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我吃掉了一個" + fruitName); this.notifyAll(); // 吃掉一個水果,通知放水果 this.mutex = !mutex; } } } -
效果展示
兩兩一組

2.4、執行緒池法
在高并發情況下,經常要進行執行緒的創建與銷毀,對性能影響很大;執行緒池法的思路就是提前創建好多個執行緒,放入執行緒池中,使用時直接獲取,使用完后放入池中,可以必變重復的創建和銷毀操作,提高服務器效率(可以類比共享單車,提前投放一批單車,使用時直接掃碼用,使用完后放回單車點)
思路:
- 多執行緒實作類實作Runnable介面
- 主類使用
ExecutorService創建執行緒池 - 使用
execute(Runnable obj)執行任務,該方法屬于上面介面,沒有回傳值 - 最后使用
shutdown(),關閉連接池
-
代碼實作
package com.kuang.class5; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 使用執行緒池方法實作執行緒通信 */ public class TestThreadPool { public static void main(String[] args) { // 創建執行緒池服務,引數為執行緒池大小 // 該部分內容屬于JUC編程,Executors也屬于JUC包 ExecutorService service = Executors.newFixedThreadPool(10); service.execute(new MyThread()); service.execute(new MyThread()); service.execute(new MyThread()); service.execute(new MyThread()); service.shutdown(); } } class MyThread implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName()); } } -
效果展示

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/506538.html
標籤:其他
上一篇:spring native 初體驗實作 小米控制美的空調
下一篇:python學習:三目運算子
