文章目錄
-
-
- 線程建立
- 線程通知與等待
-
- 虛假喚醒
- wait()、wait(long timeout)、wait(long timeout,int nanous)函數
- notify()、notifyAll()函數
- 等待線程執行終止的join方法
- 讓線程睡眠的sleep方法
-
- Sleep方法與wait方法差別
- 讓出CPU執行權的yield方法
-
- sleep()與yield()差別
- 線程中斷
- 線程死鎖
-
- 為什麼會産生死鎖??
- 如何避免線程死鎖?
- 守護線程與使用者線程
- 線程組和線程優先級
-
- 線程組
- 線程的優先級
- 線程組的常用方法及資料結構
- ThreadLocal
-
線程建立
線程建立有三種方式:
- 實作Runnable接口的run方法
- 繼承Thread類并重寫run方法
- 實作Callable接口,使用FutureTask方式
繼承Thread類并重寫run方法
package threadtxt;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadTest {
/***
* 繼承Thread 類并重寫run方法
*/
public static class MyThread extends Thread{
@Override
public void run() {
System.out.println("i am a child thread");
}
}
public static void main(String[] args) {
// 建立線程
MyThread myThread = new MyThread();
// 啟動線程
myThread.start();
}
}
注意點:
當線程建立完thread對象後該線程并沒有啟動,直到調用了start方法才真正的啟動了線程。調用start方法後線程沒有馬上執行,而是進入了就緒狀态,這個時候已經擷取了除CPU資源外的其他資源,等待擷取CPU資源後才會真正處于運作狀态。run方法執行完畢之後,該線程就處于終止狀态。
具體可以參考 : Java多線程:概念
實作Runnable接口的run方法
package threadtxt;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadTest {
/***
* 實作Runnable接口
*/
public static class RunableTask implements Runnable{
@Override
public void run() {
System.out.println("i am a child thread");
}
}
public static void main(String[] args) {
RunableTask runableTask = new RunableTask();
new Thread(runableTask).start();
new Thread(runableTask).start();
}
}
實作Callable接口,使用FutureTask方式
package threadtxt;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadTest {
/***
* 使用FutureTask方式實作
*/
public static class CallerTask implements Callable<String>{
@Override
public String call() throws Exception {
return "hello";
}
}
public static void main(String[] args) {
// 建立異步任務
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
// 建立線程并啟動
new Thread(futureTask).start();
try {
// 等待任務執行完畢,傳回結果
String rtn = futureTask.get();
System.out.println(rtn);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
知識點
在程式裡面調用start方法後,虛拟機會先為我們建立一個線程,然後等到這個線程第一次得到時間片時再調用run()方法。但是不可多次調用start方法,在第一次調用start方法後,再次調用start方法會抛出異常,因為start方法中在使用前會先判斷threadStatus變量是否為0,如果不為0則抛出異常
線程通知與等待
Java多線程的等待/通知機制是基于Object類的wait()方法和notify()、notifyAll()方法來實作。
notify()方法會随機喚醒一個正在等待的線程,而notifyAll()會叫醒所有正在等待的線程
線程調用wait方法時,該線程就會被阻塞挂起,直到發生下面幾件才會傳回:
- 其他線程調用了該共享對象的notify()或者notifyAll()方法
- 其他線程調用了該線程的interrupt()方法,該線程抛出InterruptedException異常傳回
知識點
調用wait方法的線程沒有事先擷取該對象的螢幕鎖,則調用wait方法時,會抛出illegalMonitorStateException異常
問題1:一個線程如何才能擷取一個共享變量的螢幕鎖?
(1) 執行synchronized同步代碼塊時,使用該共享變量作為參數
synchronized(共享變量){
//業務邏輯
}
(2)調用該共享變量的方法,并且該犯法使用了synchronized修飾
synchronized void add(int a,int b){
//業務邏輯
}
虛假喚醒
一個線程從挂起狀态變為運作狀态(被喚醒),即使該線程沒有被其他線程調用notify()、notifyAll()方法進行通知,或者是被中斷、或者等待逾時
問題1:怎麼防止虛假喚醒?
不停的去測試該線程被喚醒的條件是否滿足,不滿足則繼續等待,即是在一個循環中調用wait方法進行防範。退出循環的條件是滿足了喚醒該線程的條件。
synchronized(obj){
while(條件是否滿足){
obj.wait();
}
}
以上代碼中的while()循環,則不能用if代替,如果使用if則隻會判斷一次,當下次條件不滿足時 ,線程也會喚醒。是以等待應該出現在循環當中
一般并發線程操作的步驟:
- 判斷等待
- 業務操作
- 通知
知識點
目前線程調用共享變量wait方法後隻會釋放目前變量上的鎖,如果目前線程還持有其他共享變量鎖,則這些鎖是不會被釋放的。
wait()、wait(long timeout)、wait(long timeout,int nanous)函數
wait(long timeout)相比wait()方法多了一個逾時參數,如果一個線程調用共享對象的該方法挂起後,沒有在指定的timeout ms 時間内被其他線程調用該共享變量的notify()或者notifyAll()方法喚醒,那麼該函數還是會因為逾時而傳回。
知識點
wait(0)方法和wait()方法效果一樣,因為在wait方法内部就是調用了wait(0)。需要注意的是,如果在調用函數時,傳遞了一個負的timeout則會抛出illegalArgumentException異常。
notify()、notifyAll()函數
線程調用notify()方法後,會喚醒一個在該共享變量上調用wait系列方法後被挂起的線程。
知識點
- 一個共享變量上可能會有多個線程在等待,具體喚醒哪個等待的線程是随機的。
- 被喚醒的線程不能馬上從wait方法傳回并繼續執行,它必須在擷取了共享對象的螢幕鎖後才可以傳回,也就是喚醒它的線程釋放了共享變量上的螢幕鎖後,被喚醒的線程也不一定會擷取到該共享對象的螢幕鎖,因為其他線程會和目前線程一起競争該鎖,隻有該線程競争到了共享變量的螢幕鎖後才可以繼續執行。
- 隻有目前線程擷取到了共享變量的螢幕鎖後,才可以調用該共享變量的notify方法,否會抛出illegalMontitorStateException異常。
notifyAll方法則會喚醒所有在該共享變量上由于調用wait系列方法而被挂起的線程。
等待線程執行終止的join方法
怎麼快速了解下線程中join()方法???
想象一下,你現在在排隊買奶茶,快要到你的時候,突然來個非常漂亮的妹子說:帥哥,可以讓我先買嗎?(這個時候想,長得還可以,讓你先買吧,等你買完就到我了,說不定讓你先買,還能留個微信号,交個朋友),然後你說:可以啊,于是妹子站在你前面買奶茶,可是這個時候,她的七大姑八大姨都來了,都排在那個妹子前面,你從前面的第三位,直接變成第N位。這個時候隻能感歎,早知道我就先買了。線程中的join方法就是這個道理了。讓其他的線程先執行完畢後,然後自己在執行操作。
舉個栗子
package threadtxt2;
import java.util.concurrent.TimeUnit;
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
Thread tA = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(" child tA over!!! ");
});
Thread tB = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(" child tB over!!! ");
});
tA.start();
tB.start();
System.out.println("wait all child");
//等待子線程執行完畢,傳回
tA.join();
tB.join();
System.out.println("over all child");
}
}
傳回結果
wait all child
child tB over!!!
child tA over!!!
over all child
知識點
join()方法是Thread 類的一個執行個體方法。它的作用是讓目前線程陷入“等待”狀态,等待join的這個線程執行完成後,再繼續執行目前線程。
有時候,主線程建立并啟動了子線程,如果子線程需要進行大量的耗時運算,主線程往往将早于子線程結束之前結束。
如果主線程想等待子線程執行完畢後,獲得子線程中的處理完的某個資料,就要用到join方法
注意點
join()方法由兩個重載方法,一個是join(long),一個是join(long,int)。
實際上,通過源碼發現,join()方法及其重載方法底層都是利用了wait(long timeout)這個方法
對于join(long,int),通過檢視源碼(JDK1.8)發現,底層并沒有精确到納秒,而是對第二個參數做了簡單的判斷和處理
讓線程睡眠的sleep方法
當一個執行中的線程調用了Thread的sleep方法後,調用線程會暫時讓出指定時間的執行權,也就是在這期間不參與CPU的排程,但是該線程所擁有的螢幕資源,比如鎖還是持有狀态。
指定的睡眠時間到了後該函數就會正常傳回,線程處于就緒狀态,然後參與CPU排程,擷取CPU資源後就可以繼續運作了。
知識點
在調用sleep方法的線程,睡眠期間其他線程調用了interrupt()方法中斷了該線程,則該線程會在調用sleep方法的地方抛出InterruptedException異常傳回
Sleep方法與wait方法差別
- wait方法可以指定等待時間,也可以不指定。Sleep方法必須指定
- wait方法釋放CPU資源,同時釋放鎖。sleep方法釋放cpu資源,但是不釋放鎖。是以容易死鎖。
- wait方法必須方法在同步塊或同步方法中,而sleep可以放在任意位置
讓出CPU執行權的yield方法
當一個線程調用yield方法時,目前線程會讓出CPU使用權,然後處于就緒狀态,線程排程器會從線程就緒隊列裡面擷取一個線程優先級最高的線程,當然也有可能會排程到剛剛讓出CPU的那個線程。
sleep()與yield()差別
目前線程調用sleep方法時調用線程會被阻塞挂起指定的時間,在這期間線程排程器不會去排程其他線程。
調用yield方法時,線程隻是讓出自己剩餘的時間片,并沒有挂起,而是出于就緒狀态,線程排程器下一次排程時就有可能排程到目前線程執行
線程中斷
java中的線程中斷是一種線程間的協作模式,線程中斷機制是一種協作機制,通過設定線程的中斷标志并不能直接終止該線程的執行,而是被中斷的線程根據中斷狀态自行處理。
Thread類裡提供的關于線程中斷的幾個方法:
- Thread.interrupt() : 中斷線程。這裡的中斷線程并不會立即停止線程,而是設定線程的中斷狀态為true(預設為false);
- Thread.interrupted(): 測試目前線程是否被中斷,線程的中斷狀态受這個方法的影響,意思是調用一次使線程中斷設定為true,連續調用兩次會使得這個線程的中斷狀态重新轉為false
- Thread.isInterrupted() : 測試目前線程是否被中斷,與上面方法不同的是調用這個方法并不會影響線程的中斷狀态。
線上程的中斷機制裡,當其他線程通知需要被中斷的線程後,線程中斷的狀态被設定為true,但是具體被要求中斷的線程要怎麼處理,完全由被中斷線程自己而定,可以在适合的實際進行中中斷請求,也可以完全不處理繼續執行下去。
線程死鎖
死鎖:指兩個或者兩個以上的線程在執行過程中,因争奪資源而造成的互相等待的現象。在無外力作用的情況下,這些線程會一直互相等待而無法繼續運作下去。

為什麼會産生死鎖??
死鎖的産生必須滿足以下四個條件:
- 互斥條件:指線程對已經擷取到的資源進行排它性使用,即該資源同時隻有一個線程占用。如果此時還有其他線程請求擷取該資源,則請求者隻能等待,直至占有資源的線程釋放該資源。
- 請求并持有條件:指一個線程已經持有了至少一個資源,但又提出了新的資源請求,而新的資源已被其他線程占有,是以目前線程會被阻塞,但是阻塞的同時并不釋放自己已經擷取的資源。
- 不可剝奪條件:指線程擷取到的資源在自己使用完之前不能被其他線程搶占,隻有在自己使用完畢後才由自己釋放該資源
- 環路等待:指在發生死鎖時, 必然存在一個線程→資源的環形鍊, 即線程集合{TO , TL T2 ,…, Tn }中的TO 正等待一個Tl 占用的資源, Tl 正在等待T2 占用的資源,……Tn 正在等待己被TO 占用的資源。
如何避免線程死鎖?
- 對資源進行有序的配置設定,讓擷取資源有先後順序
守護線程與使用者線程
Java中的線程分為兩類,分别為daemon線程和user線程。
知識點
JVM的退出和使用者線程有關,和守護線程無關。隻要有一個使用者線程沒有結束,正常情況下JVM就不會退出。
守護線程預設的優先級比較低。一個線程預設是非守護線程,可以通過Thread類的setDaemon(boolean on )來設定
線程組和線程優先級
線程組
java中用ThreadGroup來表示線程組,我們可以使用線程組對線程進行批量控制。
每個Thread必然存在于一個ThreadGroup中,Thread不能獨立于ThreadGroup存在。
ThreadGroup管理着它下面的Thread,ThreadGroup是一個标準的向下引用的樹狀結構。這樣設計的原因是防止上級線程被下級線程引用而無法有效地被GC回收。
線程的優先級
Java中線程優先級可以指定,範圍是1~10。但是并不是所有作業系統都支援10級優先級的劃分,java隻是給作業系統一個優先級參考值,線程最終在作業系統的優先級還是由作業系統決定。
知識點
java預設的線程優先級為5,線程的執行順序有排程程式來決定,線程的優先級會線上程調用前設定。通常情況下,高優先級的線程比低優先級有更高的幾率得到執行。我們使用方法Thread類的setPriority()方法來設定線程的優先級
線程組的常用方法及資料結構
擷取目前的線程組名字
複制線程組
//複制一個線程數組到一個線程組
Thread[] threads = new Thread[threadGroup.activeCount()];
ThreadGroup threadGroup = new ThreadGroup();
threadGroup.enumerate(threads);
線程組統一異常處理
public static void main(String[] args){
ThreadGroup threadGroup1 = new ThreadGroup("group1") {
// 繼承ThreadGroup并重新定義以下⽅法
// 線上程成員抛出unchecked exception
// 會執⾏此⽅法
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t.getName() + ": " + e.getMessage());
}
};
// 這個線程是threadGroup1的⼀員
Thread thread1 = new Thread(threadGroup1, new Runnable() {
public void run() {
// 抛出unchecked異常
throw new RuntimeException("測試異常");
}
});
thread1.start();
}
線程組的資料結構
線程組還可以包含其他的線程組,不僅僅是線程。
public class ThreadGroup implements Thread.UncaughtExceptionHandler {
private final ThreadGroup parent; // ⽗親ThreadGroup
String name; // ThreadGroupr 的名稱
int maxPriority; // 線程最⼤優先級
boolean destroyed; // 是否被銷毀
boolean daemon; // 是否守護線程
boolean vmAllowSuspension; // 是否可以中斷
int nUnstartedThreads = 0; // 還未啟動的線程
int nthreads; // ThreadGroup中線程數⽬
Thread threads[]; // ThreadGroup中的線程
int ngroups; // 線程組數⽬
ThreadGroup groups[]; // 線程組數組
}
構造函數:
// 私有構造函數
private ThreadGroup() {
this.name = "system";
this.maxPriority = Thread.MAX_PRIORITY;
this.parent = null;
}
// 預設是以目前ThreadGroup傳⼊作為parent ThreadGroup,新線程組的⽗線程組是⽬前正在運⾏線
public ThreadGroup(String name) {
this(Thread.currentThread().getThreadGroup(), name);
}
// 構造函數
public ThreadGroup(ThreadGroup parent, String name) {
this(checkParentAccess(parent), parent, name);
}
// 私有構造函數,主要的構造函數
private ThreadGroup(Void unused, ThreadGroup parent, String name) {
this.name = name;
this.maxPriority = parent.maxPriority;
this.daemon = parent.daemon;
this.vmAllowSuspension = parent.vmAllowSuspension;
this.parent = parent;
parent.add(this);
}
第三個構造函數⾥調⽤了 checkParentAccess ⽅法,這⾥看看這個⽅法的源碼:
// 檢查parent ThreadGroup
private static Void checkParentAccess(ThreadGroup parent) {
parent.checkAccess();
return null;
}
// 判斷目前運⾏的線程是否具有修改線程組的權限
public final void checkAccess() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkAccess(this);
}
}
知識點
這⾥涉及到 SecurityManager 這個類,它是Java的安全管理器,它允許應⽤
程式在執⾏⼀個可能不安全或敏感的操作前确定該操作是什麼,以及是否是
在允許執⾏該操作的安全上下⽂中執⾏它。應⽤程式可以允許或不允許該操
作。
⽐如引⼊了第三⽅類庫,但是并不能保證它的安全性。
其實Thread類也有⼀個checkAccess()⽅法,不過是⽤來目前運⾏的線程是
否有權限修改被調⽤的這個線程執行個體。(Determines if the currently running
thread has permission to modify this thread.)
總結來說,線程組是⼀個樹狀的結構,每個線程組下⾯可以有多個線程或者線程
組。線程組可以起到統⼀控制線程的優先級和檢查線程的權限的作⽤。
ThreadLocal
多線程通路同一個共享變量時特别容易出現并發問題,特别是在多個線程需要對一個共享變量寫入時。為了保證線程安全,一般使用者在通路共享變量時需要進行适當的同步。同步的措施是加鎖,這就需要使用者對鎖有一定的了解。
問題:有沒有一種方式可以做到,當建立一個變量後,每個線程對其進行通路的時候通路的是自己的變量?
ThreadLocal可以做這個事情,它提供了線程本地變量,也就是如果你建立了一個ThreadLocal變量,那麼通路這個變量的每個線程都會有這個變量的一個本地副本。當多個線程操作這個變量時,實際操作的是自己本地記憶體裡面的變量,進而避免了線程安全問題。
知識點
如果開發者希望将類的某個靜态變量(UserID或者transaction ID )與線程狀态關聯,則可以考慮使用ThreadLocal。最常見的ThreadLocal使用場景為用來解決資料庫連接配接、Session管理等,資料庫連接配接和Session管理涉及多個複雜對象的初始化和關閉,如果在每個線程中聲明一些私有變量來進行操作,那這個線程就變得不那麼輕量了,需要頻繁的建立和關閉連接配接。