書接上文。上文主要講了下線程的基本概念赫悄,三種創(chuàng)建線程的方式與區(qū)別,還介紹了線程的狀態(tài)馏慨,線程通知和等待涩蜘,join等,本篇繼續(xù)介紹并發(fā)編程的基礎(chǔ)知識熏纯。
sleep
當一個執(zhí)行的線程調(diào)用了Thread的sleep方法同诫,調(diào)用線程會暫時讓出指定時間的執(zhí)行權(quán),在這期間不參與CPU的調(diào)度樟澜,不占用CPU误窖,但是不會釋放該線程鎖持有的監(jiān)視器鎖。指定的時間到了后秩贰,該線程會回到就緒的狀態(tài)霹俺,再次等待分配CPU資源,然后再次執(zhí)行毒费。
我們有時會看到sleep(1)丙唧,甚至還有sleep(0)這種寫法,肯定會覺得非常奇怪觅玻,特別是sleep(0)想际,睡0秒鐘,有意義嗎溪厘?其實是有的胡本,sleep(1),sleep(0)的意義就在于告訴操作系統(tǒng)立刻觸發(fā)一次CPU競爭畸悬。
讓我們來看看正在sleep的進程被中斷了侧甫,會發(fā)生什么事情:
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();
}
}
運行結(jié)果:
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資源并運行的,正常情況下蹋宦,一個線程只有把分配給自己的時間片用完之后披粟,線程調(diào)度器才會進行下一輪的線程調(diào)度,當執(zhí)行了Thread的yield后冷冗,就告訴操作系統(tǒng)“我不需要CPU了守屉,你現(xiàn)在就可以進行下一輪的線程調(diào)度了 ”,但是操作系統(tǒng)可以忽略這個暗示贾惦,也有可能下一輪還是把時間片分配給了這個線程胸梆。
我們來寫一個例子加深下印象:
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();
}
}
運行結(jié)果:
當然由于線程的特性须板,所以每次運行結(jié)果可能都不太相同碰镜,但是當我們運行多次后,會發(fā)現(xiàn)絕大多數(shù)的時候习瑰,兩個線程的打印都是比較平均的绪颖,我用完時間片了,你用甜奄,你用完了時間片了柠横,我再用。
當我們調(diào)用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();
}
}
運行結(jié)果:
當然在一般情況下牍氛,可能永遠也不會用到y(tǒng)ield,但是還是要對這個方法有一定的了解烟阐。
sleep 和 yield 區(qū)別
當線程調(diào)用sleep后搬俊,會阻塞當前線程指定的時間,在這段時間內(nèi)蜒茄,線程調(diào)度器不會調(diào)用此線程唉擂,當指定的時間結(jié)束后,該線程的狀態(tài)為“就緒”檀葛,等待分配CPU資源玩祟。
當線程調(diào)用yield后,不會阻塞當前線程屿聋,只是讓出時間片空扎,回到“就緒”的狀態(tài),等待分配CPU資源润讥。
死鎖
死鎖是指多個線程在執(zhí)行的過程中勺卢,因為爭奪資源而造成的相互等待的現(xiàn)象,而且無法打破這個“僵局”象对。
死鎖的四個必要條件:
- 互斥:指線程對于已經(jīng)獲取到的資源進行排他性使用黑忱,即該資源只能被一個線程占有,如果還有其他線程也想占有勒魔,只能等待甫煞,直到占有資源的線程釋放該資源。
- 請求并持有:指一個線程已經(jīng)占有了一個資源冠绢,但是還想占有其他的資源抚吠,但是其他資源已經(jīng)被其他線程占有了,所以當前線程只能等待弟胀,等待的同時并不釋放自己已經(jīng)擁有的資源楷力。
- 不可剝奪:當一個線程獲取資源后喊式,不能被其他線程占有,只有在自己使用完畢后自己釋放資源萧朝。
- 環(huán)路等待:即 T1線程正在等待T2占有的資源岔留,T2線程正在等待T3線程占有的資源,T3線程又在等待T1線程占有的資源检柬。
要想打破“死鎖”僵局献联,只需要破壞以上四個條件中的任意一個,但是程序員可以干預(yù)的只有“請求并持有”何址,“環(huán)路等待”兩個條件里逆,其余兩個條件是鎖的特性,程序員是無法干預(yù)的用爪。
聰明的你原押,一定看出來了,所謂“死鎖”就是“悲觀鎖”造成的偎血,相對于“死鎖”诸衔,還有一個“活鎖”,就是“樂觀鎖”造成的烁巫。
守護線程與用戶線程
Java中的線程分為兩類署隘,分別為 用戶線程和守護線程。在JVM啟動時亚隙,會調(diào)用main函數(shù)磁餐,這個就是用戶線程,JVM內(nèi)部還會啟動一些守護線程阿弃,比如垃圾回收線程诊霹。那么守護線程和用戶線程到底有什么區(qū)別呢?當最后一個用戶線程結(jié)束后渣淳,JVM就自動退出了脾还,而不管當前是否有守護線程還在運行。
如何創(chuàng)建一個守護線程呢入愧?
public class Daemon {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
});
thread.setDaemon(true);
thread.start();
}
}
只需要設(shè)置線程的daemon為true就可以鄙漏。
下面來演示下用戶線程與守護線程的區(qū)別:
public class Daemon {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (true){}
});
thread.start();
}
}
當我們運行后,可以發(fā)現(xiàn)程序一直沒有退出:
因為這是用戶線程棺蛛,只要有一個用戶線程還沒結(jié)束怔蚌,程序就不會退出。
再來看看守護線程:
public class Daemon {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (true){}
});
thread.setDaemon(true);
thread.start();
}
}
當我們運行后旁赊,發(fā)現(xiàn)程序立刻就停止了:
因為這是守護線程桦踊,當用戶線程結(jié)束后,不管有沒有守護線程還在運行终畅,程序都會退出籍胯。
線程中斷
之所以把線程中斷放在后面竟闪,是因為它是并發(fā)編程基礎(chǔ)中最難以理解的一個,當然這也與不經(jīng)常使用有關(guān)≌壤牵現(xiàn)在就讓我們好好看看線程中斷炼蛤。
Thread提供了stop方法,用來停止當前線程本刽,但是已經(jīng)被標記為過期鲸湃,應(yīng)該用線程中斷方法來代替stop方法赠涮。
interrupt
中斷線程子寓。當線程A運行(非阻塞)時,線程B可以調(diào)用線程A的interrupt方法來設(shè)置線程A的中斷標記為true笋除,這里要特別注意斜友,調(diào)用interrupt方法并不會真的去中斷線程,只是設(shè)置了中斷標記為true垃它,線程A還是活的好好的鲜屏。如果線程A被阻塞了,比如調(diào)用了sleep国拇、wait洛史、join,線程A會在調(diào)用這些方法的地方拋出“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("結(jié)束了,時間是" + (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();
}
}
運行結(jié)果:
結(jié)束了,時間是7643
true
在子線程中,我們通過一個循環(huán)往copyOnWriteArrayList里面添加數(shù)據(jù)來模擬一個耗時操作务热。這里要特別要注意忆嗜,一般來說,我們模擬耗時操作都是用sleep方法崎岂,但是這里不能用sleep方法捆毫,因為調(diào)用sleep方法會讓當前線程阻塞,而現(xiàn)在是要讓線程處于運行的狀態(tài)冲甘。我們可以很清楚的看到绩卤,雖然子線程剛運行,就被interrupt了江醇,但是卻沒有拋出任何異常濒憋,也沒有讓子線程終止,子線程還是活的好好的嫁审,只是最后打印出的“中斷標記”為true跋炕。
如果沒有調(diào)用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("結(jié)束了,時間是" + (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();
}
}
運行結(jié)果:
結(jié)束了,時間是1
false
在介紹sleep律适,wait辐烂,join方法的時候遏插,大家已經(jīng)看到了,如果中斷調(diào)用這些方法而被阻塞的線程會拋出異常纠修,這里就不再演示了胳嘲,但是還有一點需要注意,當我們catch住InterruptedException異常后扣草,“中斷標記”會被重置為false了牛,我們繼續(xù)做實驗:
class InterruptTask implements Runnable {
@Override
public void run() {
CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList();
try {
long start = System.currentTimeMillis();
TimeUnit.SECONDS.sleep(3);
System.out.println("結(jié)束了,時間是" + (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();
}
}
運行結(jié)果:
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了辰妙。
還有一個問題鹰祸,大家可以思考下,代碼的本意是當前線程被中斷后退出死循環(huán)密浑,這段代碼有問題嗎蛙婴?
Thread th = Thread.currentThread();
while(true) {
if(th.isInterrupted()) {
break;
}
try {
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
}
本題來自 極客時間 王寶令 老師的 《Java并發(fā)編程實戰(zhàn)》
代碼是有問題的,因為catch住異常后尔破,會把“中斷標記”重置街图。如果正好在sleep的時候,線程被中斷了懒构,又重置了“中斷標記”餐济,那么下一次循環(huán),檢測中斷標記為false胆剧,就無法退出死循環(huán)了絮姆。
isInterrupted
這個方法在上面已經(jīng)出現(xiàn)過了,就是 獲取對象線程的“中斷標記”赞赖。
interrupted
獲取當前線程的“中斷標記”滚朵,如果發(fā)現(xiàn)當前線程被中斷,會重置中斷標記為false前域,該方法是static方法辕近,通過Thread類直接調(diào)用。
并發(fā)編程基礎(chǔ)到這里就結(jié)束了匿垄,可以看到內(nèi)容還是相當多的移宅,雖說是基礎(chǔ),但是每一個知識點椿疗,如果要深究的話漏峰,都可以牽扯到“操作系統(tǒng)”,所以只有深入到了“操作系統(tǒng)”届榄,才可以說真的懂了浅乔,現(xiàn)在還是僅僅停留在Java的層面,唉。