書接上文。上文主要講了下線程的基本概念,三種創建線程的方式與區別,還介紹了線程的狀態,線程通知和等待,join等,本篇繼續介紹併發編程的基礎知識。 sleep 當一個執行的線程調用了Thread的sleep方法,調用線程會暫時讓出指定時間的執行權,在這期間不參與CPU的調度,不占用CPU,但是不會釋 ...
書接上文。上文主要講了下線程的基本概念,三種創建線程的方式與區別,還介紹了線程的狀態,線程通知和等待,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的層面,唉。