【原創(chuàng)】Java并發(fā)編程系列2:線程概念與基礎(chǔ)操作
偉大的理想只有經(jīng)過忘我的斗爭和犧牲才能勝利實現(xiàn)。
本篇為【Dali王的技術(shù)博客】Java并發(fā)編程系列第二篇薪寓,講講有關(guān)線程的那些事兒硼端。主要內(nèi)容是如下這些:
- 線程概念
- 線程基礎(chǔ)操作
線程概念
進(jìn)程代表了運行中的程序屋匕,一個運行的Java程序就是一個進(jìn)程旗笔。在Java中倒脓,當(dāng)我們啟動main函數(shù)時就啟動了一個JVM的進(jìn)程,而main函數(shù)所在的線程就是這個進(jìn)程中的一個線程里伯,稱為主線程城瞎。
進(jìn)程和線程的關(guān)系如下圖所示:
由上圖可以看出來,一個進(jìn)程中有多個線程疾瓮,多個線程共享進(jìn)程的堆的方法區(qū)資源脖镀,但是每個線程有自己的程序計數(shù)器和棧區(qū)域。
線程基礎(chǔ)操作
線程創(chuàng)建與運行
Java中有三種線程創(chuàng)建方式狼电,分別為:繼承Thread類并重寫run方法蜒灰,實現(xiàn)Runnable接口的run方法,使用FutureTask方式肩碟。
先看繼承Thread方式的實現(xiàn)卷员,代碼示例如下:
public class ThreadDemo {
public static class DemoThread extends Thread {
@Override
public void run() {
System.out.println("this is a child thread.");
}
}
public static void main(String[] args) {
System.out.println("this is main thread.")
DemoThread thread = new DemoThread();
thread.start();
}
}
上面代碼中DemoThread類繼承了Thread類,并重寫了run方法腾务。在main函數(shù)里創(chuàng)建了一個DemoThread的實例,然后調(diào)用其start方法啟動了線程削饵。
tips:調(diào)用start方法后線程并沒有馬上執(zhí)行岩瘦,而是處于就緒狀態(tài)未巫,也就是這個線程已經(jīng)獲取了除CPU資源外的其他資源,等待獲取CPU資源后才會真正處于運行狀態(tài)启昧。
使用繼承方式叙凡,好處在于通過this就可以獲取當(dāng)前線程,缺點在于Java不支持多繼承密末,如果繼承了Thread類握爷,那么就不能再繼承其他類。而且任務(wù)與代碼耦合嚴(yán)重严里,一個線程類只能執(zhí)行一個任務(wù)新啼,使用Runnable則沒有這個限制。
來看實現(xiàn)Runnable接口的run方法的方式刹碾,代碼示例如下:
public class RunnableDemo {
public static class DemoRunnable implements Runnable {
@Override
public void run() {
System.out.println("this is a child thread.");
}
}
public static void main(String[] args) {
System.out.println("this is main thread.");
DemoRunnable runnable = new DemoRunnable();
new Thread(runnable).start();
new Thread(runnable).start();
}
}
上面代碼兩個線程共用一個Runnable邏輯燥撞,如果需要,可以給RunnableTask添加參數(shù)進(jìn)行任務(wù)區(qū)分迷帜。在Java8中物舒,可以使用Lambda表達(dá)式對上述代碼進(jìn)行簡化:
public static void main(String[] args) {
System.out.println("this is main thread.");
Thread t = new Thread(() -> System.out.println("this is child thread"));
t.start();
}
上面兩種方式都有一個缺點,就是任務(wù)沒有返回值戏锹,下面看第三種冠胯,使用FutureTask的方式。代碼示例如下:
public class CallableDemo implements Callable<JsonObject> {
@Override
public JsonObject call() throws Exception {
return new JsonObject();
}
public static void main(String[] args) {
System.out.println("this is main thread.");
FutureTask<JsonObject> futureTask = new FutureTask<>(new CallableDemo()); // 1. 可復(fù)用的FutureTask
new Thread(futureTask).start();
try {
JsonObject result = futureTask.get();
System.out.println(result.toString());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
// 2. 一次性的FutureTask
FutureTask<JsonObject> innerFutureTask = new FutureTask<>(() -> {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("name", "Dali");
return jsonObject;
});
new Thread(innerFutureTask).start();
try {
JsonObject innerResult = innerFutureTask.get();
System.out.println(innerResult.toString());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
如上代碼锦针,CallableDemo實現(xiàn)了Callable接口的call方法荠察,在main函數(shù)中使用CallableDemo的實例創(chuàng)建了一個FutureTask,然后使用創(chuàng)建的FutureTask對象作為任務(wù)創(chuàng)建了一個線程并啟動它伞插,最后通過FutureTask等待任務(wù)執(zhí)行完畢并返回結(jié)果割粮。
同樣的,上面的操作過程適合于需要復(fù)用的任務(wù)媚污,如果對于一次性的任務(wù)舀瓢,大可以通過Lambda來簡化代碼,如注釋2處耗美。
等待線程終止
在項目中經(jīng)常會遇到一個場景京髓,就是需要等待某幾件事情完成后才能繼續(xù)往下執(zhí)行。Thread類中有一個join方法就可以用來處理這種場景商架。直接上代碼示例:
public static void main(String[] args) throws InterruptedException {
System.out.println("main thread starts");
Thread t1 = new Thread(() -> System.out.println("this is thread 1"));
Thread t2 = new Thread(() -> System.out.println("this is thread 2"));
t1.start();
t2.start();
System.out.println("main thread waits child threads to be over");
t1.join();
t2.join();
System.out.println("child threads are over");
}
上面代碼在主線程里啟動了兩個線程堰怨,然后分別調(diào)用了它們的join方法,主線程會在調(diào)用t1.join()后被阻塞蛇摸,等待其執(zhí)行完畢后返回备图;然后主線程調(diào)用t2.join()后再次被阻塞,等待t2執(zhí)行完畢后返回。上面代碼的執(zhí)行結(jié)果如下:
main thread starts
main thread waits child threads to be over
this is thread 1
this is thread 2
child threads are over
需要注意的是揽涮,線程1調(diào)用線程2的join方法后會被阻塞抠藕,當(dāng)其他線程調(diào)用了線程1的interrupt方法中斷了線程1時,線程1會拋出一個InterruptedException異常而返回蒋困。
讓線程睡眠
Thread類中有一個static的sleep方法盾似,當(dāng)一個執(zhí)行中的線程調(diào)用了Thread的sleep方法后,調(diào)用線程會暫時讓出指定時間的執(zhí)行權(quán)雪标,也就是在這期間不參與CPU的調(diào)度零院,但是該線程所擁有的監(jiān)視器資源,比如鎖還是不讓出的村刨。指定的睡眠時間到了后該函數(shù)會正常返回告抄,線程就處于就緒狀態(tài),然后等待CPU的調(diào)度執(zhí)行烹困。
tips:面試當(dāng)中wait和sleep經(jīng)常會被用來比較玄妈,需要多加體會二者的區(qū)別。
調(diào)用某個對象的wait()方法髓梅,相當(dāng)于讓當(dāng)前線程交出此對象的monitor拟蜻,然后進(jìn)入等待狀態(tài),等待后續(xù)再次獲得此對象的鎖枯饿;notify()方法能夠喚醒一個正在等待該對象的monitor的線程酝锅,當(dāng)有多個線程都在等待該對象的monitor的話,則只能喚醒其中一個線程奢方,具體喚醒哪個線程則不得而知搔扁。
調(diào)用某個對象的wait()方法和notify()方法,當(dāng)前線程必須擁有這個對象的monitor蟋字,因此調(diào)用wait()方法和notify()方法必須在同步塊或者同步方法中進(jìn)行(synchronized塊或者synchronized方法)稿蹲。
看一個線程睡眠的代碼示例:
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
// 獲取獨占鎖
lock.lock();
System.out.println("thread1 get to sleep");
try {
Thread.sleep(1000);
System.out.println("thread1 is awake");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
Thread t2 = new Thread(() -> {
// 獲取獨占鎖
lock.lock();
System.out.println("thread2 get to sleep");
try {
Thread.sleep(1000);
System.out.println("thread2 is awake");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
t1.start();
t2.start();
}
上面的代碼創(chuàng)建了一個獨占鎖,然后創(chuàng)建了兩個線程鹊奖,每個線程在內(nèi)部先獲取鎖苛聘,然后睡眠,睡眠結(jié)束后會釋放鎖忠聚。執(zhí)行結(jié)果如下:
thread1 get to sleep
thread1 is awake
thread2 get to sleep
thread2 is awake
從執(zhí)行結(jié)果來看设哗,線程1先獲取鎖,然后睡眠两蟀,再被喚醒网梢,之后才輪到線程2獲取到鎖,也即在線程1sleep期間赂毯,線程1并沒有釋放鎖战虏。
需要注意的是拣宰,如果子線程在睡眠期間,主線程中斷了它活烙,子線程就會在調(diào)用sleep方法處拋出了InterruptedException異常徐裸。
線程讓出CPU
Thread類中有一個static的yield方法,當(dāng)一個線程調(diào)用yield方法時啸盏,實際就是暗示線程調(diào)度器當(dāng)前線程請求讓出自己的CPU使用,如果該線程還有沒用完的時間片也會放棄骑祟,這意味著線程調(diào)度器可以進(jìn)行下一輪的線程調(diào)度了回懦。
當(dāng)一個線程調(diào)用yield方法時,當(dāng)前線程會讓出CPU使用權(quán)次企,然后處于就緒狀態(tài)怯晕,線程調(diào)度器會從線程就緒隊列里面獲取一個線程優(yōu)先級最高的線程,當(dāng)然也有可能會調(diào)度到剛剛讓出CPU的那個線程來獲取CPU執(zhí)行權(quán)缸棵。
請看代碼示例:
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
if (i == 8) {
System.out.println("current thread: " + Thread.currentThread() + " yield cpu");
}
Thread.yield(); // 2
}
System.out.println("current thread: " + Thread.currentThread() + " is over");
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
if (i == 8) {
System.out.println("current thread: " + Thread.currentThread() + " yield cpu");
}
Thread.yield(); // 1
}
System.out.println("current thread: " + Thread.currentThread() + " is over");
});
t1.start();
t2.start();
}
在如上的代碼中舟茶,兩個線程的功能一樣,運行多次堵第,同一線程的兩行輸出是順序的吧凉,但是整體順序是不確定的,取決于線程調(diào)度器的調(diào)度情況踏志。
當(dāng)把上面代碼中1和2處代碼注釋掉阀捅,會發(fā)現(xiàn)結(jié)果只有一個,如下:
current thread: Thread[Thread-1,5,main] yield cpu
current thread: Thread[Thread-0,5,main] yield cpu
current thread: Thread[Thread-1,5,main] is over
current thread: Thread[Thread-0,5,main] is over
從結(jié)果可知针余,Thread.yiled方法生效使得兩個線程分別在執(zhí)行過程中放棄CPU饲鄙,然后在調(diào)度另一個線程,這里的兩個線程有點互相謙讓的感覺圆雁,最終是由于只有兩個線程忍级,最終還是執(zhí)行完了兩個任務(wù)。
tips:sleep和yield的區(qū)別:
當(dāng)線程調(diào)用sleep方法時伪朽,調(diào)用線程會阻塞掛起指定的時間轴咱,在這期間線程調(diào)度器不會去調(diào)度該線程。而調(diào)用yield方法時驱负,線程只是讓出自己剩余的時間片嗦玖,并沒有被阻塞掛起,而是出于就緒狀態(tài)跃脊,線程調(diào)度器下一次調(diào)度時就可能調(diào)度到當(dāng)前線程執(zhí)行宇挫。
線程中斷
Java中的線程中斷是一種線程間的協(xié)作模式。每個線程對象里都有一個boolean類型的標(biāo)識(通過isInterrupted()方法返回)酪术,代表著是否有中斷請求(interrupt()方法)器瘪。例如翠储,當(dāng)線程t1想中斷線程t2,只需要在線程t1中將線程t2對象的中斷標(biāo)識置為true橡疼,然后線程2可以選擇在合適的時候處理該中斷請求援所,甚至可以不理會該請求,就像這個線程沒有被中斷一樣欣除。
在上面章節(jié)中也講到了線程中斷的一些內(nèi)容住拭,此處就不再用代碼來展開了。
Java并發(fā)編程大綱
繼續(xù)附上Java編程的系統(tǒng)學(xué)習(xí)大綱以供參考:
【參考資料】
- 《Java并發(fā)編程之美》
本文由微型公眾號【Dali王的技術(shù)博客】原創(chuàng)历帚,掃碼關(guān)注獲取更多原創(chuàng)技術(shù)文章滔岳。
[圖片上傳失敗...(image-68f97d-1584803811516)]