書接上文。上文主要講了下執行緒的基本概念,三種建立執行緒的方式與區別,還介紹了執行緒的狀態,執行緒通知和等待,join等,本篇繼續介紹併發程式設計的基礎知識。
sleep
當一個執行的執行緒呼叫了Thread的sleep方法,呼叫執行緒會暫時讓出指定時間的執行權,在這期間不參與CPU的排程,不佔用CPU,但是不會釋放該執行緒鎖持有的監視器鎖。指定的時間到了後,該執行緒會回到就緒的狀態,再次等待分配CPU資源,然後再次執行。
我們有時會看到sleep(1),甚至還有sleep(0)這種寫法,肯定會覺得非常奇怪,特別是sleep(0),睡0秒鐘,有意義嗎?其實是有的,sleep(1),sleep(0)的意義就在於告訴作業系統立刻觸發一次CPU競爭。
讓我們來看看正在sleep的程式被中斷了,會發生什麼事情:
class MySleepTask implements Runnable{
@Override
public void run() {
System.out.println("MyTask1");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
System.out.println("中斷");
e.printStackTrace();
}
System.out.println("MyTask2");
}
}
public class Sleep {
public static void main(String[] args) {
MySleepTask mySleepTask=new MySleepTask();
Thread thread=new Thread(mySleepTask);
thread.start();
thread.interrupt();
}
}
複製程式碼
執行結果:
MyTask1
中斷
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.codebear.MySleepTask.run(Sleep.java:10)
at java.lang.Thread.run(Thread.java:748)
MyTask2
複製程式碼
yield
我們知道執行緒是以時間片的機制來佔用CPU資源並執行的,正常情況下,一個執行緒只有把分配給自己的時間片用完之後,執行緒排程器才會進行下一輪的執行緒排程,當執行了Thread的yield後,就告訴作業系統“我不需要CPU了,你現在就可以進行下一輪的執行緒排程了 ”,但是作業系統可以忽略這個暗示,也有可能下一輪還是把時間片分配給了這個執行緒。
我們來寫一個例子加深下印象:
class MyYieldTask implements Runnable {
@Override
public void run() {
for (int i = 10; i > 0; i--) {
System.out.println("我是" + Thread.currentThread().getName() + ",我分配到了時間片");
}
}
}
public class MyYield {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyYieldTask());
thread1.start();
Thread thread2 = new Thread(new MyYieldTask());
thread2.start();
}
}
複製程式碼
執行結果:
當然由於執行緒的特性,所以每次執行結果可能都不太相同,但是當我們執行多次後,會發現絕大多數的時候,兩個執行緒的列印都是比較平均的,我用完時間片了,你用,你用完了時間片了,我再用。
當我們呼叫yield後:
class MyYieldTask implements Runnable {
@Override
public void run() {
for (int i = 10; i > 0; i--) {
System.out.println("我是" + Thread.currentThread().getName() + ",我分配到了時間片");
Thread.yield();
}
}
}
public class MyYield {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyYieldTask());
thread1.start();
Thread thread2 = new Thread(new MyYieldTask());
thread2.start();
}
}
複製程式碼
執行結果:
當然在一般情況下,可能永遠也不會用到yield,但是還是要對這個方法有一定的瞭解。
sleep 和 yield 區別
當執行緒呼叫sleep後,會阻塞當前執行緒指定的時間,在這段時間內,執行緒排程器不會呼叫此執行緒,當指定的時間結束後,該執行緒的狀態為“就緒”,等待分配CPU資源。 當執行緒呼叫yield後,不會阻塞當前執行緒,只是讓出時間片,回到“就緒”的狀態,等待分配CPU資源。
死鎖
死鎖是指多個執行緒在執行的過程中,因為爭奪資源而造成的相互等待的現象,而且無法打破這個“僵局”。
死鎖的四個必要條件:
- 互斥:指執行緒對於已經獲取到的資源進行排他性使用,即該資源只能被一個執行緒佔有,如果還有其他執行緒也想佔有,只能等待,直到佔有資源的執行緒釋放該資源。
- 請求並持有:指一個執行緒已經佔有了一個資源,但是還想佔有其他的資源,但是其他資源已經被其他執行緒佔有了,所以當前執行緒只能等待,等待的同時並不釋放自己已經擁有的資源。
- 不可剝奪:當一個執行緒獲取資源後,不能被其他執行緒佔有,只有在自己使用完畢後自己釋放資源。
- 環路等待:即 T1執行緒正在等待T2佔有的資源,T2執行緒正在等待T3執行緒佔有的資源,T3執行緒又在等待T1執行緒佔有的資源。
要想打破“死鎖”僵局,只需要破壞以上四個條件中的任意一個,但是程式設計師可以干預的只有“請求並持有”,“環路等待”兩個條件,其餘兩個條件是鎖的特性,程式設計師是無法干預的。
聰明的你,一定看出來了,所謂“死鎖”就是“悲觀鎖”造成的,相對於“死鎖”,還有一個“活鎖”,就是“樂觀鎖”造成的。
守護執行緒與使用者執行緒
Java中的執行緒分為兩類,分別為 使用者執行緒和守護執行緒。在JVM啟動時,會呼叫main函式,這個就是使用者執行緒,JVM內部還會啟動一些守護執行緒,比如垃圾回收執行緒。那麼守護執行緒和使用者執行緒到底有什麼區別呢?當最後一個使用者執行緒結束後,JVM就自動退出了,而不管當前是否有守護執行緒還在執行。 如何建立一個守護執行緒呢?
public class Daemon {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
});
thread.setDaemon(true);
thread.start();
}
}
複製程式碼
只需要設定執行緒的daemon為true就可以。 下面來演示下使用者執行緒與守護執行緒的區別:
public class Daemon {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (true){}
});
thread.start();
}
}
複製程式碼
當我們執行後,可以發現程式一直沒有退出:
因為這是使用者執行緒,只要有一個使用者執行緒還沒結束,程式就不會退出。再來看看守護執行緒:
public class Daemon {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (true){}
});
thread.setDaemon(true);
thread.start();
}
}
複製程式碼
當我們執行後,發現程式立刻就停止了:
因為這是守護執行緒,當使用者執行緒結束後,不管有沒有守護執行緒還在執行,程式都會退出。執行緒中斷
之所以把執行緒中斷放在後面,是因為它是併發程式設計基礎中最難以理解的一個,當然這也與不經常使用有關。現在就讓我們好好看看執行緒中斷。 Thread提供了stop方法,用來停止當前執行緒,但是已經被標記為過期,應該用執行緒中斷方法來代替stop方法。
interrupt
中斷執行緒。當執行緒A執行(非阻塞)時,執行緒B可以呼叫執行緒A的interrupt方法來設定執行緒A的中斷標記為true,這裡要特別注意,呼叫interrupt方法並不會真的去中斷執行緒,只是設定了中斷標記為true,執行緒A還是活的好好的。如果執行緒A被阻塞了,比如呼叫了sleep、wait、join,執行緒A會在呼叫這些方法的地方丟擲“InterruptedException”。 我們來做個試驗,證明下interrupt方法不會中斷正在執行的執行緒:
class InterruptTask implements Runnable {
@Override
public void run() {
CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList();
try {
long start = System.currentTimeMillis();
for (int i = 0; i < 150000; i++) {
copyOnWriteArrayList.add(i);
}
System.out.println("結束了,時間是" + (System.currentTimeMillis() - start));
System.out.println(Thread.currentThread().isInterrupted());
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public class InterruptTest {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new InterruptTask());
thread1.start();
thread1.interrupt();
}
}
複製程式碼
執行結果:
結束了,時間是7643
true
複製程式碼
在子執行緒中,我們通過一個迴圈往copyOnWriteArrayList裡面新增資料來模擬一個耗時操作。這裡要特別要注意,一般來說,我們模擬耗時操作都是用sleep方法,但是這裡不能用sleep方法,因為呼叫sleep方法會讓當前執行緒阻塞,而現在是要讓執行緒處於執行的狀態。我們可以很清楚的看到,雖然子執行緒剛執行,就被interrupt了,但是卻沒有丟擲任何異常,也沒有讓子執行緒終止,子執行緒還是活的好好的,只是最後列印出的“中斷標記”為true。
如果沒有呼叫interrupt方法,中斷標記為false:
class InterruptTask implements Runnable {
@Override
public void run() {
CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList();
try {
long start = System.currentTimeMillis();
for (int i = 0; i < 500; i++) {
copyOnWriteArrayList.add(i);
}
System.out.println("結束了,時間是" + (System.currentTimeMillis() - start));
System.out.println(Thread.currentThread().isInterrupted());
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public class InterruptTest {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new InterruptTask());
thread1.start();
}
}
複製程式碼
執行結果:
結束了,時間是1
false
複製程式碼
在介紹sleep,wait,join方法的時候,大家已經看到了,如果中斷呼叫這些方法而被阻塞的執行緒會丟擲異常,這裡就不再演示了,但是還有一點需要注意,當我們catch住InterruptedException異常後,“中斷標記”會被重置為false,我們繼續做實驗:
class InterruptTask implements Runnable {
@Override
public void run() {
CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList();
try {
long start = System.currentTimeMillis();
TimeUnit.SECONDS.sleep(3);
System.out.println("結束了,時間是" + (System.currentTimeMillis() - start));
} catch (Exception ex) {
System.out.println(Thread.currentThread().isInterrupted());
ex.printStackTrace();
}
}
}
public class InterruptTest {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new InterruptTask());
thread1.start();
thread1.interrupt();
}
}
複製程式碼
執行結果:
false
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.codebear.InterruptTask.run(InterruptTest.java:20)
at java.lang.Thread.run(Thread.java:748)
複製程式碼
可以很清楚的看到,“中斷標記”被重置為false了。
還有一個問題,大家可以思考下,程式碼的本意是當前執行緒被中斷後退出死迴圈,這段程式碼有問題嗎?
Thread th = Thread.currentThread();
while(true) {
if(th.isInterrupted()) {
break;
}
try {
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
}
複製程式碼
本題來自 極客時間 王寶令 老師的 《Java併發程式設計實戰》
程式碼是有問題的,因為catch住異常後,會把“中斷標記”重置。如果正好在sleep的時候,執行緒被中斷了,又重置了“中斷標記”,那麼下一次迴圈,檢測中斷標記為false,就無法退出死迴圈了。
isInterrupted
這個方法在上面已經出現過了,就是 獲取物件執行緒的“中斷標記”。
interrupted
獲取當前執行緒的“中斷標記”,如果發現當前執行緒被中斷,會重置中斷標記為false,該方法是static方法,通過Thread類直接呼叫。
併發程式設計基礎到這裡就結束了,可以看到內容還是相當多的,雖說是基礎,但是每一個知識點,如果要深究的話,都可以牽扯到“作業系統”,所以只有深入到了“作業系統”,才可以說真的懂了,現在還是僅僅停留在Java的層面,唉。