多執行緒基礎入門
什麼是執行緒
說到執行緒,不得不提程式,對於程式相信大家都不陌生。比如當我們啟動qq的時候,作業系統會給qq程式建立一個程式,啟動桌面版微信,作業系統也會給微信建立一個程式,同理,java程式啟動後,也會建立一個程式。
根據狹義的定義,程式就是正在執行程式的抽象。
話說回來,那什麼是執行緒呢?
在某些程式內部,還需要同時執行一些子任務,比如在一個Java程式中,後臺除了執行正常使用者程式碼之外,可能還需要執行緒在後臺執行垃圾回收,即時編譯等,我們稱這些子任務為執行緒。我們可以理解執行緒為一種輕量級的程式,它們有自己的程式計數器、棧以及區域性變數等,可以被作業系統進行排程,而且相比程式而言,執行緒建立、上下文切換的代價都更小,所以現代作業系統都是以執行緒作為排程的最小單位。
程式和執行緒的關係
-
程式是系統進行資源分配基本單位,執行緒是系統排程的基本單位。
-
一個程式可以包含一個或多個執行緒。
-
同一個程式所有執行緒可共享該程式的資源,如記憶體空間等。
-
程式之間通訊較為複雜
-
同一臺計算機內部的程式通訊,稱為IPC(Inter-Process Communication)
-
不同計算機之間程式通訊,需要透過網路,遵循共同的協議,如TCP/IP
-
-
執行緒之間通訊比較方便,因為同一個程式中所有執行緒共享記憶體,比如多個執行緒可以訪問同一個共享變數。
我們可以把程式理解成一個營業的酒店,而執行緒就是酒店的老闆及工作人員如大堂經理、保潔阿姨、保安、廚師等。酒店的老闆及工作人員,都能共用酒店的資源。一個酒店再怎麼樣,就算沒有任何工作人員,也必須有一個老闆才行。
並行和併發
cpu執行程式碼是一條一條順序執行的,但是,即便是單核CPU,也可以同時執行多個任務。這是因為作業系統會讓多個任務輪流交替執行,每個任務執行若干時間,執行完後切換到下一個任務執行,這個過程非常快,造成一種同時執行的假象,這種在宏觀上同時執行,微觀上交替執行
的現象,我們稱之為***併發(Concurrency)***。
當然,對於擁有多核CPU的計算機而言,是可以允許多個CPU同時執行不同任務的,比如在CPU1執行Word,在CPU2上執行QQ音樂聽歌,這種真正意義上的同時執行,我們稱為***並行(Parallelism)***。
引用golang語言創造者Rob Pike的一段話:
- 併發是同一時間,應對(dealing with)多件事情的能力。
- 並行,是同一時間動手做(doing)多件事情的能力。
併發和並行的區別,有點類似於以下場景:
高速公路上設有收費站,假設有兩條道路,但是收費通道只有一個,這時兩個道路的車,就需要交替排隊進入同一個收費入口進行收費,這種情況類似於併發,假如有兩個收費入口,兩條道的車都在自己的收費入口收費,這種情況類似於並行。
建立執行緒的方式
在java中建立並使用執行緒非常簡單,只需要建立一個執行緒物件,並呼叫其start方法,就可以了,比如下面這樣:
public class CreateThread {
public static void main(String[] args) {
Thread t1 = new Thread();
t1.start();
}
}
當我們執行這段程式的時候,jvm實際首先會建立一個主執行緒,用來執行main()
方法,然後在執行main()
方法的第3行程式碼時,會建立再次建立執行緒t1,在第4行透過start()
,啟動執行緒t1。不過這個執行緒啟動後,實際並沒有執行任何程式碼就結束了,如果我們希望執行緒啟動後能執行指定程式碼,可以透過以下兩種方式:
繼承Thread類並重寫run方法
public class ExtendThread {
public static void main(String[] args) {
Thread t1 = new MyThread();
t1.start();
}
public static class MyThread extends Thread{
@Override
public void run() {
Debug.debug("我是執行緒t1");
}
}
}
上述方法可以簡寫成匿名內部類的形式:
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
Debug.debug("我是執行緒t1");
}
};
t1.start();
}
建立執行緒時傳入Runnable物件
public class RunnableThread {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
Debug.debug("我是執行緒t2");
}
};
Thread t2 = new Thread(runnable);
t2.start();
}
}
從java8開始,這種方式可以使用lamda表示式簡寫
public class LambdaRunnableThread {
public static void main(String[] args) {
Thread t2 = new Thread(() -> Debug.debug("我是執行緒t2"));
t2.start();
}
}
那麼,這兩種寫法更推薦哪種呢?一般而言下,更建議使用第二種方法,因為
- java裡面類是單繼承的,使用介面的方式,可以避開這種限制。
- 有利於任務拆分。如果一個任務需要拆分成很多小任務,不必為每個任務建立一個執行緒。
- 將任務的建立和執行解耦,一個執行緒生產任務,可以交給其他執行緒去執行。
run()和start()的區別
需要特別注意的是,run()
和start()
的區別,執行緒建立完成後,執行start()
方法,才會真正啟動執行緒去併發執行任務,而run()
只是一個普通的例項方法,沒有啟動執行緒的作用。
public class StartAndRunTest {
public static void main(String[] args) {
Debug.debug("我是執行緒:{}",Thread.currentThread().getName());
Thread t2 = new Thread(() -> Debug.debug("我是執行緒:{}",Thread.currentThread().getName()),"t2");
t2.run();
}
}
以上程式碼中Thread.currentThread().getName()
會列印當前執行執行緒的名字。這段程式碼首先會列印主執行緒的名字,然後建立執行緒t2,接著啟動執行緒t2,t2執行緒啟動後執行其任務程式碼,會列印出正在執行該程式碼的執行緒名字也就是t2,其執行結果如下:
2021-03-07 20:07:05 [main] 我是執行緒:main
2021-03-07 20:07:05 [t2] 我是執行緒:t2
如果我們把程式碼第5行改成: t2.run()
,就會輸出下面的結果了:
2021-03-07 20:07:19 [main] 我是執行緒:main
2021-03-07 20:07:19 [main] 我是執行緒:main
因為run()
方法並沒有真正啟動執行緒t1,只是在主執行緒中呼叫了t2執行緒的一個普通方法。
執行緒的常用api
名稱 | 型別 | 作用 |
---|---|---|
sleep(long millis) | 靜態方法 | 使當前執行緒休眠millis毫秒 |
yield | 靜態方法 | 當前執行緒讓出cpu |
join | 執行緒例項方法 | 等待直到某個執行緒執行完畢再執行後續程式碼 |
join(long millis) | 執行緒例項方法 | 等待某個執行緒執行完畢再執行後續程式碼,最多等待millis毫秒 |
sleep
sleep(long millis)
是一個靜態方法,其作用是使當前執行緒進入休眠millis毫秒,比如下面這個例子,我們讓執行緒休眠5s再執行
public class SleepTest {
public static void main(String[] args) {
new Thread(() -> {
Debug.debug("開始執行");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Debug.debug("執行結束");
},"t1").start();
}
}
執行結果如下:
2021-03-07 20:09:24 [t1] 開始執行
2021-03-07 20:09:29 [t1] 執行結束
需要注意的是,sleep()
會丟擲InterruptedException異常,這個異常的作用是讓我們可以中斷正在休眠中的執行緒。
yield
yield的翻譯過來是屈服、讓步的意思,由此可以看出,yield()
方法的作用是讓當前執行緒主動讓出cpu,從執行狀態變成就緒狀態,相當於是把執行機會讓給其他執行緒,但不一定能成功讓出。打個簡單的比方,就像是你在排隊買車票,本來輪到你了,這時後面有個人因為時間比較趕,於是你非常紳士的把位置讓給他。那麼這個方法的應用場景是什麼呢?看了該方法的註釋,發現原來這個方法實際上很少有機會用到,主要用於程式碼除錯,復現bug。
/**
* A hint to the scheduler that the current thread is willing to yield
* its current use of a processor. The scheduler is free to ignore this
* hint.
*
* Yield is a heuristic attempt to improve relative progression
* between threads that would otherwise over-utilise a CPU. Its use
* should be combined with detailed profiling and benchmarking to
* ensure that it actually has the desired effect.
*
*
It is rarely appropriate to use this method. It may be useful
* for debugging or testing purposes, where it may help to reproduce
* bugs due to race conditions. It may also be useful when designing
* concurrency control constructs such as the ones in the
* {@link java.util.concurrent.locks} package.
*/
public static native void yield();
join
在多執行緒應用中,假如執行緒A的輸入依賴於執行緒B的輸出結果,此時,執行緒A就需要等待執行緒B執行完畢再繼續執行,我們可以使用jdk提供的join()
方法來實現這種執行緒之間的協作。如下所示有兩個join方法:
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
無參的join表示A執行緒會無限等待直到執行緒B執行完畢,而有參的join方法,會等待直到最大超時時間,超出這個時間後哪怕執行緒B還在執行,就不再繼續等待。透過下面這個簡單的例子,我們來驗證一下join()
的作用:
public class JoinMain {
public static void main(String[] args) throws InterruptedException {
MyTask myTask = new MyTask();
Thread t = new Thread(myTask);
t.start();
//t.join();
System.out.println(myTask.result);
}
private static class MyTask implements Runnable{
private int result;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
result = 100;
}
}
}
這段程式碼的執行結果是0,而不是100,因為main執行緒執行第7行程式碼的時候,執行緒t還處於休眠狀態,此時result還是等於0,當我們去掉第7行的註釋,main執行緒等待t執行緒執行完畢,設定了result的值,才執行第8行程式碼,結果就是100了。
守護執行緒
java執行緒可以分為守護執行緒和非守護執行緒,在java程式中,一旦非守護執行緒全部執行完畢,即便守護執行緒還沒執行完,該程式也會強行終止。顧名思義,守護執行緒和它的名字一樣,就是在後臺默默的做一些系統工作,比如java程式中的垃圾回收執行緒、JIT執行緒就是守護執行緒,正因為如此,當系統中沒有其他執行緒後,守護執行緒也就失去了存在的意義,無事可做,整個程式自然也就應該結束了。守護執行緒可以在建立執行緒後透過setDeamon()
進行設定。舉個例子:
public class DaemonThreadMain {
public static void main(String[] args) {
Thread t = new Thread(()->{
Debug.debug("開始執行");
Sleep.seconds(3);//睡眠3s
Debug.debug("執行結束");
},"t1");
// t.setDaemon(true);
t.start();
Sleep.seconds(1);
Debug.debug("執行結束");
}
}
執行結果如下:
2021-03-06 20:47:59 [t1] 開始執行
2021-03-06 20:48:00 [main] 執行結束
2021-03-06 20:48:02 [t1] 執行結束
可以看出,執行緒t1執行耗時3s,main執行緒耗時1s,main執行緒1s後就執行完畢退出了,而t1執行緒作為非守護執行緒,在主執行緒結束後,依然是過了3s後才執行完畢。現在我們把第8行t.setDaemon(true)
這行程式碼去掉註釋,最終執行結果如下:
2021-03-06 20:52:29 [t1] 開始執行
2021-03-06 20:52:30 [main] 執行結束
可以看出,由於t1是守護執行緒,主執行緒退出後,執行緒t1就退出而沒有往下執行了。
需要注意的是,setDameon()
方法必須線上程開始也就是呼叫start()
執行之前呼叫,否則會報以下異常:
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.setDaemon(Thread.java:1359)
at com.taoge.demos.DaemonThreadMain.main(DaemonThreadMain.java:20)
附錄-工具類 Sleep & Debug
sleep
/**
* Sleep 是對Thread.sleep的簡單封裝
*
* @author chentao
* @date 2021/3/6
*/
public final class Sleep {
public static void sleep(TimeUnit unit, long duration){
try {
unit.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void sleepInterruptibly(TimeUnit unit, long duration) throws InterruptedException{
unit.sleep(duration);
}
public static void millis(long millis){
sleep(TimeUnit.MILLISECONDS, millis);
}
public static void seconds(long seconds){
sleep(TimeUnit.SECONDS, seconds);
}
}
Debug
/**
* Debug類是對System.out.println的簡單封裝,便於列印出類似這樣格式化的日誌:
* 2021-03-07 20:11:06 [t1] 這是測試
* 包含了日期時間、執行緒名稱和自定義的列印內容
* @author chentao
* @date 2021/3/4
*/
public class Debug {
private static SimpleDateFormat format = new SimpleDateFormat();
static {
format.applyPattern("yyyy-MM-dd HH:mm:ss");
}
public static void debug(String msg, Object... params){
for (Object param : params) {
msg = msg.replaceFirst("\{\}",param.toString());
}
System.out.println(format.format(new Date())+" ["+Thread.currentThread().getName()+"] "+msg);
}
}
原始碼地址
> 文章所有程式碼都放在github上
>
> github.com/ThomasChant/jucDemos
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4289/viewspace-2797975/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Java-基礎-執行緒入門Java執行緒
- 多執行緒系列(1),多執行緒基礎執行緒
- 多執行緒系列(二):多執行緒基礎執行緒
- 多執行緒基礎執行緒
- Java多執行緒(一)多執行緒入門篇Java執行緒
- 多執行緒學習一(多執行緒基礎)執行緒
- Java多執行緒入門Java執行緒
- java - 多執行緒基礎Java執行緒
- Java—多執行緒基礎Java執行緒
- Java 多執行緒基礎(四)執行緒安全Java執行緒
- 多執行緒系列(三):執行緒池基礎執行緒
- 【Python入門基礎】程式和執行緒Python執行緒
- 多執行緒基礎-基礎實現執行緒
- 多執行緒與高併發(一)多執行緒入門執行緒
- Java多執行緒學習(一)Java多執行緒入門Java執行緒
- Java 多執行緒基礎(八)執行緒讓步Java執行緒
- python 多執行緒 入門Python執行緒
- Java 多執行緒基礎 - CyclicBarrierJava執行緒
- Java多執行緒-基礎篇Java執行緒
- pthread 多執行緒基礎thread執行緒
- 基礎鞏固 --多執行緒執行緒
- 多執行緒基礎知識執行緒
- python多執行緒基礎Python執行緒
- 入門python多執行緒/多程式Python執行緒
- day20_多執行緒入門丶執行緒安全執行緒
- C#多執行緒開發-執行緒基礎 01C#執行緒
- Java入門教程十三(多執行緒)Java執行緒
- Android入門教程 | 多執行緒Android執行緒
- Linux程式多執行緒入門Linux執行緒
- C++多執行緒基礎教程C++執行緒
- 併發與多執行緒基礎執行緒
- JAVA_基礎多執行緒(一)Java執行緒
- JAVA多執行緒-基礎篇-synchronizedJava執行緒synchronized
- go語言多執行緒入門筆記-執行緒同步Go執行緒筆記
- 多執行緒程式設計基礎(一)-- 執行緒的使用執行緒程式設計
- Java 多執行緒基礎(六)執行緒等待與喚醒Java執行緒
- 細說C#多執行緒那些事:執行緒基礎C#執行緒
- 多執行緒基礎必要知識點!看了學習多執行緒事半功倍執行緒