詳細代碼在文章底部 目錄 "基礎概念" "進程與線程" "單線程與多線程" "實現線程的4中方式" "thread.start()和runnable.run()的區別" 和runnable.run()的區別) "Thread和Runnable的異同" "線程的基本操作" "線程的優先順序與守護線程" ...
詳細代碼在文章底部
目錄
- 基礎概念
- 實現線程的4中方式
- 線程的基本操作
- 線程的優先順序與守護線程
- synchronized關鍵字
- 實例鎖與全局鎖
- wait和notify
- 線程的讓步yeild
- 線程的休眠sleep
- Thread中的join
- 線程的中斷interrupt
- 線程的狀態與轉換
- 生產者消費者問題
- 鉤子線程
- 線程中的異常
基礎概念
進程與線程
進程(Process)
是電腦中的程式關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。 在當代面向線程設計的電腦結構中,進程是線程的容器。程式是指令、數據及其組織形式的描述,進程是程式的實體。是電腦中的程式關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。程式是指令、數據及其組織形式的描述,進程是程式的實體。進程之間通過TCP/IP的埠來實現相互交互。
線程(thread)
是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務,多個線程共用本進程的資源。線程的通信就比較簡單,有一大塊共用的記憶體,只要大家的指針是同一個就可以看到各自的記憶體。
小結
:
- 進程要分配一大部分的記憶體,而線程只需要分配一部分棧就可以了
- 一個程式至少有一個進程,一個進程至少有一個線程
- 進程是資源分配的最小單位,線程是程式執行的最小單位
- 一個線程可以創建和撤銷另一個線程,同一個進程中的多個線程之間可以併發執行
併發
: 對於單核cpu來說,多線程並不是同時進行的,操作系統將時間分成了多個時間片(按時間均分或按優先順序,JVM按優先順序),大概均勻的分配給線程,到達某個線程的時間段,該線程運行,其餘時間待命,這樣從微觀上看,一個線程是走走停停的,巨集觀感官上,在某一時刻似乎所有線程都在運行。併發是針對時間片段來說的,在某個時間段內多個線程處於runnable到running之間,但每個時刻只有一個線程在running,這叫做併發。
單線程與多線程
單線程就是進程中只有一個線程。單線程在程式執行時,所走的程式路徑按照連續順序排下來,前面的必須處理好,後面的才會執行。
// 單線程實例
public class SingleThread {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
多個線程組成的程式稱為多線程程式。常見的多線程程式如:GUI應用程式、I/O操作、網路容器等。
實現線程的4中方式
- 繼承Thread類
- 實現Runnable介面
- 實現Callable介面(有返回值)
- 通過線程池來實現ExecuteService
public class NewThreadThread {
public static void main(String[] args) {
new NewThread().start();
}
}
// 繼承自Thread類
class NewThread extends Thread {
@Override
public void run() {
System.out.println("Thread running");
}
}
public class NewThreadRunnable {
public static void main(String[] args) {
// run方法運行
new NewRunnable().run();
// start方法運行
NewRunnable newRunnable = new NewRunnable();
Thread thread = new Thread(newRunnable);
thread.start();
// 輸出
/**
* main running
* Thread-0 running
*/
}
}
// 實現runnable介面
class NewRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " running");
}
}
public class NewThreadCallable {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Callable newCallable = new NewCallable();
FutureTask futureTask = new FutureTask(newCallable);
Thread thread = new Thread(futureTask);
thread.start();
Object result = futureTask.get();
System.out.println(String.valueOf(result));
Callable<Integer> newCallable2 = new NewCallable2();
FutureTask<Integer> task = new FutureTask(newCallable2);
new Thread(task).start();
Integer i = task.get();
System.out.println(i);
}
}
// 實現Callable介面
class NewCallable implements Callable {
@Override
public Object call() throws Exception {
return "Hello World";
}
}
class NewCallable2 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 1;
}
}
public class NewThreadExecutorService {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
pool.shutdown();
}
}
Thread和Runnable的異同
- Thread 和 Runnable 的相同點:都是“多線程的實現方式”。
- Thread 和 Runnable 的不同點:
- Thread 是類,而Runnable是介面;Thread本身是實現了Runnable介面的類。我們知道“一個類只能有一個父類,但是卻能實現多個介面”,因此Runnable具有更好的擴展性。此外,Runnable還可以用於“資源的共用”。即,多個線程都是基於某一個Runnable對象建立的,它們會共用Runnable對象上的資源(變數)。通常,建議通過“Runnable”實現多線程!
thread.start()和runnable.run()的區別
Thread類繼承了Runnable介面,調用start()方法會啟動一個新的線程來執行相應的run()方法;run()方法和普通的成員方法一樣,可以被重覆調用,會在當前線程中執行該方法,而不會啟動新的線程。(參考上面通過實現Runnable介面來實現新線程)
線程的優先順序與守護線程
線程的優先順序
Java中的線程優先順序的範圍是1~10,預設的優先順序是5,10極最高。線程的優先順序具有以下特性:
概率性
: “高優先順序線程”被分配CPU的概率高於“低優先順序線程”
隨機性
: 根據時間片輪循調度,能夠併發執行,無論是是級別相同還是不同,線程調用都不會絕對按照優先順序執行,每次執行結果都不一樣,調度演算法無規律可循,所以線程之間不能有先後依賴關係。無時間片輪循機制時,高級別的線程優先執行,如果低級別的線程正在運行時,有高級別線程可運行狀態,則會執行完低級別線程,再去執行高級別線程。如果低級別線程處於等待、睡眠、阻塞狀態,或者調用yield()函數讓當前運行線程回到可運行狀態,以允許具有相同優先順序或者高級別的其他線程獲得運行機會。因此,使用yield()的目的是讓相同優先順序的線程之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因為讓步的線程還有可能被線程調度程式再次選中。結論:yield()從未導致線程轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致線程從運行狀態轉到可運行狀態,但有可能沒有效果。
用戶線程與守護線程
在Java中有兩類線程:User Thread(用戶線程)、Daemon Thread(守護線程)
用個比較通俗的比如,任何一個守護線程都是整個JVM中所有非守護線程的保姆。只要當前JVM實例中尚存在任何一個非守護線程沒有結束,守護線程就全部工作;只有當最後一個非守護線程結束時,守護線程隨著JVM一同結束工作。Daemon的作用是為其他線程的運行提供便利服務,守護線程最典型的應用就是GC (垃圾回收器)
,它就是一個很稱職的守護者。User和Daemon兩者幾乎沒有區別,唯一的不同之處就在於虛擬機的離開:如果 User Thread已經全部退出運行了,只剩下Daemon Thread存在了,虛擬機也就退出了。 因為沒有了被守護者,Daemon也就沒有工作可做了,也就沒有繼續運行程式的必要了。
值得一提的是,守護線程並非只有虛擬機內部提供,用戶在編寫程式時也可以自己設置守護線程。下麵的方法就是用來設置守護線程的。
// 設定 daemonThread 為 守護線程,default false(非守護線程)
daemonThread.setDaemon(true);
// 驗證當前線程是否為守護線程,返回 true 則為守護線程
daemonThread.isDaemon();
這裡有幾點需要註意:
- thread.setDaemon(true)必須在thread.start()之前設置,否則會跑出一個IllegalThreadStateException異常。你不能把正在運行的常規線程設置為守護線程。
- 在Daemon線程中產生的新線程也是Daemon的。
- 不要認為所有的應用都可以分配給Daemon來進行服務,比如讀寫操作或者計算邏輯。
那麼守護線程的作用是什麼?
舉例, GC垃圾回收線程:就是一個經典的守護線程,當我們的程式中不再有任何運行的Thread,程式就不會再產生垃圾,垃圾回收器也就無事可做,所以當垃圾回收線程是JVM上僅剩的線程時,垃圾回收線程會自動離開。它始終在低級別的狀態中運行,用於實時監控和管理系統中的可回收資源。
應用場景:(1)來為其它線程提供服務支持的情況;(2) 或者在任何情況下,程式結束時,這個線程必須正常且立刻關閉,就可以作為守護線程來使用;反之,如果一個正在執行某個操作的線程必須要正確地關閉掉否則就會出現不好的後果的話,那麼這個線程就不能是守護線程,而是用戶線程。通常都是些關鍵的事務,比方說,資料庫錄入或者更新,這些操作都是不能中斷的。
JVM 程式在什麼情況下能夠正常退出?
The Java Virtual Machine exits when the only threads running are all daemon threads.
上面這句話來自 JDK 官方文檔,意思是:如果 JVM 中沒有一個正在運行的非守護線程,這個時候,JVM 會退出。換句話說,守護線程擁有自動結束自己生命周期的特性,而非守護線程不具備這個特點
線程的基本操作
- thread.start()線程啟動運行
- thread.run()在當前線程中運行run方法
- Thread.currentThread()獲取當前線程,getName()獲取名字
synchronized關鍵字
synchronized方法
public synchronized void foo() {
System.out.println("synchronized methoed");
}
synchronized代碼塊
public void foo() {
synchronized (this) {
System.out.println("synchronized methoed");
}
}
- synchronized代碼塊中的this是指當前對象。也可以將this替換成其他對象,例如將this替換成obj,則foo2()在執行synchronized(obj)時就獲取的是obj的同步鎖。
- synchronized代碼塊中XXClass.class是指這個類,新建多個實例來訪問同步方法或同步代碼塊也會被阻塞
synchronized代碼塊可以更精確的控制衝突限制訪問區域,有時候表現更高效率。
synchronized關鍵字使用原則
- 當一個線程訪問一個對象的synchronized方法或者synchronized代碼塊時,其他線程對
該對象
的該synchronized方法或者synchronized代碼塊的訪問將被阻塞。 - 當一個線程訪問一個對象的synchronized方法或者synchronized代碼塊時,其他線程對
該對象
的非synchronized方法的訪問將不會被阻塞。 - 當一個線程訪問一個對象的synchronized方法或者synchronized代碼塊時,其他線程對
該對象
的其他synchronized方法或代碼塊的訪問將會被阻塞。
synchronized底層原理
- 同步代碼塊:代碼反編譯後在代碼塊的前面會加上monitorenter,在代碼塊的最後加上monitorexit。
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("Method 1 start");
}
}
}
每個對象有一個監視器鎖(monitor)。當monitor被占用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的所有權,過程:
- 如果monitor的進入數為0,則該線程進入monitor,然後將進入數設置為1,該線程即為monitor的所有者。
- 如果線程已經占有該monitor,只是重新進入,則進入monitor的進入數加1。
- 如果其他線程已經占用了monitor,則該線程進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權。
執行monitorexit的線程必須是objectref所對應的monitor的所有者。
指令執行時,monitor的進入數減1,如果減1後進入數為0,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去獲取這個 monitor 的所有權。
通過這兩段描述,我們應該能很清楚的看出Synchronized的實現原理,Synchronized的語義底層是通過一個monitor的對象來完成,其實wait/notify等方法也依賴於monitor對象,這就是為什麼只有在同步的塊或者方法中才能調用wait/notify等方法,否則會拋出java.lang.IllegalMonitorStateException的異常的原因。
- 同步方法:
public class SynchronizedMethod {
public synchronized void method() {
System.out.println("Hello World!");
}
}
從反編譯的結果來看,方法的同步並沒有通過指令monitorenter和monitorexit來完成(理論上其實也可以通過這兩條指令來實現),不過相對於普通方法,其常量池中多了ACC_SYNCHRONIZED標示符。JVM就是根據該標示符來實現方法的同步的:當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,如果設置了,執行線程將先獲取monitor,獲取成功之後才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其他任何線程都無法再獲得同一個monitor對象。 其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需通過位元組碼來完成。
實例鎖與全局鎖
實例鎖:鎖在某一個實例對象上。如果該類是單例,那麼該鎖也具有全局鎖的概念。實例鎖對應的就是synchronized關鍵字。
synchronized(this) synchronized(obj)
public synchronized void foo()
全局鎖:該鎖針對的是類,無論實例多少個對象,那麼線程都共用該鎖。全局鎖對應的就是static synchronized(或者是鎖在該類的class或者classloader對象上)。
synchronized(XXXClass.class)
public static synchronized void foo()
例子
pulbic class Something {
public synchronized void isSyncA(){}
public synchronized void isSyncB(){}
public static synchronized void cSyncA(){}
public static synchronized void cSyncB(){}
}
假設,Something有兩個實例x和y。分析下麵4組表達式獲取的鎖的情況。
- x.isSyncA()與x.isSyncB() 不能同時訪問。實例鎖,訪問兩個同步方法的對象是同一個對象x
- x.isSyncA()與y.isSyncA() 能同時訪問。實例鎖,訪問同一個同步方法的對象是兩個不同的對象,實例鎖不是同一個
- x.cSyncA()與y.cSyncB() 不能同時訪問。因為cSyncA()和cSyncB()都是static類型,x.cSyncA()相當於Something.isSyncA(),y.cSyncB()相當於Something.isSyncB(),因此它們共用一個同步鎖,不能被同時反問。
- x.isSyncA()與Something.cSyncA() 可以被同時訪問。因為isSyncA()是實例方法,x.isSyncA()使用的是對象x的鎖;而cSyncA()是靜態方法,Something.cSyncA()可以理解對使用的是“類的鎖”。因此,它們是可以被同時訪問的。
public class SynchronizedLockExample {
public static void main(String[] args) {
SynchronizedLock x = new SynchronizedLock();
// x.syncA()與x.syncB()
new Thread(()-> {
try {
x.syncA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Threadx ").start();
new Thread(()-> {
try {
x.syncB();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Thready ").start();
/** 實例鎖。不能同時訪問
* Threadx 0
* Threadx 1
* Threadx 2
* Thready 0
* Thready 1
* Thready 2
*/
// x.syncA()與y.syncA()
SynchronizedLock y = new SynchronizedLock();
SynchronizedLock y2 = new SynchronizedLock();
new Thread(() -> {
try {
y.syncA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Thready1").start();
new Thread(() -> {
try {
y2.syncA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Thready2").start();
/**實例鎖。可以同時訪問,實例不是同一個對象鎖
* Thready10
* Thready20
* Thready21
* Thready11
* Thready22
* Thready12
*/
// x.syncC()與y.syncD()
SynchronizedLock x1 = new SynchronizedLock();
SynchronizedLock y3 = new SynchronizedLock();
new Thread(()-> {
try {
x1.syncC();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Threadx1 ").start();
new Thread(()-> {
try {
y3.syncD();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Thready3 ").start();
/** 全局鎖。不能同時訪問,static synchronized修飾的方法是全局靜態的,與實例無關
* Threadx1 0
* Threadx1 1
* Threadx1 2
* Thready3 0
* Thready3 1
* Thready3 2
*/
// x.syncA與SynchronizedLock.syncD
SynchronizedLock x3 = new SynchronizedLock();
new Thread(()-> {
try {
x3.syncA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Theradx3").start();
new Thread(() -> {
try {
SynchronizedLock.syncD();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Threadstatic ").start();
/** 可以同時訪問。x.syncA是實例鎖,SynchronizedLock.syncD是全局鎖
* Theradx30
* Threadstatic 0
* Theradx31
* Threadstatic 1
* Theradx32
* Threadstatic 2
*/
}
}
class SynchronizedLock {
public synchronized void syncA() throws InterruptedException {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + i);
Thread.sleep(1000);
}
}
public synchronized void syncB() throws InterruptedException {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + i);
Thread.sleep(1000);
}
}
public static synchronized void syncC() throws InterruptedException {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + i);
Thread.sleep(1000);
}
}
public static synchronized void syncD() throws InterruptedException {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + i);
Thread.sleep(1000);
}
}
}
wait和notify
wait, notify, notifyAll
在Object.java中,定義了wait(), notify()和notifyAll()等介面。wait()的作用是讓當前線程進入等待狀態,同時,wait()也會讓當前線程釋放它所持有的鎖。而notify()和notifyAll()的作用,則是喚醒當前對象上的等待線程;notify()是喚醒單個線程,而notifyAll()是喚醒所有的線程。
Object類中關於等待/喚醒的API詳細信息如下:
- notify() -- 喚醒在此對象監視器上等待的單個線程。
- notifyAll() -- 喚醒在此對象監視器上等待的所有線程。
- wait() -- 讓當前線程處於“等待(阻塞)狀態”,“直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法”,當前線程被喚醒(進入“就緒狀態”)。
- wait(long timeout) -- 讓當前線程處於“等待(阻塞)狀態”,“直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量”,當前線程被喚醒(進入“就緒狀態”)。
- wait(long timeout, int nanos) -- 讓當前線程處於“等待(阻塞)狀態”,“直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法,或者其他某個線程中斷當前線程,或者已超過某個實際時間量”,當前線程被喚醒(進入“就緒狀態”)。
public class NotifyExample {
public static void main(String[] args) {
Notify notify = new Notify();
new Thread(()-> {
synchronized (notify) {
while (notify.flag) {
System.out.println("User A");
try {
notify.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
notify.call();
synchronized (notify) {
notify.notifyAll();
}
}, "User A").start();
new Thread(()-> {
synchronized (notify) {
while (notify.flag) {
System.out.println("User B");
try {
notify.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
notify.call();
synchronized (notify) {
notify.notifyAll();
}
}, "User B").start();
/**
* Begin to call
* User A calling 0%
* User B
* User A calling 50%
* User A calling 100%
* End to call
* Begin to call
* User B calling 0%
* User B calling 50%
* User B calling 100%
* End to call
*/
}
}
class Notify {
public boolean flag = false;
public void call() {
flag = true;
System.out.println("Begin to call");
for (int i = 0; i < 101; i+=50) {
System.out.println(Thread.currentThread().getName() + " calling " + i + "%");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("End to call");
flag = false;
}
}
註意事項
- “當前線程”在調用wait()時,必須擁有該對象的同步鎖。該線程調用wait()之後,會釋放該鎖;然後一直等待直到“其它線程”調用對象的同步鎖的notify()或notifyAll()方法。然後,該線程繼續等待直到它重新獲取“該對象的同步鎖”,然後就可以接著運行。, synchronized(obj),否則會出現
java.lang.IllegalMonitorStateException
- 調用notify時也需要獲得該對象的“同步鎖”,jdk中的註釋:
This method should only be called by a thread that is the owner of this object's monitor. A thread becomes the owner of the object's monitor in one of three ways:
1. By executing a synchronized instance method of that object. 通過獲得該對象的同步鎖
2. By executing the body of a {@code synchronized} statement that synchronizes on the object. 在該對象的同步代碼塊中執行
3. For objects of type {@code Class,} by executing a synchronized static method of that class. 通過執行全局鎖的方法
- Only one thread at a time can own an object's monitor.
為什麼notify(), wait()等函數定義在Object中,而不是Thread中
Object中的wait(), notify()等函數,和synchronized一樣,會對“對象的同步鎖”進行操作。
wait()會使“當前線程”等待,因為線程進入等待狀態,所以線程應該釋放它鎖持有的“同步鎖”,否則其它線程獲取不到該“同步鎖”而無法運行!
OK,線程調用wait()之後,會釋放它鎖持有的“同步鎖”;而且,根據前面的介紹,我們知道:等待線程可以被notify()或notifyAll()喚醒。現在,請思考一個問題:notify()是依據什麼喚醒等待線程的?或者說,wait()等待線程和notify()之間是通過什麼關聯起來的?答案是:依據“對象的同步鎖”。
負責喚醒等待線程的那個線程(我們稱為“喚醒線程”),它只有在獲取“該對象的同步鎖”(這裡的同步鎖必須和等待線程的同步鎖是同一個),並且調用notify()或notifyAll()方法之後,才能喚醒等待線程。雖然,等待線程被喚醒;但是,它不能立刻執行,因為喚醒線程還持有“該對象的同步鎖”。必須等到喚醒線程釋放了“對象的同步鎖”之後,等待線程才能獲取到“對象的同步鎖”進而繼續運行。
總之,notify(), wait()依賴於“同步鎖”,而“同步鎖”是對象鎖持有,並且每個對象有且僅有一個!這就是為什麼notify(), wait()等函數定義在Object類,而不是Thread類中的原因。
線程的讓步yeild
- Yield是一個靜態的原生(native)方法
- Yield告訴當前正在執行的線程把運行機會交給線程池中擁有相同優先順序的線程。
- Yield不能保證使得當前正在運行的線程迅速轉換到可運行的狀態,它僅能使一個線程從運行狀態轉到可運行狀態,而不是等待或阻塞狀態
public class YieldExample {
public static void main(String[] args) {
Yeild yeild = new Yeild();
new Thread(yeild::call, "yeild1 ").start();
new Thread(yeild::call, "yeild2 ").start();
new Thread(yeild::call, "yeild3 ").start();
new Thread(yeild::call, "yeild4 ").start();
new Thread(yeild::call, "yeild5 ").start();
new Thread(yeild::call, "yeild6 ").start();
new Thread(yeild::call, "yeild7 ").start();
new Thread(yeild::call, "yeild8 ").start();
new Thread(yeild::call, "yeild9 ").start();
new Thread(yeild::call, "yeild0 ").start();
}
}
class Yeild {
public synchronized void call() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + i);
if(i % 4 == 0) {
Thread.yield();
}
}
}
}
yeild和wait的區別
wait()的作用是讓當前線程由“運行狀態”進入“等待(阻塞)狀態”的同時,也會釋放同步鎖。而yield()的作用是讓步,它也會讓當前線程離開“運行狀態”。它們的區別是:
- wait()是讓線程由“運行狀態”進入到“等待(阻塞)狀態”,而yield()是讓線程由“運行狀態”進入到“就緒狀態”。
- wait()是會線程釋放它所持有對象的同步鎖,而yield()方法不會釋放鎖。
線程的休眠sleep
sleep() 定義在Thread.java中。
sleep() 的作用是讓當前線程休眠,即當前線程會從“運行狀態”進入到“休眠(阻塞)狀態”。sleep()會指定休眠時間,線程休眠的時間會大於/等於該休眠時間;線上程重新被喚醒時,它會由“阻塞狀態”變成“就緒狀態”,從而等待cpu的調度執行。
sleep() 與 wait()的比較
我們知道,wait()的作用是讓當前線程由“運行狀態”進入“等待(阻塞)狀態”的同時,也會釋放同步鎖。而sleep()的作用是也是讓當前線程由“運行狀態”進入到“休眠(阻塞)狀態”。但是,wait()會釋放對象的同步鎖,而sleep()則不會釋放鎖。
Thread中的join
When we call this method using a thread object, it suspends the execution of the calling thread until the object called finishes its execution.
當我們調用某個線程的這個方法時,這個方法會掛起調用線程,直到被調用線程結束執行,調用線程才會繼續執行。
join() 一共有三個重載版本,分別是無參、一個參數、兩個參數:
public final void join() throws InterruptedException;
public final synchronized void join(long millis) throws InterruptedException;
public final synchronized void join(long millis, int nanos) throws InterruptedException;
源碼分析
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);// 此處調用wait會讓當前線程從運行狀態變為阻塞狀態,並讓出對象鎖
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
小結
- 三個方法都被final修飾,無法被子類重寫。
- join(long), join(long, long) 是synchronized method,同步的對象是當前線程實例。
- 無參版本和兩個參數版本最終都調用了一個參數的版本。join() 和 join(0) 是等價的,表示一直等下去;join(非0)表示等待一段時間。
- 從源碼可以看到 join(0) 調用了Object.wait(0),其中Object.wait(0) 會一直等待,直到被notify/中斷才返回。
while(isAlive())是為了防止子線程偽喚醒(spurious wakeup),只要子線程沒有TERMINATED的,父線程就需要繼續等下去。 - join() 和 sleep() 一樣,可以被中斷(被中斷時,會拋出 InterrupptedException 異常);不同的是,join() 內部調用了 wait(),會出讓鎖,而 sleep() 會一直保持鎖。
線程的中斷interrupted
interrupt()的作用是中斷本線程。
本線程中斷自己是被允許的;其它線程調用本線程的interrupt()方法時,會通過checkAccess()檢查許可權。這有可能拋出SecurityException異常。
- 終止處於“阻塞狀態”的線程: 通過“中斷”方式終止處於“阻塞狀態”的線程。當線程由於被調用了sleep(), wait(), join()等方法而進入阻塞狀態;若此時調用線程的interrupt()將線程的中斷標記設為true。由於處於阻塞狀態,中斷標記會被清除,同時產生一個InterruptedException異常。將InterruptedException放在適當的為止就能終止線程。
@Override
public void run() {
while (true) {
try {
// 執行任務...
} catch (InterruptedException ie) {
// InterruptedException在while(true)迴圈體內。
// 當線程產生了InterruptedException異常時,while(true)仍能繼續運行!需要手動退出
break;
}
}
}
}
說明:在while(true)中不斷的執行任務,當線程處於阻塞狀態時,調用線程的interrupt()產生InterruptedException中斷。中斷的捕獲在while(true)之外,這樣就退出了while(true)迴圈!
InterruptedException異常的捕獲在whle(true)之內。當產生InterruptedException異常時,被catch處理之外,仍然在while(true)迴圈體內;要退出while(true)迴圈體,需要額外的執行退出while(true)的操作。thread在“等待(阻塞)狀態”時,被interrupt()中斷;此時,會清除中斷標記(即isInterrupted()會返回false),而且會拋出InterruptedException異常(該異常在while迴圈體內被捕獲)
線程的狀態與轉換
線程共包括以下5種狀態。
- 新建狀態(New): 創建了線程對象但尚未調用start()方法時的狀態。
- 就緒狀態(Runnable): 也被稱為“可執行狀態”。線程對象被創建後,其它線程調用了該對象的start()方法,從而來啟動該線程。例如,thread.start()。處於就緒狀態的線程,隨時可能被CPU調度執行。
- 阻塞狀態(Blocked): 阻塞狀態是線程因為某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,才有機會轉到運行狀態。阻塞的情況分三種: (01) 等待阻塞 -- 通過調用線程的wait()方法,讓線程等待某工作的完成。 (02) 同步阻塞 -- 線程在獲取synchronized同步鎖失敗(因為鎖被其它線程所占用),它會進入同步阻塞狀態。 (03) 其他阻塞 -- 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
- 等待狀態(Waiting):線程處於等待狀態,處於該狀態標識的當前線程需要等待其他線程做出一些特定的操作來喚醒自己。
- 超時等待狀態(Time Waiting):超時等待狀態,與Waiting不同,在等待指定的時間後會自行返回。
- 終止狀態(Terminated): 線程執行完了或者因異常退出了run()方法,該線程結束生命周期。
補充說明
- Thread.sleep(long millis),一定是當前線程調用此方法,當前線程進入TIMED_WAITING狀態,但不釋放對象鎖,millis後線程自動蘇醒進入就緒狀態。作用:給其它線程執行機會的最佳方式。
- Thread.yield(),一定是當前線程調用此方法,當前線程放棄獲取的CPU時間片,但不釋放鎖資源,由運行狀態變為就緒狀態,讓OS再次選擇線程。作用:讓相同優先順序的線程輪流執行,但並不保證一定會輪流執行。實際中無法保證yield()達到讓步目的,因為讓步的線程還有可能被線程調度程式再次選中。Thread.yield()不會導致阻塞。該方法與sleep()類似,只是不能由用戶指定暫停多長時間。
- t.join()/t.join(long millis),當前線程里調用其它線程t的join方法,當前線程進入WAITING/TIMED_WAITING狀態,當前線程不會釋放已經持有的對象鎖。線程t執行完畢或者millis時間到,當前線程進入就緒狀態。
- obj.wait(),當前線程調用對象的wait()方法,當前線程釋放對象鎖,進入等待隊列。依靠notify()/notifyAll()喚醒或者wait(long timeout) timeout時間到自動喚醒。
- obj.notify()喚醒在此對象監視器上等待的單個線程,選擇是任意性的。notifyAll()喚醒在此對象監視器上等待的所有線程。
生產者消費者問題
所謂的生產者消費者模型,是通過一個容器來解決生產者和消費者的強耦合問題。通俗的講,就是生產者在不斷的生產,消費者也在不斷的消費,可是消費者消費的產品是生產者生產的,這就必然存在一個中間容器,我們可以把這個容器想象成是一個貨架,當貨架空的時候,生產者要生產產品,此時消費者在等待生產者往貨架上生產產品,而當貨架滿的時候,消費者可以從貨架上拿走商品,生產者此時等待貨架的空位,這樣不斷的迴圈。那麼在這個過程中,生產者和消費者是不直接接觸的,所謂的‘貨架’其實就是一個阻塞隊列,生產者生產的產品不直接給消費者消費,而是仍給阻塞隊列,這個阻塞隊列就是來解決生產者消費者的強耦合的。就是生產者消費者模型。
public class Product {
private int size;
private final int capactiy = 100;
public synchronized void product(int n) {
while(size >= capactiy) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
size+=n;
System.out.println(Thread.currentThread().getName() + "生產" + n + "個,剩餘" + size);
notifyAll();
}
public synchronized void consume(int n) {
while ((size - n) < 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
size-=n;
System.out.println(Thread.currentThread().getName() + "消費" + n + "個,剩餘" + size);
notifyAll();
}
}
鉤子線程
線上上Java程式中經常遇到進程程掛掉,一些狀態沒有正確的保存下來,這時候就需要在JVM關掉的時候執行一些清理現場的代碼。Java中得ShutdownHook提供了比較好的方案。
JDK在1.3之後提供了Java Runtime.addShutdownHook(Thread hook)方法,可以註冊一個JVM關閉的鉤子,這個鉤子可以在以下幾種場景被調用:
- 程式正常退出
- 使用System.exit()
- 終端使用Ctrl+C觸發的中斷
- 系統關閉
- 使用Kill pid命令幹掉進程
註:在使用kill -9 pid是不會JVM註冊的鉤子不會被調用。
在JDK中方法的聲明:
public void addShutdownHook(Thread hook) 參數 hook – 一個初始化但尚未啟動的線程對象,註冊到JVM鉤子的運行代碼。
public static void main(String[] args) throws InterruptedException {
System.out.println("Begin to run");
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hook");
}
}));
Scanner scanner = new Scanner(System.in);
// 按Ctrl + x會調用hook thread,idea中按了ctrl+d
System.out.println(scanner.next());
Thread.sleep(10000);
// 執行系統推出會調用hook thread
System.exit(0);
System.out.println("End to run");
// 系統正常退出會調用hook thread
/**
* egin to run
* ^D
* Hook
* Exception in thread "main" java.util.NoSuchElementException
* at java.util.Scanner.throwFor(Scanner.java:862)
* at java.util.Scanner.next(Scanner.java:1371)
* at com.zhonghuasheng.basic.java.thread.hook.HookThreadExample.main(HookThreadExample.java:16)
* ^D
*/
}
}
線程中的異常
異常分為checked exception和unchecked exception。
- checked exception: 線上程中遇到checked exception,我們可以直接catch住,然後處理。
- unchecked exception: 可以通過thread.setUncaughtExceptionHandler來捕獲
public class UncaughtExceptionExample {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 2; i > -1 ; i--) {
System.out.println(10 / i);
}
}
}, "ThreadA ");
thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("Caught exception in thread " + t.getName());
System.out.println("Exception is " + e.getMessage());
}
});
thread.start();
/**直接運行的結果
* 5
* Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
* 10
* at com.zhonghuasheng.basic.java.thread.exception.UncaughtExceptionExample$1.run(UncaughtExceptionExample.java:10)
* at java.lang.Thread.run(Thread.java:745)
** 加了UncaughtExceptionHandler運行的結果
* 5
* 10
* Caught exception in thread ThreadA
* Exception is / by zero
*
* Process finished with exit code 0
*/
}
}
引用
- https://www.cnblogs.com/skywang12345/
- https://www.cnblogs.com/walixiansheng/p/9588603.html
- https://segmentfault.com/u/niteip/articles?sort=vote
- https://www.cnblogs.com/qq1290511257/p/10645106.html
- https://www.cnblogs.com/developer_chan/p/10391365.html
- https://www.exception.site/java-concurrency/java-concurrency-hook-thread