第十章【執行緒】
一、程序和執行緒
1、程序:
代表了記憶體中正在執行的應用程式,計算機中的資源(cpu 記憶體 磁碟 網路等),會按照需求分配給每個程序,從而這個程序對應的應用程式就可以使用這些資源了。程序就是在系統中執行一個應用程式的基本單位。
2、執行緒:
是程序中的一個程式碼執行單元,負責當前程序中程式碼程式的執行,一個程序中有一個或多個執行緒。當一個程序中啟動了多個執行緒去分別執行程式碼的時候,這個程式就是多執行緒程式。
透過JDK自帶的jconsole
工具,可以檢測到當前執行Hello這個類的時候,JVM的執行情況,包含記憶體的使用、執行緒的執行狀態、類的載入等資訊
二、併發和並行
1、併發:
是指在一個時間段內,倆個或多個執行緒,使用一個CPU,進行交替執行。
2、並行:
是指在同一時刻,倆個或多個執行緒,各自使用一個CPU,同時進行執行。
三、時間片
1、概念
時間片,當前一個執行緒要使用CPU的時候,CPU會分配給這個執行緒一小段時間(毫秒級別),這段時間就叫做時間片,也就是該執行緒允許使用CPU執行的時間,在這個期間,執行緒擁有CPU的使用權。
2、排程
當倆個或多個執行緒使用一個CPU來執行程式碼的時候,在作業系統的核心中,就會有相應的演算法來控制執行緒獲取CPU時間片的方式,從而使得這些執行緒可以按照某種順序來使用cpu執行程式碼,這種情況被稱為執行緒呼叫。
常見的排程方式:
-
時間片輪轉
所有執行緒輪流使用 CPU 的使用權,平均分配每個執行緒佔用 CPU 的時間。
-
搶佔式排程
系統會讓優先順序高的執行緒優先使用 CPU(提高搶佔到的機率),但是如果執行緒的優先順序相同,那麼會隨機選擇一個執行緒獲取當前CPU的時間片。 【JVM中的執行緒,使用的為搶佔式排程】
四、執行緒的相關操作
1、main執行緒
使用java
命令來執行一個類的時候,首先會啟動JVM(程序),JVM會在建立一個名字叫做main的執行緒,來執行類中的程式入口(main方法),我們寫在main方法中的程式碼,其實都是由名字叫做main的執行緒去執行的。
Thread.currentThread();
可以寫在任意方法中,返回就是執行這個方法的執行緒物件
2、執行緒的建立和啟動
- Java中透過繼承Thread類來建立並啟動一個新的執行緒
-
定義 Thread 類的子類(可以是匿名內部類),並重寫 Thread 類中的 run 方法, run 方法中的程式碼就是執行緒的執行任務
-
建立 Thread 子類的物件,這個物件就代表了一個要獨立執行的新執行緒
-
呼叫執行緒物件的 start 方法來啟動該執行緒
class MyThread extends Thread{ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("hello world"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { Thread t = new MyThread(); t.start(); }
匿名內部類方法 public static void main(String[] args) { Thread t = new MyThread(){ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("hello world"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; t.start(); }
-
利用Runnable介面來完成執行緒建立和啟動【推薦】
public class Thread implements Runnable { private Runnable target; public Thread() { //... } public Thread(Runnable target) { this.target = target; //.. } @Override public void run() { if (target != null) { target.run(); } } }
使用 Runnable 介面的匿名內部類,來指定執行緒的執行任務(重寫介面中的run方法) public static void main(String[] args) { Runnable run = new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("hello world"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; Thread t = new Thread(run); t.start(); }
3、執行緒的名字
透過Thread類中的currentThread方法,可以獲取當前執行緒的物件,然後呼叫執行緒物件的getName方法,可以獲取當前執行緒的名字。String name = Thread.currentThread().getName();
主執行緒中,建立出的執行緒,它們的都會有一個預設的名字:Thread-" + nextThreadNum()
可以建立執行緒物件的時候,給它設定一個指定的名字:
//方法一:
Thread t = new Thread("t執行緒");
//方法二:
Thread t = new Thread(new Runnable(){
@Override
public void run(){
//執行任務
}
},"t執行緒");
//方法三:
Thread t = new Thread();
t.setName("t執行緒");
4、執行緒的分類
- 前臺執行緒,又叫做執行執行緒、使用者執行緒
- 後臺執行緒,又叫做守護執行緒、精靈執行緒
前臺執行緒:
這種執行緒專門用來執行使用者編寫的程式碼,地位比較高,JVM是否會停止執行,就是要看當前是否還有前臺執行緒沒有執行完,如果還剩下任意一個前臺執行緒沒有“死亡”,那麼JVM就不能停止!
後臺執行緒:
這種執行緒是用來給前臺執行緒服務的,給前臺執行緒提供一個良好的執行環境,地位比較低,JVM是否停止執行,根本不關心後臺執行緒的執行情況和狀態。
在主執行緒中,建立出來的執行緒物件,預設就是前臺執行緒,在它啟動之前,我們還可以給它設定為後臺執行緒:
public static void main(String[] args) {
Thread t = new Thread("t執行緒"){
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 10; i++) {
System.out.println(name+": hello "+i);
}
}
};
//在啟動執行緒之前,可以將其設定為後臺執行緒,否則預設是前臺執行緒
t.setDaemon(true);
t.start();
}
5、執行緒的優先順序
執行緒的優先順序使用int型別數字表示,最大是10,最小是1,預設的優先順序是5。
最終設定執行緒優先順序的方法,是一個native方法,並不是java語言實現的。
當倆個執行緒爭奪CPU時間片的時候:
- 優先順序相同,獲得CPU使用權的機率相同
- 優先順序不同,那麼高優先順序的執行緒有更高的機率獲取到CPU的使用權
6、執行緒組
Java中使用java.lang.ThreadGroup
類來表示執行緒組,它可以對一批執行緒進行管理,對執行緒組進行操作,同時也會對執行緒組裡面的這一批執行緒操作。
@Test
public void testThreadGroup1() {
/*
* JVM會構建一個預設的執行緒組,管理JVM構建的main執行緒組
*/
Thread currentThread = Thread.currentThread();
ThreadGroup threadGroup = currentThread.getThreadGroup();
System.out.println(threadGroup);
//建立一個新執行緒,沒有放到執行緒組
Thread th = new Thread("myThread");
System.out.println(th.getThreadGroup());
//自己構建一個執行緒組
ThreadGroup tg = new ThreadGroup("myThreadGroup");
tg.setMaxPriority(9);
Thread th1 = new Thread(tg,"執行緒2");
System.out.println(th1.getThreadGroup());
}
@Test
public void testThreadGroup2() {
/*
* 執行緒放線上程組,可以檢視和管理執行緒組中的執行緒
*/
Runnable run = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
ThreadGroup group = new ThreadGroup("myThreadGroup");
Thread t1 = new Thread(group,run,"執行緒1");
Thread t2 = new Thread(group,run,"執行緒2");
Thread t3 = new Thread(group,run,"執行緒3");
t1.start();
t2.start();
t3.start();
System.out.println("執行緒組中還在存活的執行緒個數為:"+group.activeCount());
Thread[] arr = new Thread[group.activeCount()];
System.out.println("arr陣列中存放的執行緒個數為:"+group.enumerate(arr));
System.out.println(Arrays.toString(arr));
}
7、執行緒狀態
java.lang.Thread.State
列舉型別中(內部類形式),定義了執行緒的幾種狀態:
執行緒狀態 | 名稱 | 描述 |
---|---|---|
NEW | 新建 | 執行緒剛被建立,還沒呼叫start方法,或者剛剛呼叫了start方法,呼叫start方法不一定"立即"改變執行緒狀態,中間可能需要一些步驟才完成一個執行緒的啟動。 |
RUNNABLE | 可執行 | start方法呼叫結束,執行緒由NEW變成RUNNABLE,執行緒存活著,並嘗試搶佔CPU資源,或者已經搶佔到CPU資源正在執行,這倆種情況的狀態都顯示為RUNNABLE |
BLOCKED | 鎖阻塞 | 執行緒A和執行緒B都要執行方法test,而且方法test被加了鎖,執行緒A先拿到了鎖去執行test方法,執行緒B這時候需要等待執行緒A把鎖釋放。這時候執行緒B就是處理BLOCKED |
WAITING | 無限期等待 | 一個執行緒在等待另一個執行緒執行一個(喚醒)動作時,該執行緒進入Waiting狀態。進入這個狀態後是不能自動喚醒的,必須等待另一個執行緒呼叫notify或者notifyAll方法才能夠喚醒。 |
TIMED_WAITING | 有限期等待 | 和WAITING狀態類似,但是有一個時間期限,時間到了,自己也會主動醒來 |
TERMINATED | 終止(死亡) | run方法執行結束的執行緒處於這種狀態。 |
執行緒在BLOCKED,WAITING,TIMED_WAITING這三種情況的阻塞下,都具備相同的特點:
-
執行緒不執行程式碼
-
執行緒也不參與CPU時間片的爭奪
注意:
-
剛建立好的執行緒物件,就是出於NEW的狀態
-
執行緒啟動後,會出於RUNNABLE狀態,從就緒狀態到執行狀態,之間會經過多次反覆的CPU執行權的爭奪
RUNNABLE狀態包含倆種情況:
- 就緒狀態:此時這個執行緒沒有執行,因為沒有搶到CPU的執行權
- 執行狀態:此時這個執行緒正在執行,因為搶到CPU的執行權
-
執行緒執行了
sleep(long million)
方法後,會從RUNNABLE狀態進入到TIMED_WAITING狀態TIMED_WAITING狀態阻塞結束後,執行緒會自動回到RUNNABLE狀態
-
執行緒執行了
join()
方法後,會從RUNNABLE狀態進入到WAITING狀態,呼叫執行緒的interrupt()
方法,可以從WAITING狀態進入到RUNNABLE狀態執行緒執行了
join(long million)
方法後,會從RUNNABLE狀態進入到TIMED_WAITING狀態 -
interrupt()
方法是透過改變執行緒物件中的一個標識的值(true|false),來達到打斷阻塞狀態的效果檢視就緒/執行狀態下的執行緒物件中“打斷標識”值的倆個方法:
-
isInterrupted()
方法呼叫了interrupt()方法後,執行緒中的“打斷標識”值設定為true,可以透過執行緒物件中的
isInterrupted
方法返回這個標識的值,並且不會修改這個值,所以輸出顯示的一直是ture。 -
interrupted()
方法呼叫了interrupt()方法後,執行緒中的“打斷標識”值設定為true,可以透過執行緒物件中的
interrupted
方法,第一次返回true之後,後面在呼叫方法檢視這個“打斷標識”值,都是false。interrupted
返回true後,會直接把這個值給清除掉。
-
五、執行緒安全
當多個執行緒同時共享,同一個全域性變數或靜態變數,做寫的操作時,可能會發生資料衝突問題,也就是執行緒安全問題。但是做讀操作是不會發生資料衝突問題。
六、執行緒同步
Java中實現執行緒同步的方式,是給需要同步的程式碼進行synchronized
關鍵字加鎖。
七、Synchronized
使用格式為:
synchronized (鎖物件){
//操作共享變數的程式碼,這些程式碼需要執行緒同步,否則會有執行緒安全問題
//...
}
八、wait和notify
Object類中有三個方法: wait()
、notify()
、notifyAll()
三個核心點:
-
任何物件中都一定有這三個方法
-
只有物件作為鎖物件的時候,才可以呼叫
-
只有在同步的程式碼塊中,才可以呼叫
當前呼叫鎖物件的wait方法後,當前執行緒釋放鎖,然後進入到阻塞狀態,並且等待其他執行緒先喚醒自己,如果沒有其他執行緒喚醒自己,那麼就一直等著。所以現在的情況是,倆個執行緒t1和t2都是在處於阻塞狀態,等待別人喚醒自己,所以程式不執行了,但是也沒結束!
如果有執行緒呼叫了notify()方法進行了喚醒,或者interrupt方法進行了打斷,那麼這個執行緒就會從等待池進入到鎖池,而進入到鎖池的執行緒,會時刻關注鎖物件是否可用,一旦可用,這個執行緒就會立刻自動恢復到RUNNABLE狀態。
鎖物件.notify(),該方法可以在等待池中,隨機喚醒一個等待指定鎖物件的執行緒,使得這個執行緒進入到鎖池中,而進入到鎖池的執行緒, 一旦發現鎖可用,就可以自動恢復到RUNNABLE狀態了
鎖物件.notifyAll(),該方法可以在等待池中,喚醒所有等待指定鎖物件的執行緒,使得這個執行緒進入到鎖池中,而進入到鎖池的執行緒, 一旦發現鎖可用,就可以自動恢復到RUNNABLE狀態了
由圖可知,TIMED_WAITING
、WAITING
、BLOCKED
都屬於執行緒阻塞,他們共同的特點是就是執行緒不執行程式碼,也不參與CPU的爭奪,除此之外,它們還有各自的特點:(重要)
-
阻塞1,執行緒執行時,呼叫sleep或者join方法後,進入這種阻塞,該阻塞狀態可以恢復到RUNNABLE狀態,條件是執行緒被打斷了、或者指定的時間到了,或者join的執行緒結束了
-
阻塞2,執行緒執行時,發現鎖不可用後,進入這種阻塞,該阻塞狀態可以恢復到RUNNABLE狀態,條件是執行緒需要爭奪的鎖物件變為可用了(別的執行緒把鎖釋放了)
-
阻塞3,執行緒執行時,呼叫了wait方法後,執行緒先釋放鎖後,再進入這種阻塞,該阻塞狀態可以恢復到BLOCKED狀態(也就是阻塞2的情況),條件是執行緒被打斷了、或者是被別的執行緒喚醒了(notify方法)
九、死鎖
在程式中要儘量避免出現死鎖情況,一旦發生那麼只能手動停止JVM的執行,然後查詢並修改產生死鎖的問題程式碼
簡單的描述死鎖就是:倆個執行緒t1和t2,t1拿著t2需要等待的鎖不釋放,而t2又拿著t1需要等待的鎖不釋放,倆個執行緒就這樣一直僵持下去。