目錄
認識線程
線程是什么:
線程與進程的區別
Java中的線程和操作系統線程的關系
創建線程
繼承Thread類
實現Runnable接口
其他變形
Thread類及其常見方法
Thread的常見構造方法
Thread類的幾個常見屬性
Thread類常用的方法
啟動一個線程-start()
中斷一個線程-interrupt()
?等待一個線程-join()
線程的狀態
觀察線程的所有狀態
?觀察線程狀態和轉移
線程的安全(重點)
線程安全的概念
線程不安全的原因
?修改共享數據
原子性
可見性
順序性
解決線程不安全的問題
synchronized關鍵字
synchronized使用示例
volatile關鍵字
volatile和synchronized區別
wait和notify關鍵字
wait和sleep的區別(面試)
多線程案例
單例模式
餓漢模式
懶漢模式
阻塞式隊列
生產者消費者模式
?定時器
?線程池
認識線程
線程是什么:
1)首先一個線程就是一個“執行流”,每一個線程直間都可以按照順序執行自己的代碼,多個線程之間“同時”執行多份代碼(這里可能會有疑問,為什么同時要有一個引號呢,后面我們來揭曉)
線程與進程的區別
1)進程是包含線程的,每一個進程至少有一個線程稱作為主線程。
2)進程與進程之間不共享空間,但是同一個進程中的線程共享同一個內存空間
3)進程是系統分配的最小單元,線程是系統調度的最小單元
4)線程比進程更加輕量化,創建銷毀調度都比進稱更快
最后,線程雖然比進程更加輕量化,但是還不能滿足我們的需求,于是就引進了線程池和協程。
Java中的線程和操作系統線程的關系
線程是操作系統的概念,操作系統內核實現了線程這樣的機制,并且對用戶層提供了一些API供用戶來使用(如Linux中的pthread庫)
Java標準庫中Thread類可以視為是對操作系統提供的API進行進一步的抽象和封裝
創建線程
繼承Thread類
class Thread1 extends Thread{@Overridepublic void run() {System.out.println("這里是線程運行代碼!");}
}
public class Demo1 {public static void main(String[] args) {//創建Thread1類的實例Thread1 t = new Thread1();//調用start方法 啟動線程t.start();}
}
實現Runnable接口
class MyRunnable implements Runnable{@Overridepublic void run() {System.out.println("這里是線程運行代碼");}
}
public class Demo1 {public static void main(String[] args) {//創建Thread1類的實例//Thread1 t = new Thread1();//創建Thread類實例,調用Thread的構造方法時,將Runnable對象作為target參數Thread t = new Thread(new MyRunnable());//調用start方法 啟動線程t.start();}
}
其他變形
1)匿名內部類創建子類對象
Thread t = new Thread(){@Overridepublic void run() {System.out.println("使用匿名內部類創建Thread子類對象");}};//調用start方法 啟動線程t.start();
2)匿名內部類創建Runnale子類對象
Thread t = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("使用匿名內部類創建Runnable子類對象");}});//調用start方法 啟動線程t.start();
3)lambda表達式創建Runnable子類對象
Thread t = new Thread(()->{System.out.println("使用lambda表達式創建Runnable子類對象");});//調用start方法 啟動線程t.start();
Thread類及其常見方法
Thread類是JVM用來管理線程的一個類,換句話來說,每一個線程都有唯一的Thread對象與之關聯用我們上面的例子來看,每一個執行流,都需要一個對象來描述,而Thread對象就是用來描述一個線程執行流的,JVM會將這些Thread對象組織起來,用于線程調度、管理。
Thread的常見構造方法
方法 | |
Thread() | 創建線程對象 |
Thread(Runnable target) | 使用Runnable對象創建線程對象 |
Thread(String name) | 創建線程對象,并且命名 |
Thread(Runnable target,String name) | 使用Runnable創建線程對象,并命名 |
Thread(TreadGroup group,Runnable target)(了解) | 線程可以用來分組管理,分好的組即為線程組,這個目前我們了解即可 |
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("這是我的名字");
Thread t4 = new Thread(new MyRunnable(), "這是我的名字");
Thread類的幾個常見屬性
屬性 | 獲取方法 |
ID | getId() |
名稱 | getName() |
狀態 | getState() |
優先級 | getPriority() |
是否后臺線程 | isDaemon() |
是否存活 | isAlive() |
是否被中斷 | isInterrupted() |
ID是線程的唯一標識,不同線程不會重復
名稱是各種調試工具用到
狀態表示線程但前所處于的一個情況,下面我們會進一步的說明
優先級高的線程理論上是更容易被調度到
關于后臺線程,需要記住一點:JVM會在一個進程的所有非后臺進程結束后,才會結束運行。
是否存活,即簡單的理解為,run方法是否結束運行
線程中斷問題,之后我們進一步的說明
我們可以將下面的代碼運行一下理解,上面的屬性。
public static void main(String[] args) {Thread thread = new Thread(() -> {for (int i = 0; i < 10; i++) {try {System.out.println(Thread.currentThread().getName() + ": 我還活著");Thread.sleep(1 * 1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + ": 我即將死去");});System.out.println(Thread.currentThread().getName()+ ": ID: " + thread.getId());System.out.println(Thread.currentThread().getName()+ ": 名稱: " + thread.getName());System.out.println(Thread.currentThread().getName()+ ": 狀態: " + thread.getState());System.out.println(Thread.currentThread().getName()+ ": 優先級: " + thread.getPriority());System.out.println(Thread.currentThread().getName()+ ": 后臺線程: " + thread.isDaemon());System.out.println(Thread.currentThread().getName()+ ": 活著: " + thread.isAlive());System.out.println(Thread.currentThread().getName()+ ": 被中斷: " + thread.isInterrupted());thread.start();while (thread.isAlive()) {}System.out.println(Thread.currentThread().getName()+ ": 狀態: " + thread.getState());}
Thread類常用的方法
啟動一個線程-start()
之前我們已經看到了如何覆寫run方法創建一個對象,但是線程對象被創建出來并不意味著線程開始運行,而調用start方法,才真正的在操作系統底層創建出了一個線程。
中斷一個線程-interrupt()
例如:李四一旦進到工作狀態,他就會按照行動指南上的步驟去進行工作,不完成是不會結束的。但有時我們 需要增加一些機制,例如老板突然來電話了,說轉賬的對方是個騙子,需要趕緊停止轉賬,那張三該如 何通知李四停止呢?這就涉及到我們的停止線程的方式了。
目前常見的中斷線程常見下面兩種方式:
? ? ? ? 1、通過共享一個標記(創建一個變量)
? ? ? ? 2、調用interrupt()方法
實例1:使用自定義變量來作為標志位
? ? ? ? 需要給標志位加上volatile 關鍵字(這個關鍵字的功能我們后面會介紹)
public class Demo2 {public static volatile boolean isQuit = false;public static void main(String[] args) {Thread t1 = new Thread(()->{while(!isQuit){System.out.println(Thread.currentThread().getName()+"別管我,我在轉賬呢!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(Thread.currentThread().getName()+"幸虧沒全轉過去!");},"李四");System.out.println(Thread.currentThread().getName()+":開始轉賬!");t1.start();try {Thread.sleep(2000); // 主線程休眠,給 t1 線程一些時間來執行} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName()+"老板來電話了,通知李四對方是騙子");isQuit=true;}
}
運行結果:
實例2:使用Thread,interrupted()或者Therad.currentThread().isInterrupted()代替自定義標志位
Thread內部包含了一個boolean類型的變量作為線程中斷的標記?
方法 | 說明 |
public void interrupt() | 中斷對象關聯的線程,如果線程正在堵塞,則以異常通知否則設置標志位 |
public static boolean interrupted() | 判斷當前線程的中斷標志位是否設置,調用后清楚標志位 |
public boolean isInterrupted() | 判斷對象關聯的線程的標志位是否設置,調用后不清楚標志位 |
總結來說就是:interrupt()
用于設置中斷標志位,isInterrupted()
用于檢查中斷標志位的狀態,而 interrupted()
則是檢查并清除當前線程的中斷狀態。
public class Demo2 {//public static volatile boolean isQuit = false;public static void main(String[] args) {Thread t1 = new Thread(()->{//while(!Thread.interrupted())//兩種都可以while(!Thread.currentThread().isInterrupted()){System.out.println(Thread.currentThread().getName()+"別管我,我在轉賬呢!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(Thread.currentThread().getName()+"幸虧沒全轉過去!");},"李四");System.out.println(Thread.currentThread().getName()+":開始轉賬!");t1.start();try {Thread.sleep(2000); // 主線程休眠,給 t1 線程一些時間來執行} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName()+":老板來電話了,通知李四對方是騙子");//isQuit=true;t1.interrupt();}
}
如果我們按照上述代碼直接進行修改會報錯:
原因是?當你在 t1
線程的循環內使用 Thread.sleep(1000)
時,線程可能會在 sleep
過程中被 t1.interrupt()
中斷,從而導致 InterruptedException
被拋出。然而,你在 catch
塊中拋出了 RuntimeException
,可能導致編譯錯誤。
所以我們對上述代碼進行修改:
public class Demo2 {//public static volatile boolean isQuit = false;public static void main(String[] args) {Thread t1 = new Thread(()->{//while(!Thread.interrupted())//兩種都可以while(!Thread.currentThread().isInterrupted()){System.out.println(Thread.currentThread().getName()+"別管我,我在轉賬呢!");try {Thread.sleep(1000);} catch (InterruptedException e) {System.out.println(Thread.currentThread().getName()+ "被中斷了,幸虧沒轉過去!");// 恢復中斷狀態,以便后續代碼可以檢查中斷狀態Thread.currentThread().interrupt();}}System.out.println(Thread.currentThread().getName()+"幸虧沒全轉過去!");},"李四");System.out.println(Thread.currentThread().getName()+":開始轉賬!");t1.start();try {Thread.sleep(2000); // 主線程休眠,給 t1 線程一些時間來執行} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName()+":老板來電話了,通知李四對方是騙子");//isQuit=true;t1.interrupt();}
}
運行結果:
?等待一個線程-join()
有時我們需要等待一個線程才能完成它的工作,才能進行自己的工作。例如:張三只有等李四轉 賬成功,才決定是否存錢,這時我們需要一個方法明確等待線程的結束。
class MyRunnable implements Runnable{@Overridepublic void run() {for (int i = 0; i < 3; i++) {System.out.println(Thread.currentThread().getName()+":我正在工作!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(Thread.currentThread().getName()+":我的工作結束了!");}
}
public class Demo3 {public static void main(String[] args) throws InterruptedException {MyRunnable target = new MyRunnable();Thread t1 = new Thread(target,"李四");Thread t2 = new Thread(target,"張三");System.out.println("先讓李四工作!");t1.start();t1.join();System.out.println("李四的工作結束了,讓張三來工作!");t2.start();t2.join();System.out.println("張三的工作結束了!");}
}
運行結果:
可能這么看的話,不是特別明白join的作用,如果我們把join注釋掉:
作用就是顯而易見了!
線程的狀態
觀察線程的所有狀態
線程的狀態是一個枚舉類型 Thread.State
public class Demo4 {public static void main(String[] args) {for (Thread.State state : Thread.State.values()) {System.out.println(state);}}
}
?運行結果:
狀態 | 說明 |
NEW | 安排了工作,但是沒有運行 |
RUNNABLE | 可工作的,又可正在工作和即將工作 |
BLOCKED | 排隊等著其他事情 |
WAITING | 排隊等著其他事情 |
TIMED_WAITING | 排隊等著其他事情 |
TERMINATED | 工作完成 |
?關于new和Runnable狀態區別:
"New" 狀態是線程對象被創建但尚未啟動執行的階段,而 "Runnable" 狀態是線程已經準備好執行但還沒有被操作系統選中執行的階段。使用 start()
方法可以將線程從 "New" 狀態切換到 "Runnable" 狀態,然后操作系統負責將其切換到 "Running" 狀態并執行線程代碼。
狀態理解圖
?觀察線程狀態和轉移
觀察NEW、RUNNABLE、TERMINATED狀態的轉換
使用isAlive方法判定線程存活狀態
isAlive()
方法用于判斷線程是否已經啟動并且尚未終止。該方法返回一個布爾值,如果線程處于活動狀態,即正在運行或者已經啟動但還未終止,返回true
;如果線程已經終止,返回false
。?
public class Demo4 {public static void main(String[] args) {Thread t = new Thread(()->{for (int i = 0; i < 100; i++) {}},"李四");System.out.println(t.getName()+": "+t.getState());t.start();while(t.isAlive()){System.out.println(t.getName()+": "+t.getState());}System.out.println(t.getName()+": "+t.getState());}
}
?運行結果:
當線程處于不同的狀態時,可以通過示例更好地理解它們。以下是針對"BLOCKED"、"WAITING" 和 "TIMED_WAITING" 三種狀態的示例:
BLOCKED(阻塞狀態):
Object lock = new Object();Thread thread1 = new Thread(() -> {synchronized (lock) {// 執行一些同步操作}
});Thread thread2 = new Thread(() -> {synchronized (lock) {// 這里的線程會進入 BLOCKED 狀態,因為 lock 被 thread1 持有}
});thread1.start();
thread2.start();
WAITING(等待狀態):
Object monitor = new Object();Thread thread1 = new Thread(() -> {synchronized (monitor) {try {monitor.wait(); // 進入 WAITING 狀態,等待被喚醒} catch (InterruptedException e) {// 處理中斷異常}}
});Thread thread2 = new Thread(() -> {synchronized (monitor) {monitor.notify(); // 喚醒等待中的線程}
});thread1.start();
thread2.start();
TIMED_WAITING(定時等待狀態):
Thread thread = new Thread(() -> {try {Thread.sleep(5000); // 進入 TIMED_WAITING 狀態,等待 5 秒后自動恢復} catch (InterruptedException e) {// 處理中斷異常}
});thread.start();
在上述示例中:
- "BLOCKED" 狀態:在第一個示例中,
thread2
試圖獲得與thread1
共享的鎖時,會進入 "BLOCKED" 狀態,因為鎖被thread1
持有。 - "WAITING" 狀態:在第二個示例中,
thread1
調用了monitor.wait()
,進入 "WAITING" 狀態,等待被thread2
喚醒。 - "TIMED_WAITING" 狀態:在第三個示例中,
thread
調用了Thread.sleep(5000)
,進入 "TIMED_WAITING" 狀態,等待 5 秒后自動恢復到 "RUNNABLE" 狀態。
上訴會有一些關于鎖的用法,后面會說到。
yield()大公無私,讓出CPU
public static void main(String[] args) {Thread t1 = new Thread(()->{while(true){System.out.println("張三");//Thread.yield();}},"t1");Thread t2 = new Thread(()->{while(true){System.out.println("李四");}},"t2");t1.start();t2.start();}
?這段代碼,如果我把yield()注釋掉,張三和李四會幾乎均等的打印,但是如果我不注釋掉李四打印次數就會遠遠大于張三。
通過調用 yield
方法,線程可以主動放棄執行,讓其他線程獲得運行的機會,從而實現更合理的調度。
線程的安全(重點)
線程安全的概念
如果多線程環境下代碼運行結果是符合我們的預期的,即在單線程的環境下應該的結果,則說這個程序是線程安全的。
線程不安全的原因
首先我們看以下的代碼:
public class Demo5 {public static int count=0;public static void increase(){count++;}public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 5000; i++) {increase();}});Thread t2 = new Thread(()->{for (int i = 0; i < 5000; i++) {increase();}});t1.start();t2.start();t1.join(); // 等待 t1 線程完成t2.join(); // 等待 t2 線程完成System.out.println(count);}
}
這段代碼我們想的結果肯定是10000,t1線程執行5000次,之后t2線程執行5000次,但是得出的結果是一個接近10000的隨機值
此時我們就可以說這個線程是不安全的
?修改共享數據
上面不安全的代碼中涉及到多個線程針對count變量進行修改,此時count是一個多個線程都可以訪問到的”共享數據“
原子性
我們可以把一段代碼想象成一間房間,每一個線程就是進入這個房間的人。如果沒有任何機制保證,A進入房間之后,沒有出來,B是不可以進入房間的,這就是原子性。如果B進入房間了,打斷了A的隱私,這個就是不具備原子性.原子是保證是不可分割的。
比如我們剛剛的操作:count++,其實是由三個步驟組成的。
1、從內存把數據讀到CPU(load)
2、進行數據更新(add)
3、把數據寫回到CPU(save)
我們可以看一下這個圖,由于多個線程是并發執行的,當t1,t2線程把count都讀取的時候這時候讀取的count都是0?,兩個線程都對其++,這個時候t1讀取的count=1,t2讀取的也count=1,當把數據寫回CPU的時候是count=1,所以count并不是等于2,而且上述的過程是隨機的,所以最后的結果不是10000。
所以如果我們如果保證t1執行的過程t2不會插入就可以了,就如同A在房間的時候把房間上鎖,B進不來,就可以了。
可見性
可見性指的是:一個線程對共享變量值的修改,能夠及時的被其他線程看到。
就比如上述例子,如果t1增加的時候t2知道,就不會出現線程不安全的問題了。
順序性
一段代碼可能是這樣的:
1、去商場吃飯
2、回家寫10分鐘作業
3、去商場買文具
如果實在單線程情況下,JVM,CPU指令集會對其優化,比如,按照1->3->2的方式執行,也是沒有問題的,并且少去了一次商場。這種就叫做指令重排序。
處理器在執行指令時,可能會根據各種因素(例如處理器架構、緩存等)重新排列指令的執行順序,以最大程度地提高吞吐量和性能。這意味著代碼中的指令可能不會按照編寫的順序來執行。然而,指令重排序在單線程環境下通常不會引發問題,因為重排序不會影響單線程程序的結果。
指令重排序是現代處理器為了提高性能而采取的一種優化技術,它可以改變代碼中指令的執行順序,以更有效地利用處理器的執行單元。盡管指令重排序可以提高程序的執行速度,但在多線程編程中可能會引發一些問題,特別是與內存可見性和線程安全性有關的問題。
解決線程不安全的問題
對于上述線程不安全的問題,我們對代碼進行了修改加入了synchronized關鍵字。
public class Demo5 {public static int count=0;public static synchronized void increase(){count++;}public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 5000; i++) {increase();}});Thread t2 = new Thread(()->{for (int i = 0; i < 5000; i++) {increase();}});t1.start();t2.start();t1.join(); // 等待 t1 線程完成t2.join(); // 等待 t2 線程完成System.out.println(count);}
}
運行結果:
此時的運行結果就是和我們預期的相同了,下面我們就來說一下synchronized關鍵字。
synchronized關鍵字
1)互斥
?synchronized 會引起互斥效果,某個線程執行到某個對象的synchronized中時,其他線程如果也執行到同一個對象時,synochronized就會堵塞等待.
- ? ? ? ? 進入synchronized修飾的代碼塊,就相當于加鎖.
- ? ? ? ??退出synchronized修飾的代碼塊,就相當于解鎖.
理解 "阻塞等待". 針對每一把鎖, 操作系統內部都維護了一個等待隊列. 當這個鎖被某個線程占有的時候, 其他線程嘗 試進行加鎖, 就加不上了, 就會阻塞等待, 一直等到之前的線程解鎖之后, 由操作系統喚醒一個新的 線程, 再來獲取到這個鎖.
注意:
???????? 上一個線程解鎖之后, 下一個線程并不是立即就能獲取到鎖. 而是要靠操作系統來 "喚醒". 這 也就是操作系統線程調度的一部分工作.
????????假設有 A B C 三個線程, 線程 A 先獲取到鎖, 然后 B 嘗試獲取鎖, 然后 C 再嘗試獲取鎖, 此時 B 和 C 都在阻塞隊列中排隊等待. 但是當 A 釋放鎖之后, 雖然 B 比 C 先來的, 但是 B 不一定就能 獲取到鎖, 而是和 C 重新競爭, 并不遵守先來后到的規則.
synchronized底層是使用操作系統mutex lock實現的.
2)刷新內存
synochronized的工作過程;
? ? ? ? 1、獲得互斥鎖
? ? ? ? 2、從主內存拷貝變量的最新副本到工作的內存
? ? ? ? 3、執行代碼
? ? ? ? 4、將更改后的共享變量的值刷新到主內存
? ? ? ? 5、釋放互斥鎖
所以synchronized也能保證內存可見性.
3)可重入
可重入(Reentrancy),也被稱為遞歸性,是指一個線程在持有某個鎖的情況下,能夠再次獲得該鎖而不會被阻塞。這種情況下,鎖會記錄線程持有的次數,每次成功獲取鎖時,計數器會增加,釋放鎖時,計數器會遞減。只有當計數器為零時,鎖才會被完全釋放,其他線程才能獲得鎖。
可重入性使得編寫遞歸代碼和多層嵌套調用時更加方便,因為你不必擔心線程會因為多次獲得同一個鎖而陷入死鎖狀態。同時,它也為一些高級同步機制(如可重入鎖、讀寫鎖等)的實現提供了基礎。
synchronized使用示例
synchronized 本質上要修改指定對象的 "對象頭". 從使用角度來看, synchronized 也勢必要搭配一個具 體的對象來使用.
示例1:同步實例方法
public class SynchronizedExample {private int count = 0;public synchronized void increment() {count++;}public synchronized int getCount() {return count;}
}
在這個示例中,increment
和 getCount
方法都被使用 synchronized
修飾,這意味著同一時刻只有一個線程可以訪問這些方法。這樣可以確保在多線程環境中對 count
變量的操作是安全的。
示例 2:同步代碼塊
public class SynchronizedExample {private int count = 0;private Object lock = new Object();public void increment() {synchronized (lock) {count++;}}public int getCount() {synchronized (lock) {return count;}}
}
在這個示例中,我們使用了同步代碼塊來控制對 count
變量的訪問。通過指定一個對象作為鎖,我們確保在同一時刻只有一個線程可以進入同步塊,從而實現了線程安全。
示例 3:靜態同步方法
public class SynchronizedExample {private static int count = 0;public static synchronized void increment() {count++;}public static synchronized int getCount() {return count;}
}
這個示例中,我們使用 synchronized
關鍵字修飾了靜態方法。靜態同步方法鎖定的是類的 Class
對象,確保在同一時刻只有一個線程可以訪問這些靜態方法。
區別總結:
- 示例1中的鎖是實例對象的監視器鎖,用于同步實例方法。
- 示例2中的鎖是自定義的
lock
對象,用于同步代碼塊。它可以實現更細粒度的同步控制,也可以用于不同實例之間的同步。 - 示例3中的鎖是類的
Class
對象,用于同步靜態方法。
在選擇使用哪種同步方式時,你需要根據實際需求來考慮同步的粒度、范圍以及性能等因素。
volatile關鍵字
volatile能保證內存可見性
volatile
關鍵字在Java中用于確保變量的可見性。它的主要作用是告訴Java虛擬機,這個變量可能被多個線程同時訪問,因此需要確保線程之間對這個變量的修改對其他線程是可見的。具體來說,volatile
變量具有以下特性:
-
禁止重排序:
volatile
變量會禁止編譯器和運行時環境對其賦值和讀取操作進行重排序。這確保了寫操作不會被提前到讀操作之前,從而避免了可能的可見性問題。 -
強制刷新主內存: 當一個線程對
volatile
變量進行寫操作時,會強制將該變量的值刷新到主內存中,而不僅僅是線程的本地緩存。當其他線程需要讀取這個變量時,它們會從主內存中讀取最新的值,而不是從自己的本地緩存。
通過禁止重排序和強制刷新主內存,volatile
變量確保了對這個變量的寫操作對其他線程是可見的,從而保證了內存可見性。
代碼示例:
import java.util.Scanner;public class Demo6 {public static int flag=0;public static void main(String[] args) {Thread t1 = new Thread(()->{while(flag==0){}System.out.println("循環結束!");});Thread t2 = new Thread(()->{Scanner scan = new Scanner(System.in);System.out.println("輸入一個整數:");flag = scan.nextInt();});t1.start();t2.start();}
}// 執行效果
// 當用戶輸入非0值時, t1 線程循環不會結束. (這顯然是一個 bug)
讀的是自己工作內存中的內容. 當 t2 對 flag 變量進行修改, 此時 t1 感知不到 flag 的變化.
這種情況就是典型的可見性問題,即一個線程對共享變量的修改對其他線程不可見。使用 volatile
關鍵字可以解決這個問題,因為它會強制線程在讀取和寫入 flag
變量時都從主內存中讀取和寫入,從而確保線程之間的可見性。
public static volatile int flag = 0;
// 執行效果
// 當用戶輸入非0值時, t1 線程循環能夠立即結束.
volatile和synchronized區別
volatile不保證原子性
volatile 和 synchronized 有著本質的區別. synchronized 能夠保證原子性, volatile 保證的是內存可見性.
public class Demo5 {public static volatile int count=0;public static void increase(){count++;}public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 5000; i++) {increase();}});Thread t2 = new Thread(()->{for (int i = 0; i < 5000; i++) {increase();}});t1.start();t2.start();t1.join(); // 等待 t1 線程完成t2.join(); // 等待 t2 線程完成System.out.println(count);}
}//運行結果:7602
此時可以看到,最終count無法保證是10000
synchronized 也能保證內存可見性
synchronized 既能保證原子性, 也能保證內存可見性.
public volatile static int flag=0;public static Object lock = new Object();public static void main(String[] args) {Thread t1 = new Thread(()->{while(true){synchronized (lock){if(flag!=0){break;}}}System.out.println("循環結束!");});Thread t2 = new Thread(()->{Scanner scan = new Scanner(System.in);System.out.println("輸入一個整數:");flag = scan.nextInt();});t1.start();t2.start();}
wait和notify關鍵字
由于線程之間是搶占式執行的,因此線程之間執行的先后順序難以預知,但是實際開發中有時候我們希望合理的協調多個線程之間的執行先后順序。
完成這個協調工作, 主要涉及到三個方法?
????????wait() / wait(long timeout): 讓當前線程進入等待狀態.
????????notify() / notifyAll(): 喚醒在當前對象上等待的線程.
注意: wait, notify, notifyAll 都是 Object 類的方法.
wait()方法
wait作用:
- ?????????使當前執行代碼的線程進行等待(把線程放入到等待隊列中)
- ? ???????釋放當前鎖
- ? ? ? ? ?滿足一定條件時被喚醒,重新嘗試獲取這個鎖
wati要搭配synchronized來使用,脫離synchronized使用wait會拋出異常
wait結束的等待條件:
- ? ? ? ? 其他線程調用該對象的notify方法
- ? ? ? ? wait等待的時間超過(wait方法提供一個帶有timeout參數的版本,來指定時間)
- ? ? ? ? 其他線程調用該等待線程的interrupted方法,導致wait拋出InterruptedException異常
示例:
public static void main(String[] args) {Object lock = new Object();Thread t = new Thread(()->{synchronized (lock){System.out.println("正在執行");try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("執行結束");}});t.start();}
這樣在執行到object.wait()之后就一直等待下去,那么程序肯定不能一直這么等待下去了。這個時候就 需要使用到了另外一個方法喚醒的方法notify()
notify()方法
notify方法是喚醒等待線程
示例:
public class Demo7 {private static final Object lock = new Object();private static boolean flag = false;public static void main(String[] args) {Thread t1 = new Thread(() -> {synchronized (lock) {while (!flag) {try {lock.wait(); // 等待條件滿足} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("Thread t1: Condition met!");}});Thread t2 = new Thread(() -> {synchronized (lock) {System.out.println("Thread t2: Condition is about to be met.");flag = true;lock.notify(); // 通知等待的線程條件已滿足}});t1.start();t2.start();}
}
?方法notify()也要在同步方法或同步塊中調用,該方法是用來通知那些可能等待該對象的對象鎖的 其它線程,對其發出通知notify,并使它們重新獲取該對象的對象鎖。
如果有多個線程等待,則有線程調度器隨機挑選出一個呈 wait 狀態的線程。(并沒有 "先來后到")
在notify()方法后,當前線程不會馬上釋放該對象鎖,要等到執行notify()方法的線程將程序執行 完,也就是退出同步代碼塊之后才會釋放對象鎖。
notifyAll()方法
notifyAll
方法是 Java 中用于線程間通信的一個方法,它類似于 notify
方法,但有一些不同之處。
notifyAll
方法用于喚醒在當前對象上調用了 wait
方法而進入等待狀態的所有線程。與 notify
方法不同,它會通知所有等待的線程,而不僅僅是其中一個。這在某些情況下非常有用,特別是當有多個線程在同一個對象上等待某個條件滿足時。
示例:
public class Demo7 {private static final Object lock = new Object();private static boolean flag = false;public static void main(String[] args) {Thread t1 = new Thread(() -> {synchronized (lock) {while (!flag) {try {lock.wait(); // 等待條件滿足} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("Thread t1: Condition met!");}});Thread t2 = new Thread(() -> {synchronized (lock) {System.out.println("Thread t2: Condition is about to be met.");flag = true;lock.notifyAll(); // 通知所有等待的線程條件已滿足}});Thread t3 = new Thread(() -> {synchronized (lock) {while (!flag) {try {lock.wait(); // 等待條件滿足} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("Thread t3: Condition met!");}});t1.start();t2.start();t3.start();}
}
//運行結果:
//Thread t2: Condition is about to be met.
//Thread t1: Condition met!
//Thread t3: Condition met!
在這個示例中,除了使用 notifyAll
方法通知等待的 t1
線程外,還有一個額外的 t3
線程等待條件滿足。當 t2
線程滿足條件并調用 notifyAll
后,所有等待在 lock
上的線程都會被喚醒。
需要注意的是,使用 notifyAll
可以確保所有等待的線程都能夠被喚醒,但在某些情況下可能會導致過多的線程被喚醒,從而影響性能。因此,在選擇使用 notify
還是 notifyAll
時,需要根據具體的應用場景來考慮。
wait和sleep的區別(面試)
總結來說,wait
和 sleep
的主要區別在于:
wait
是Object
類的方法,用于線程之間的協作和通信;sleep
是Thread
類的方法,用于線程休眠。wait
需要在同步代碼塊內部使用,而sleep
可以在任何地方使用。- 調用
wait
會釋放鎖,調用sleep
不會釋放鎖。 wait
通常和條件判斷一起使用,用于等待某個條件滿足;sleep
用于實現線程的延遲或定時操作。
多線程案例
單例模式
提到單例模式dehua,我們就需要了解一下什么是設計模式:
設計模式好比象棋中的 "棋譜". 紅方當頭炮, 黑方馬來跳. 針對紅方的一些走法, 黑方應招的時候有 一些固定的套路. 按照套路來走局勢就不會吃虧.
軟件開發中也有很多常見的 "問題場景". 針對這些問題場景, 大佬們總結出了一些固定的套路. 按照 這個套路來實現代碼, 也不會吃虧.
單例模式就是一種設計模式。?
單例模式能保證某個類在程序中只存在唯一一份實例,而不會創建出多個實例。
就比如JDBC中的DataSource實例就是只需要一個。
而單例模式具體實現又分為”餓漢模式“和”懶漢模式“兩種。
餓漢模式
它在類加載時就創建了單例實例,無論是否會被使用。在這種模式下,實例在類加載的時候就被創建,因此稱為“餓漢”模式。
class Singleton {private static Singleton instance = new Singleton();private Singleton() {// 私有構造方法}public static Singleton getInstance() {return instance;}
}
作用是:在第一次使用單例實例時不需要等待,但可能會導致資源浪費,因為實例在類加載時就被創建,即使在后續的運行中可能并未使用到它。
而其中將構造方法設置為私有是為了防止其他類實例化該類。這是單例模式的關鍵特性之一,確保只有一個實例能夠被創建。
懶漢模式
單線程版本
類在加載的過程中不創建實例,第一次使用的時候創建實例
public class Singleton {private static Singleton instance;private Singleton() {// 私有構造方法}public static Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}public static void main(String[] args) {Singleton singleton1 = Singleton.getInstance();Singleton singleton2 = Singleton.getInstance();System.out.println(singleton1 == singleton2); // 輸出 true,表示是同一個實例}
}
多線程版本
懶漢模式-多線程版本可能是不安全的:
在懶漢模式的多線程版本中,如果不加入額外的線程安全機制,可能會導致在多線程環境下創建多個實例,從而違背了單例模式的要求。這是因為多個線程可能會同時通過
if (instance == null)
的檢查,然后都進入到實例化的邏輯,最終導致創建多個實例
加入synchronized可以改善線程安全問題:
class Singleton {private static Singleton instance = null;private Singleton() {}public synchronized static Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}
?多線程改良版本
public class Singleton {private static volatile Singleton instance;private Singleton() {// 私有構造方法}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}public static void main(String[] args) {// 在多線程環境下測試Runnable task = () -> {Singleton singleton = Singleton.getInstance();System.out.println(singleton.hashCode());};// 啟動多個線程Thread thread1 = new Thread(task);Thread thread2 = new Thread(task);thread1.start();thread2.start();}
}//運行結果
//2122349303
//2122349303
在這個版本的代碼中,我們使用了雙重檢查鎖機制,首先檢查 instance
是否為 null
,如果是,才進入同步塊進行實例化。同時,使用 volatile
關鍵字修飾 instance
變量,以確保在多線程環境下對變量的可見性。
二者的區別
這兩種方式的主要區別在于性能和鎖的粒度:
-
synchronized 方法:
- 每次調用
getInstance
方法都會對整個方法進行加鎖,這會造成性能上的一些開銷,特別是在高并發情況下。 - 只有一個線程可以進入方法,其他線程必須等待該線程執行完畢,才能繼續執行。
- 這種方式簡單,不需要雙重檢查,但可能會對性能有一定的影響。
- 每次調用
-
雙重檢查鎖:
- 通過兩次檢查
instance
變量,第一次在無鎖的情況下進行,只有在instance
為null
時才會嘗試加鎖創建實例。 - 如果
instance
不為null
,就不需要進入同步塊,避免了每次都加鎖的性能開銷。 - 這種方式在第一次創建實例時才會加鎖,之后獲取實例時無需加鎖,可以減小鎖的粒度,提高了性能。
- 通過兩次檢查
綜合來看,如果您對性能要求較高且在多線程環境下使用單例模式,雙重檢查鎖可能是更好的選擇。如果您的應用程序不太關注性能,而且代碼簡單易懂,使用 synchronized 方法也可以保證線程安全。
阻塞式隊列
阻塞式隊列是一種特殊的隊列,也遵守“先進先出”原則
阻塞隊列是一種線程安全的數據結構,具有以下特征:
- 當隊列滿的時候,繼續入隊列就會阻塞,直到有其他線程從隊列中取走元素
- 當隊列空的時候,繼續出隊列也會阻塞,直到有其他線程往隊列中插入元素
阻塞隊列的一個典型應用場景就是“生產者消費者模型”,這是一種非常典型的開發模型。
生產者消費者模式
生產者-消費者模式是一種多線程協作的模式,用于解決一個線程生產數據,另一個線程消費數據的問題。這種模式可以有效地實現數據的異步傳遞和處理,同時也能夠控制資源的利用和線程的協調。
在生產者-消費者模式中,通常有以下角色:
-
生產者(Producer): 負責生成數據并將數據放入共享的緩沖區中,以供消費者使用。
-
消費者(Consumer): 從共享的緩沖區中獲取數據并進行處理。消費者在緩沖區為空時會等待,直到有新數據可用。
-
緩沖區(Buffer): 用于存儲生產者生成的數據,以及供消費者從中獲取數據。緩沖區應該是線程安全的數據結構。
生產者-消費者模式的目標是實現生產者和消費者之間的解耦,使它們能夠獨立運行,并在合適的時候協調工作。通過合理地控制生產者和消費者的速度,可以避免資源的浪費和線程的競爭問題。
示例:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;public class Demo9 {public static void main(String[] args) {BlockingQueue<Integer> buffer = new ArrayBlockingQueue<>(20);Thread producerThread = new Thread(()->{int value=0;while(true){try {buffer.put(value);System.out.println(Thread.currentThread().getName()+" "+value);value++;Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}},"生產者");Thread consumerThread = new Thread(()->{while (true){try {int value = buffer.take();System.out.println(Thread.currentThread().getName()+" "+value);} catch (InterruptedException e) {throw new RuntimeException(e);}}},"消費者");producerThread.start();consumerThread.start();}
}
//運行結果:
//生產者 0
//消費者 0
//生產者 1
//消費者 1
//生產者 2
//消費者 2
//消費者 3
//生產者 3
//生產者 4
//消費者 4
//...
?生產者線程生成數據并放入隊列,消費者線程從隊列中獲取數據。通過堵塞隊列,我們可以避免手動實現等待和通知機制,從而實現了線程間的協調。
堵塞隊列則提供了以下幾種常用的操作:
-
put(E element)
:將元素放入隊列中,如果隊列已滿,線程會被阻塞,直到隊列有空位。 -
take()
:從隊列中取出元素,如果隊列為空,線程會被阻塞,直到隊列有元素。 -
offer(E element, long timeout, TimeUnit unit)
:將元素放入隊列中,如果隊列已滿,在指定的時間內等待,如果仍然無法放入則返回特定值。 -
poll(long timeout, TimeUnit unit)
:從隊列中取出元素,如果隊列為空,在指定的時間內等待,如果仍然無法取出則返回特定值。?
?定時器
定時器是軟件開發中的一個重要的組件,類似一個鬧鐘,達到某個時間之后,就執行某個指定的代碼。
定時器式一種實際開發中非常常用的組件.
比如網絡通信中,如果對方500ms內沒有返回數據,則斷開連接嘗試重新連接.
比如一個Map,希望里面某個Key在3s之后自動刪除.
類似這樣的場景就需要用到定時器.
?標準庫中的定時器
- 標準庫中提供了一個Timer類,Timer類的核心方法為schedule
- schedule包含兩個參數,第一個參數指定即將要執行的任務代碼,第二個參數指定多長時間執行
public static void main(String[] args) {Timer timer = new Timer();timer.schedule(new TimerTask(){@Overridepublic void run() {System.out.println("hello");}},3000);
?自實現定時器
定時器的構成:
- 一個帶優先級的阻塞隊列
為啥要帶優先級呢? 因為阻塞隊列中的任務都有各自的執行時刻 (delay). 最先執行的任務一定是 delay 最小的. 使用帶 優先級的隊列就可以高效的把這個 delay 最小的任務找出來.
- 隊列中的每個元素是一個 Task 對象.
- Task 中帶有一個時間屬性, 隊首元素就是即將
- 同時有一個 worker 線程一直掃描隊首元素, 看隊首元素是否需要執行
public class Timer {static class Task implements Comparable<Task> {private Runnable command;private long time;public Task(Runnable command, long time) {this.command = command;// time 中存的是絕對時間, 超過這個時間的任務就應該被執行this.time = System.currentTimeMillis() + time;}public void run() {command.run();}@Overridepublic int compareTo(Task o) {// 誰的時間小誰排前面return (int)(time - o.time);}}// 核心結構private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue();// 存在的意義是避免 worker 線程出現忙等的情況private Object mailBox = new Object();class Worker extends Thread{@Overridepublic void run() {while (true) {try {Task task = queue.take();long curTime = System.currentTimeMillis();if (task.time > curTime) {// 時間還沒到, 就把任務再塞回去queue.put(task);synchronized (mailBox) {// 指定等待時間 waitmailBox.wait(task.time - curTime);}} else {// 時間到了, 可以執行任務task.run();}} catch (InterruptedException e) {e.printStackTrace();break;}}}}public Timer() {// 啟動 worker 線程Worker worker = new Worker();worker.start();}// schedule 原意為 "安排"public void schedule(Runnable command, long after) {Task task = new Task(command, after);queue.offer(task);synchronized (mailBox) {mailBox.notify();}}public static void main(String[] args) {Timer timer = new Timer();Runnable command = new Runnable() {@Overridepublic void run() {System.out.println("我來了");timer.schedule(this, 3000);}};timer.schedule(command, 3000);}
}
?線程池
線程池是一種用于管理和復用線程的機制,它能夠有效地管理線程的創建、銷毀以及復用,從而提高系統的性能和資源利用率。使用線程池可以避免頻繁地創建和銷毀線程,降低了線程創建的開銷,并且能夠更好地控制線程的數量,防止過多的線程造成系統資源的耗盡。
想象一下您是一個餐館的經理,而餐廳的服務員就像是線程,而顧客的訂單就是任務。每當一個顧客進來,您需要為他們提供服務,即執行任務。現在,您有兩種處理方式:
沒有線程池的情況:
- 顧客進來,您臨時雇傭一個服務員(創建一個新線程)。
- 顧客的訂單被處理后,您解雇服務員(銷毀線程)。
這種方式會導致一些問題:
- 每次都需要花時間和資源雇傭/解雇服務員,浪費了開銷。
- 如果顧客進來太多,可能導致服務員不夠,從而出現長時間等待的情況。
有線程池的情況:
- 您預先雇傭一批服務員(線程池中的線程)。
- 顧客進來,您指派一個空閑的服務員(從線程池中選取一個線程)來處理訂單。
- 訂單處理完畢后,服務員繼續待命,等待下一個顧客。
這種方式的優勢在于:
- 您不需要頻繁地雇傭/解雇服務員,節省了開銷。
- 線程池管理著一定數量的服務員,確保總是有服務員可用,避免等待時間。
總結來說,線程池就像是一個預先雇傭好的服務員團隊,可以高效地處理顧客的訂單,避免了頻繁創建和銷毀線程帶來的性能開銷,同時提供了更好的資源利用率和任務管理。這在處理多任務并發的情況下非常有用,例如網絡請求、后臺處理等場景。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Demo11 {public static void main(String[] args) {// 提交任務給線程池執行ExecutorService executor = Executors.newFixedThreadPool(3);// 提交任務給線程池執行for (int i = 1; i <= 5; i++) {int taskNumber = i;executor.execute(()->{System.out.println("Task " + taskNumber+ " 正在由線程池 "+ Thread.currentThread().getName()+"執行");});}// 關閉線程池executor.shutdown();}
}//運行結果:
//Task 1 正在由線程池 pool-1-thread-1執行
//Task 3 正在由線程池 pool-1-thread-3執行
//Task 2 正在由線程池 pool-1-thread-2執行
//Task 5 正在由線程池 pool-1-thread-3執行
//Task 4 正在由線程池 pool-1-thread-1執行
在這個例子中,我們首先創建了一個固定大小為3的線程池。然后,我們提交了5個任務給線程池執行。每個任務輸出自己的編號和執行線程的名稱。最后,我們關閉了線程池。
Executors創建線程池的幾種方式:
- newFixedThreadPool:創建固定的線程數的線程池.
- newCachedThreadPool:創建線程數動態增長的線程池.
- newSingleThreadExecutor:創建只包含單個線程的線程池.
- newScheduledThreadPool:設定延遲時間后執行命令或定期執行命令(可以理解成進階版的Timer).
Executors本質就是ThreadPoolExecutor類的封裝.
實現線程池
- 核心操作為 submit, 將任務加入線程池中
- 使用 Worker 類描述一個工作線程.
- 使用 Runnable 描述一個任務.
- 使用一個 BlockingQueue 組織所有的任務
- 每個 worker 線程要做的事情: 不停的從 BlockingQueue 中取任務并執行.
- 指定一下線程池中的最大線程數 maxWorkerCount; 當當前線程數超過這個最大值時, 就不再新增線程了.??
class Worker extends Thread {private LinkedBlockingQueue<Runnable> queue = null;public Worker(LinkedBlockingQueue<Runnable> queue) {super("worker");this.queue = queue;}@Overridepublic void run() {// try 必須放在 while 外頭, 或者 while 里頭應該影響不大try {while (!Thread.interrupted()) {Runnable runnable = queue.take();runnable.run();}} catch (InterruptedException e) {}}
}
public class MyThreadPool {private int maxWorkerCount = 10;private LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue();public void submit(Runnable command) {if (workerList.size() < maxWorkerCount) {// 當前 worker 數不足, 就繼續創建 workerWorker worker = new Worker(queue);worker.start();}// 將任務添加到任務隊列中queue.put(command);}public static void main(String[] args) throws InterruptedException {MyThreadPool myThreadPool = new MyThreadPool();myThreadPool.execute(new Runnable() {@Overridepublic void run() {System.out.println("吃飯");}});Thread.sleep(1000);}
}