并發(fā)編程對(duì)于很多人來說都是一個(gè)難點(diǎn),因?yàn)槠鋬?nèi)部涉及到的內(nèi)容非常繁雜骚勘,同時(shí)在實(shí)際開發(fā)中很多人雖然做了多年铐伴,但是仍然還停留在業(yè)務(wù)代碼開發(fā)。對(duì)于并發(fā)的接觸可以說是少之又少俏讹〉毖纾可是現(xiàn)在的面試中,并發(fā)這一部分又是一個(gè)高頻考點(diǎn)泽疆,很多人都在并發(fā)這座大山面前無法翻越户矢。
本系列會(huì)帶領(lǐng)大家從并發(fā)編程的基礎(chǔ)開始,逐步深入并發(fā)的核心殉疼。讓大家能夠系統(tǒng)的將并發(fā)全部知識(shí)點(diǎn)拿下逗嫡。
并發(fā)編程基礎(chǔ)
Java從誕生開始,其就已經(jīng)內(nèi)置了對(duì)于多線程的支持株依。當(dāng)多個(gè)線程能夠同時(shí)執(zhí)行時(shí)驱证,大多數(shù)情況下都能夠顯著提升系統(tǒng)性能,尤其現(xiàn)在的計(jì)算機(jī)普遍都是多核的恋腕,所以性能的提升會(huì)更加明顯抹锄。但是,多線程在使用中也需要注意諸多的問題荠藤,如果使用不當(dāng)伙单,也會(huì)對(duì)系統(tǒng)性能造成非常嚴(yán)重的影響。
進(jìn)程哈肖、線程吻育、協(xié)程
什么是進(jìn)程
進(jìn)程可以理解為就是應(yīng)用程序的啟動(dòng)實(shí)例。如微信淤井、Idea布疼、Navicat等摊趾,當(dāng)打開它們后,就相當(dāng)于開啟了一個(gè)進(jìn)程游两。每個(gè)進(jìn)程都會(huì)在操作系統(tǒng)中擁有獨(dú)立的內(nèi)存空間砾层、地址、文件資源贱案、數(shù)據(jù)資源等肛炮。進(jìn)程是資源分配和管理的最小單位。
1.1.2)什么是線程
線程從屬于進(jìn)程宝踪,是程序的實(shí)際執(zhí)行者侨糟,一個(gè)進(jìn)程中可以包含若干個(gè)線程,并且也可以把線程稱為輕量級(jí)進(jìn)程瘩燥。每個(gè)線程都會(huì)擁有自己的計(jì)數(shù)器秕重、堆棧、局部變量等屬性颤芬,并且能夠訪問共享的內(nèi)存變量悲幅。線程是操作系統(tǒng)(CPU)調(diào)度和執(zhí)行的最小單位。CPU會(huì)在這些線程上來回切換站蝠,讓使用者感覺線程是在同時(shí)執(zhí)行的汰具。
一個(gè)線程具有五種狀態(tài),分別為:新建菱魔、就緒留荔、運(yùn)行、阻塞澜倦、銷毀聚蝶。
同時(shí)對(duì)于線程狀態(tài)切換的工作,是由JVM中的TCB(Thread Control Block)來執(zhí)行藻治。
線程使用帶來的問題
有很多人都會(huì)存在一個(gè)誤區(qū)碘勉,在代碼中使用多線程,一定會(huì)為系統(tǒng)帶來性能提升桩卵,這個(gè)觀點(diǎn)是錯(cuò)誤的验靡。并發(fā)編程的目的是為了讓程序運(yùn)行的更快,但是雏节,絕對(duì)不是說啟動(dòng)的線程越多胜嗓,性能提升的就越大,其會(huì)受到很多因素的影響钩乍,如鎖問題辞州、線程狀態(tài)切換問題、線程上下文切換問題寥粹,還會(huì)受到硬件資源的影響变过,如CPU核數(shù)埃元。
什么叫做線程上下文切換
不管是在多核甚至單核處理器中,都是能夠以多線程形式執(zhí)行代碼的牵啦,CPU通過給每個(gè)線程分配CPU時(shí)間片來實(shí)現(xiàn)線程執(zhí)行間的快速切換亚情。 所謂的時(shí)間片就是CPU分配給每個(gè)線程的執(zhí)行時(shí)間妄痪,當(dāng)某個(gè)線程獲取到CPU時(shí)間片后哈雏,就會(huì)在一定時(shí)間內(nèi)執(zhí)行,當(dāng)時(shí)間片到期衫生,則該線程會(huì)進(jìn)入到掛起等待狀態(tài)裳瘪。時(shí)間片一般為幾十毫秒,通過在CPU的高速切換罪针,讓使用者感覺是在同時(shí)執(zhí)行彭羹。
同時(shí)還要保證線程在切換的過程中,要記錄線程被掛起時(shí)泪酱,已經(jīng)執(zhí)行了哪些指令滤淳、變量值是多少术健,那這點(diǎn)則是通過每個(gè)線程內(nèi)部的程序計(jì)數(shù)器來保證。
簡(jiǎn)單來說:線程從掛起到再加載的過程,就是一次上下文切換弯囊。其實(shí)比較耗費(fèi)資源的。
引起上下文切換的幾種情況:
- 時(shí)間片用完台妆,CPU正常調(diào)度下一個(gè)任務(wù)础锐。
- 被其他優(yōu)先級(jí)更高的任務(wù)搶占。
- 執(zhí)行任務(wù)碰到IO阻塞勿锅,調(diào)度器掛起當(dāng)前任務(wù)帕膜,切換執(zhí)行下一個(gè)任務(wù)。
- 用戶代碼主動(dòng)掛起當(dāng)前任務(wù)讓出CPU時(shí)間溢十。
- 多任務(wù)搶占資源垮刹,由于沒有搶到被掛起。
- 硬件中斷张弛。
生產(chǎn)者/消費(fèi)者模式實(shí)現(xiàn)
public class Producer extends Thread{
private static final int QUEUE_SIZE=5;
private final Queue queue;
public Producer(Queue queue){
super();
this.queue=queue;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (queue){
while (queue.size() >= QUEUE_SIZE){
System.out.println("隊(duì)列滿了荒典,等待消費(fèi)");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.add(i);
System.out.println("入隊(duì)數(shù)據(jù):"+i);
queue.notify();
}
}
}
}
import java.util.Queue;
public class Consumer extends Thread{
private final Queue queue;
public Consumer(Queue queue){
super();
this.queue=queue;
}
@Override
public void run() {
while (true){
synchronized (queue){
while (queue.size() ==0){
System.out.println("隊(duì)列沒有數(shù)據(jù),等待生產(chǎn)");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//取出數(shù)據(jù)
System.out.println("取出數(shù)據(jù):"+queue.poll());
queue.notify();
}
}
}
}
public class PCTest {
public static void main(String[] args) {
final Queue<Integer> queue = new LinkedList<>();
Producer producer = new Producer(queue);
Consumer consumer = new Consumer(queue);
producer.start();
consumer.start();
}
}
演示效果如下:
這種方式雖然實(shí)現(xiàn)了生產(chǎn)者/消費(fèi)者模式乌庶,但其性能非常低下种蝶。因?yàn)樯婕暗搅送芥i、線程等待與喚醒的狀態(tài)轉(zhuǎn)換和線程上下文切換瞒大。這些操作都是機(jī)器耗費(fèi)性能的螃征。
什么是協(xié)程
協(xié)程是一種比線程更加輕量級(jí)的存在,一個(gè)線程中可以包含若干個(gè)協(xié)程透敌。同時(shí)Java本身是并不支持協(xié)程的盯滚。但是如python踢械、go都是支持的。協(xié)程不是被操作系統(tǒng)內(nèi)核所管理魄藕,而完全是由程序所控制(也就是在用戶態(tài)執(zhí)行)内列,所以其非常輕量級(jí)。而線程是由操作系統(tǒng)內(nèi)核管理背率,所以是重量級(jí)的话瞧。只要是由程序也就是用戶態(tài)完成操作,無需經(jīng)過操作系統(tǒng)內(nèi)核都是輕量級(jí)的寝姿。一旦經(jīng)過操作系統(tǒng)內(nèi)核就是重量級(jí)的交排。協(xié)程的開銷遠(yuǎn)遠(yuǎn)小于線程的開銷。
CPU時(shí)間片輪轉(zhuǎn)機(jī)制&優(yōu)化
之前已經(jīng)提到了線程的執(zhí)行饵筑,是依賴于CPU給每個(gè)線程分配的時(shí)間來進(jìn)行埃篓。在CPU時(shí)間片輪轉(zhuǎn)機(jī)制中,如果一個(gè)線程的時(shí)間片到期根资,則CPU會(huì)掛起該線程并給另一個(gè)線程分片一定的時(shí)間分片架专。如果進(jìn)程在時(shí)間片結(jié)束前阻塞或結(jié)束,則 CPU 會(huì)立即進(jìn)行切換玄帕。調(diào)度程序所要做的就是維護(hù)一張就緒進(jìn)程列表,當(dāng)進(jìn)程用完它的時(shí)間片后,它被移到隊(duì)列的末尾部脚。
時(shí)間片太短會(huì)導(dǎo)致頻繁的進(jìn)程切換,降低了 CPU 效率: 而太長(zhǎng)又可能引起對(duì)短的交互請(qǐng)求的響應(yīng)變差桨仿。將時(shí)間片設(shè)為 100ms 通常是一個(gè)比較合理的折衷睛低。
并行與并發(fā)的理解
對(duì)于這兩個(gè)概念,如果剛看到的話服傍,可能會(huì)很不屑钱雷。但是真的理解什么叫做并行,什么叫做并發(fā)嗎吹零?
所謂并發(fā)即讓多個(gè)任務(wù)能夠交替執(zhí)行罩抗,一般都會(huì)附帶一個(gè)時(shí)間單位,也就是所謂的在單位時(shí)間內(nèi)的并發(fā)量有多少灿椅。
所謂并行即讓多個(gè)任務(wù)能夠同時(shí)執(zhí)行套蒂。比如說:你可以一遍上廁所,一遍吃飯茫蛹。
線程啟動(dòng)&中止
線程的實(shí)現(xiàn)方式有兩種:繼承Thread類操刀、實(shí)現(xiàn)Runnable接口。但是有一些書籍或者文章會(huì)說有三種方式婴洼,即實(shí)現(xiàn)Callable接口骨坑。但通過該接口定義線程并不是Java標(biāo)準(zhǔn)的定義方式,而是基于Future思想來完成。 Java官方說明中欢唾,已經(jīng)明確指出且警,只有兩種方式。
那么Thread和Runnable有什么區(qū)別和聯(lián)系呢礁遣? 一般來說斑芜,Thread是對(duì)一個(gè)線程的抽象,而Runnable是對(duì)業(yè)務(wù)邏輯的抽象祟霍,并且Thread 可以接受任意一個(gè) Runnable 的實(shí)例并執(zhí)行杏头。
線程啟動(dòng)
/**
* 新建線程
*/
public class NewThread {
private static class UseThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+": use thread");
}
}
private static class UseRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+": use runnable");
}
}
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+": use main");
UseThread useThread = new UseThread();
useThread.start();
UseRunnable useRunnable = new UseRunnable();
new Thread(useRunnable).start();
}
}
優(yōu)化:?jiǎn)?dòng)線程前,最好為這個(gè)線程設(shè)置特定的線程名稱浅碾,這樣在出現(xiàn)問題時(shí)大州,給開發(fā)人員一些提示续语,快速定位到問題線程垂谢。
Thread.currentThread().setName("Runnable demo");
線程中止
線程在正常下當(dāng)run執(zhí)行完,或出現(xiàn)異常都會(huì)讓該線程中止疮茄。
理解suspend()滥朱、resume()、stop()
這三個(gè)方法對(duì)應(yīng)的是暫停力试、恢復(fù)和中止徙邻。對(duì)于這三個(gè)方法的使用效果演示如下:
public class Srs {
private static class MyThread implements Runnable{
@Override
public void run() {
Thread.currentThread().setName("my thread");
DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
while (true){
System.out.println(Thread.currentThread().getName()+"run at"+dateFormat.format(new Date()));
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
Thread thread = new Thread(new MyThread());
//開啟線程
System.out.println("開啟線程");
thread.start();
TimeUnit.SECONDS.sleep(3);
//暫停線程
System.out.println("暫停線程");
thread.suspend();
TimeUnit.SECONDS.sleep(3);
//恢復(fù)線程
System.out.println("恢復(fù)線程");
thread.resume();
TimeUnit.SECONDS.sleep(3);
//中止線程
System.out.println("中止線程");
thread.stop();
}
}
執(zhí)行結(jié)果
可以看到這三個(gè)方式,很好的完成了其本職工作畸裳。但是三個(gè)已經(jīng)在Java源碼中被標(biāo)注為過期方法缰犁。那這三個(gè)方式為什么會(huì)被標(biāo)記為過期方法呢?
當(dāng)調(diào)用suspend()時(shí)怖糊,線程不會(huì)將當(dāng)前持有的資源釋放(如鎖)帅容,而是占有者資源進(jìn)入到暫停狀態(tài),這樣的話伍伤,容易造成死鎖問題的出現(xiàn)并徘。
public class Srs {
private static Object obj = new Object();//作為一個(gè)鎖
private static class MyThread implements Runnable{
@Override
public void run() {
synchronized (obj){
DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
while (true){
System.out.println(Thread.currentThread().getName()+"run at"+dateFormat.format(new Date()));
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyThread(),"正常線程");
Thread thread1 = new Thread(new MyThread(),"死鎖線程");
//開啟線程
thread.start();
TimeUnit.SECONDS.sleep(3);
//暫停線程
thread.suspend();
System.out.println("暫停線程");
thread1.start();
TimeUnit.SECONDS.sleep(3);
//恢復(fù)線程
/*thread.resume();
System.out.println("恢復(fù)線程");
TimeUnit.SECONDS.sleep(3);*/
//中止線程
/*thread.stop();
System.out.println("中止線程");*/
}
}
在上述代碼中,正常線程持有了鎖扰魂,當(dāng)調(diào)用suspend()時(shí)麦乞,因?yàn)樵摲椒ú粫?huì)釋放鎖,所以死鎖線程因?yàn)楂@取不到鎖而導(dǎo)致無法執(zhí)行劝评。所以該方法必須與resume()成對(duì)出現(xiàn)姐直。
當(dāng)調(diào)用stop()時(shí),會(huì)立即停止run()中剩余的操作蒋畜,包括在catch或finally語句中的內(nèi)容声畏。因此可能會(huì)導(dǎo)致一些收尾性的工作的得不到完成,如文件流百侧,數(shù)據(jù)庫(kù)等關(guān)閉砰识。并且會(huì)立即釋放該線程所持有的所有的鎖能扒,導(dǎo)致數(shù)據(jù)得不到同步的處理,出現(xiàn)數(shù)據(jù)不一致的問題辫狼。
public class StopProblem {
public static void main(String[] args) throws Exception {
TestObject testObject = new TestObject();
Thread t1 = new Thread(() -> {
try {
testObject.print("1", "2");
} catch (Exception e) {
e.printStackTrace();
}
});
t1.start();
//讓子線程有執(zhí)行時(shí)間
//Thread.sleep(1000);
t1.stop();
System.out.println("first : " + testObject.getFirst() + " " + "second : " + testObject.getSecond());
}
}
class TestObject {
private String first = "ja";
private String second = "va";
public synchronized void print(String first, String second) throws Exception {
System.out.println(Thread.currentThread().getName());
this.first = first;
//模擬數(shù)據(jù)不一致
//Thread.sleep(10000);
this.second = second;
}
public String getFirst() {
return first;
}
public String getSecond() {
return second;
}
}
線程中止的安全且優(yōu)雅姿勢(shì)
Java對(duì)于線程安全中止實(shí)現(xiàn)設(shè)計(jì)了一個(gè)中斷屬性初斑,其可以理解是線程的一個(gè)標(biāo)識(shí)位屬性。它用于表示一個(gè)運(yùn)行中的線程是否被其他線程進(jìn)行了中斷操作膨处。好比其他線程對(duì)這個(gè)線程打了一個(gè)招呼见秤,告訴它你該中斷了。通過interrupt()實(shí)現(xiàn)真椿。
public class InterruptDemo {
private static class MyThread implements Runnable{
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName()+" is running");
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyThread(),"myThread");
thread.start();
TimeUnit.SECONDS.sleep(3);
thread.interrupt();
}
}
添加該方法后鹃答,會(huì)出現(xiàn)一個(gè)異常,但是可以發(fā)現(xiàn)并不會(huì)線程的繼續(xù)執(zhí)行突硝。
線程通過通過檢查自身是否被中斷來進(jìn)行響應(yīng)测摔,可以通過isInterrupted()進(jìn)行判斷,如果返回值為true解恰,代表添加了中斷標(biāo)識(shí)锋八,返回false,代表沒有添加中斷標(biāo)識(shí)护盈。通過它可以對(duì)線程進(jìn)行中斷操作挟纱。
public class InterruptDemo {
private static class MyThread implements Runnable{
@Override
public void run() {
//while (true){
while (!Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName()+" is running");
}
System.out.println(Thread.currentThread().getName()+" Interrupt flag is : "+Thread.currentThread().isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyThread(),"myThread");
thread.start();
TimeUnit.SECONDS.sleep(3);
thread.interrupt();
}
}
對(duì)線程中斷屬性的判斷,可以利用其進(jìn)行線程執(zhí)行的中斷操作腐宋。
線程也可以通過靜態(tài)方法Thread.interrupted()對(duì)中斷標(biāo)識(shí)進(jìn)行復(fù)位紊服,如果該線程已經(jīng)被添加了中斷標(biāo)識(shí),當(dāng)使用了該方法后胸竞,會(huì)將線程的中斷標(biāo)識(shí)由true改為false欺嗤。
public class InterruptDemo {
private static class MyThread implements Runnable{
@Override
public void run() {
//while (true){
//while (!Thread.currentThread().isInterrupted()){
while (!Thread.interrupted()){
System.out.println(Thread.currentThread().getName()+" is running");
}
System.out.println(Thread.currentThread().getName()+" Interrupt flag is : "+Thread.currentThread().isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyThread(),"myThread");
thread.start();
TimeUnit.SECONDS.sleep(3);
thread.interrupt();
}
}
中斷線程的注意點(diǎn)
線程在執(zhí)行的過程中,可能會(huì)進(jìn)行中斷或者阻塞撤师,比方說使用了sleep()或者wait()剂府。那么在線程執(zhí)行中一旦出現(xiàn)這類操作的話,則會(huì)出現(xiàn)InterruptedException異常剃盾。但是并不會(huì)影響線程的繼續(xù)執(zhí)行腺占。
那么一般都會(huì)進(jìn)行try/catch的操作。具體效果如下:
public class InterruptDemo {
private static class MyThread implements Runnable{
@Override
public void run() {
while (!Thread.interrupted()){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName()+"in catch Interrupt flag is : "+Thread.currentThread().isInterrupted());
}
System.out.println(Thread.currentThread().getName()+" is running");
}
System.out.println(Thread.currentThread().getName()+" Interrupt flag is : "+Thread.currentThread().isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyThread(),"myThread");
thread.start();
TimeUnit.SECONDS.sleep(3);
thread.interrupt();
}
}
此時(shí)可以明顯觀察到痒谴,線程會(huì)繼續(xù)向下執(zhí)行衰伯,并且catch中的中斷標(biāo)記屬性為false。因?yàn)門hread.interrupted()不僅會(huì)判斷線程中斷標(biāo)記屬性积蔚,同時(shí)如果該線程的中斷屬性為true意鲸,會(huì)將true改變?yōu)閒alse。
myThread is running
myThread is running
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 base.InterruptDemo$MyThread.run(InterruptDemo.java:16)
at java.lang.Thread.run(Thread.java:745)
myThread in catch Interrupt flag is : false
myThread is running
myThread is running
myThread is running
myThread is running
此時(shí)既然捕捉到了InterruptedException異常,代表該線程應(yīng)該被中斷了怎顾,應(yīng)該讓線程停止读慎,并且在catch中要將線程中斷掉。所以注意槐雾,在使用時(shí)夭委,應(yīng)該在catch中再次執(zhí)行interrupt(),讓線程中斷掉募强。 這樣可以在catch中在設(shè)置中斷標(biāo)記前株灸,進(jìn)行資源釋放。
public class InterruptDemo {
private static class MyThread implements Runnable{
@Override
public void run() {
//while (true){
while (!Thread.currentThread().isInterrupted()){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+" in catch Interrupt flag is : "+Thread.currentThread().isInterrupted());
//todo:資源釋放
//設(shè)置中斷標(biāo)記
Thread.currentThread().interrupt();
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" is running");
}
System.out.println(Thread.currentThread().getName()+" Interrupt flag is : "+Thread.currentThread().isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyThread(),"myThread");
thread.start();
TimeUnit.SECONDS.sleep(3);
thread.interrupt();
}
}
同時(shí)要注意:處于死鎖下的線程擎值,無法被中斷
深入線程操作常見方法
理解run()&start()
這兩個(gè)方法都可以啟動(dòng)線程慌烧,但是它倆是有本質(zhì)上的區(qū)別的。當(dāng)線程執(zhí)行了 start()方法后鸠儿,才真正意義上的啟動(dòng)線程屹蚊,其會(huì)讓一個(gè)線程進(jìn)入就緒隊(duì)列等到分配CPU時(shí)間片,分到時(shí)間片后才會(huì)調(diào)用run()捆交。注意淑翼,同一個(gè)線程的start()不能被重復(fù)調(diào)用,否則會(huì)出現(xiàn)異常品追,因?yàn)橹貜?fù)調(diào)用了,start方法冯丙,線程的state就不是new了肉瓦,那么threadStatus就不等于0了。
//start源碼分析
public synchronized void start() {
/**
Java里面創(chuàng)建線程之后胃惜,必須要調(diào)用start方法才能創(chuàng)建一個(gè)線程泞莉,該方法會(huì)通過虛擬機(jī)啟動(dòng)一個(gè)本地線程,本地線程的創(chuàng)建會(huì)調(diào)用當(dāng)前系統(tǒng)去創(chuàng)建線程的方法進(jìn)行創(chuàng)建線程船殉。
最終會(huì)調(diào)用run()將線程真正執(zhí)行起來
0這個(gè)狀態(tài)鲫趁,等于‘New’這個(gè)狀態(tài)。
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* 線程會(huì)加入到線程分組利虫,然后執(zhí)行start0() */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
執(zhí)行流程圖
而run()則僅僅是一個(gè)普通方法挨厚,與類中的成員方法意義相同。在該方法中可以實(shí)現(xiàn)線程執(zhí)行的業(yè)務(wù)邏輯糠惫。但并不會(huì)以異步的方式將線程啟動(dòng)疫剃,換句話說就是并不會(huì)去開啟一個(gè)新的線程。其可以單獨(dú)執(zhí)行硼讽,也可以重復(fù)執(zhí)行巢价。
理解yield()
當(dāng)某個(gè)線程調(diào)用了這個(gè)方法后,該線程立即釋放自己持有的時(shí)間片。線程會(huì)進(jìn)入到就緒狀態(tài)壤躲,同時(shí)CPU會(huì)重新選擇一個(gè)線程賦予時(shí)間分片城菊,但注意,調(diào)用了這個(gè)方法的線程碉克,也有可能被CPU再次選中賦予執(zhí)行役电。
而且該方法不會(huì)釋放鎖。 如需釋放鎖的話棉胀,可以在調(diào)用該方法前自己手動(dòng)釋放法瑟。
public class Demo {
private static class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i==5){
System.out.println(Thread.currentThread().getName());
yield();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new MyThread());
Thread thread2 = new Thread(new MyThread());
Thread thread3 = new Thread(new MyThread());
thread1.start();
thread2.start();
thread3.start();
}
}
從結(jié)果看出,當(dāng)調(diào)用了該方法后線程會(huì)讓出自己的時(shí)間分片唁奢,但也有可能被再次選中執(zhí)行霎挟。
理解join()
該方法的使用,在實(shí)際開發(fā)中麻掸,應(yīng)用的是比較少的酥夭。但在面試中,常常伴隨著產(chǎn)生一個(gè)問題脊奋,如何保證線程的執(zhí)行順序熬北? 就可以通過該方法來設(shè)置。
使用
當(dāng)線程調(diào)用了該方法后诚隙,線程狀態(tài)會(huì)從就緒狀態(tài)進(jìn)入到運(yùn)行狀態(tài)讶隐。
public class JoinDemo {
private static class MyThread extends Thread{
int i;
Thread previousThread; //上一個(gè)線程
public MyThread(Thread previousThread,int i){
this.previousThread=previousThread;
this.i=i;
}
@Override
public void run() {
//調(diào)用上一個(gè)線程的join方法. 不使用join方法解決是不確定的
//previousThread.join();
System.out.println("num:"+i);
}
}
public static void main(String[] args) {
Thread previousThread=Thread.currentThread();
for(int i=0;i<10;i++){
//每一個(gè)線程實(shí)現(xiàn)都持有前一個(gè)線程的引用。
MyThread joinDemo=new MyThread(previousThread,i);
joinDemo.start();
previousThread=joinDemo;
}
}
}
可是等到開啟了join之后久又,結(jié)果就是有序的了巫延。
根據(jù)結(jié)果可以看到,當(dāng)前線程需要等待previousThread線程終止之后才從thread.join返回地消÷澹可以理解完,線程會(huì)在join處等待脉执。
原理剖析
//源碼解析
public final void join() throws InterruptedException {
join(0);
}
...
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
//判斷是否攜帶阻塞的超時(shí)時(shí)間疼阔,等于0表示沒有設(shè)置超時(shí)時(shí)間
if (millis == 0) {
//isAlive獲取線程狀態(tài),無限等待直到previousThread線程結(jié)束
while (isAlive()) {
//調(diào)用Object中的wait方法實(shí)現(xiàn)線程的阻塞
wait(0);
}
} else { //阻塞直到超時(shí)
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
可以看到剛方法是被synchronized修飾的半夷,因?yàn)樵谄鋬?nèi)部對(duì)于線程阻塞的實(shí)現(xiàn)婆廊,是通過Object中wait方法實(shí)現(xiàn)的,而要調(diào)用wait()玻熙,則必須添加synchronized否彩。
為什么join()阻塞的是主線程呢?按照上述的理解嗦随,很多人認(rèn)為其應(yīng)該阻塞的是previousThread列荔。實(shí)際上主線程會(huì)持有previousThread這個(gè)對(duì)象的鎖敬尺,然后調(diào)用wait方法去阻塞,而這個(gè)方法的調(diào)用者是在主線程中的贴浙。所以造成主線程阻塞砂吞。
為什么previousThread線程執(zhí)行完畢就能夠喚醒住線程呢?或者說是在什么時(shí)候喚醒的崎溃?對(duì)于wait的使用蜻直,對(duì)應(yīng)的會(huì)有notify或notifyAll。在JVM內(nèi)部袁串,會(huì)設(shè)置native線程對(duì)象為null同時(shí)調(diào)用notifyAll喚醒所有線程概而。
總的來說,Thread.join其實(shí)底層是通過wait/notifyall來實(shí)現(xiàn)線程的通信達(dá)到線程阻塞的目的囱修;當(dāng)線程執(zhí)行結(jié)束以后赎瑰,會(huì)觸發(fā)兩個(gè)事情,第一個(gè)是設(shè)置native線程對(duì)象為null破镰、第二個(gè)是通過notifyall方法餐曼,讓等待在previousThread對(duì)象鎖上的wait方法被喚醒。
線程優(yōu)先級(jí)
操作對(duì)于線程執(zhí)行鲜漩,是通過CPU時(shí)間片來調(diào)用運(yùn)行的源譬。那么一個(gè)線程被分配的時(shí)間片的多少,就決定了其使用資源的多少孕似。而線程優(yōu)先級(jí)就是決定線程需要能夠使用資源多少的線程屬性踩娘。
線程優(yōu)先級(jí)的范圍是1~10。一個(gè)線程的默認(rèn)優(yōu)先級(jí)是5鳞青,可以在構(gòu)建線程時(shí)霸饲,通過setPriority()修改該線程的優(yōu)先級(jí)。優(yōu)先級(jí)高的線程分配時(shí)間片的數(shù)量會(huì)高于優(yōu)先級(jí)低的線程臂拓。
一般來說對(duì)于頻繁阻塞的線程需要設(shè)置優(yōu)先級(jí)高點(diǎn),而偏重計(jì)算的線程優(yōu)先級(jí)會(huì)設(shè)置低些习寸,確保處理器不會(huì)被獨(dú)占胶惰。
但注意,線程優(yōu)先級(jí)不能作為線程執(zhí)行正確性的依賴霞溪,因?yàn)椴煌牟僮飨到y(tǒng)可能會(huì)忽略優(yōu)先級(jí)的設(shè)置孵滞。
守護(hù)線程
守護(hù)線程是一種支持形的線程,我們之前創(chuàng)建的線程都可以稱之為用戶線程鸯匹。通過守護(hù)線程可以完成一些支持性的工作坊饶,如GC、分布式鎖續(xù)期殴蓬。守護(hù)線程會(huì)伴隨著用戶線程的結(jié)束而結(jié)束匿级。
對(duì)于守護(hù)線程的創(chuàng)建蟋滴,可以通過setDaemon()設(shè)置。
public class DaemonDemo {
private static class MyThread implements Runnable{
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
try {
System.out.println(Thread.currentThread().getName());
} finally {
System.out.println("thread run into finally");
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyThread());
//設(shè)置守護(hù)線程
thread.setDaemon(true);
thread.start();
TimeUnit.SECONDS.sleep(5);
}
}
當(dāng)線程實(shí)例沒有被設(shè)置為守護(hù)線程時(shí)痘绎,該線程并不會(huì)隨著主線程的結(jié)束而結(jié)束津函。但是當(dāng)被設(shè)置為守護(hù)線程后,當(dāng)主線程結(jié)束孤页,該線程也會(huì)伴隨著結(jié)束尔苦。同時(shí)守護(hù)線程不一定會(huì)執(zhí)行finally代碼塊。所以當(dāng)線程被設(shè)定為守護(hù)線程后行施,無法確保清理資源等操作一定會(huì)被執(zhí)行允坚。
線程狀態(tài)
理解了上述方法后,再來看一下這些對(duì)于線程狀態(tài)轉(zhuǎn)換能起到什么樣的影響蛾号。