【原創】Java併發編程系列2:線程概念與基礎操作 偉大的理想只有經過忘我的鬥爭和犧牲才能勝利實現。 本篇為【Dali王的技術博客】Java併發編程系列第二篇,講講有關線程的那些事兒。主要內容是如下這些: 線程概念 線程基礎操作 線程概念 進程代表了運行中的程式,一個運行的Java程式就是一個進程。 ...
【原創】Java併發編程系列2:線程概念與基礎操作
偉大的理想只有經過忘我的鬥爭和犧牲才能勝利實現。
本篇為【Dali王的技術博客】Java併發編程系列第二篇,講講有關線程的那些事兒。主要內容是如下這些:
- 線程概念
- 線程基礎操作
線程概念
進程代表了運行中的程式,一個運行的Java程式就是一個進程。在Java中,當我們啟動main函數時就啟動了一個JVM的進程,而main函數所在的線程就是這個進程中的一個線程,稱為主線程。
進程和線程的關係如下圖所示:
由上圖可以看出來,一個進程中有多個線程,多個線程共用進程的堆的方法區資源,但是每個線程有自己的程式計數器和棧區域。
線程基礎操作
線程創建與運行
Java中有三種線程創建方式,分別為:繼承Thread類並重寫run方法,實現Runnable介面的run方法,使用FutureTask方式。
先看繼承Thread方式的實現,代碼示例如下:
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函數里創建了一個DemoThread的實例,然後調用其start方法啟動了線程。
tips:調用start方法後線程並沒有馬上執行,而是處於就緒狀態,也就是這個線程已經獲取了除CPU資源外的其他資源,等待獲取CPU資源後才會真正處於運行狀態。
使用繼承方式,好處在於通過this就可以獲取當前線程,缺點在於Java不支持多繼承,如果繼承了Thread類,那麼就不能再繼承其他類。而且任務與代碼耦合嚴重,一個線程類只能執行一個任務,使用Runnable則沒有這個限制。
來看實現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添加參數進行任務區分。在Java8中,可以使用Lambda表達式對上述代碼進行簡化:
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();
}
上面兩種方式都有一個缺點,就是任務沒有返回值,下麵看第三種,使用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. 可復用的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實現了Callable介面的call方法,在main函數中使用CallableDemo的實例創建了一個FutureTask,然後使用創建的FutureTask對象作為任務創建了一個線程並啟動它,最後通過FutureTask等待任務執行完畢並返回結果。
同樣的,上面的操作過程適合於需要復用的任務,如果對於一次性的任務,大可以通過Lambda來簡化代碼,如註釋2處。
等待線程終止
在項目中經常會遇到一個場景,就是需要等待某幾件事情完成後才能繼續往下執行。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");
}
上面代碼在主線程里啟動了兩個線程,然後分別調用了它們的join方法,主線程會在調用t1.join()後被阻塞,等待其執行完畢後返回;然後主線程調用t2.join()後再次被阻塞,等待t2執行完畢後返回。上面代碼的執行結果如下:
main thread starts
main thread waits child threads to be over
this is thread 1
this is thread 2
child threads are over
需要註意的是,線程1調用線程2的join方法後會被阻塞,當其他線程調用了線程1的interrupt方法中斷了線程1時,線程1會拋出一個InterruptedException異常而返回。
讓線程睡眠
Thread類中有一個static的sleep方法,當一個執行中的線程調用了Thread的sleep方法後,調用線程會暫時讓出指定時間的執行權,也就是在這期間不參與CPU的調度,但是該線程所擁有的監視器資源,比如鎖還是不讓出的。指定的睡眠時間到了後該函數會正常返回,線程就處於就緒狀態,然後等待CPU的調度執行。
tips:面試當中wait和sleep經常會被用來比較,需要多加體會二者的區別。
調用某個對象的wait()方法,相當於讓當前線程交出此對象的monitor,然後進入等待狀態,等待後續再次獲得此對象的鎖;notify()方法能夠喚醒一個正在等待該對象的monitor的線程,當有多個線程都在等待該對象的monitor的話,則只能喚醒其中一個線程,具體喚醒哪個線程則不得而知。
調用某個對象的wait()方法和notify()方法,當前線程必須擁有這個對象的monitor,因此調用wait()方法和notify()方法必須在同步塊或者同步方法中進行(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();
}
上面的代碼創建了一個獨占鎖,然後創建了兩個線程,每個線程在內部先獲取鎖,然後睡眠,睡眠結束後會釋放鎖。執行結果如下:
thread1 get to sleep
thread1 is awake
thread2 get to sleep
thread2 is awake
從執行結果來看,線程1先獲取鎖,然後睡眠,再被喚醒,之後才輪到線程2獲取到鎖,也即線上程1sleep期間,線程1並沒有釋放鎖。
需要註意的是,如果子線程在睡眠期間,主線程中斷了它,子線程就會在調用sleep方法處拋出了InterruptedException異常。
線程讓出CPU
Thread類中有一個static的yield方法,當一個線程調用yield方法時,實際就是暗示線程調度器當前線程請求讓出自己的CPU使用,如果該線程還有沒用完的時間片也會放棄,這意味著線程調度器可以進行下一輪的線程調度了。
當一個線程調用yield方法時,當前線程會讓出CPU使用權,然後處於就緒狀態,線程調度器會從線程就緒隊列裡面獲取一個線程優先順序最高的線程,當然也有可能會調度到剛剛讓出CPU的那個線程來獲取CPU執行權。
請看代碼示例:
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();
}
在如上的代碼中,兩個線程的功能一樣,運行多次,同一線程的兩行輸出是順序的,但是整體順序是不確定的,取決於線程調度器的調度情況。
當把上面代碼中1和2處代碼註釋掉,會發現結果只有一個,如下:
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
從結果可知,Thread.yiled方法生效使得兩個線程分別在執行過程中放棄CPU,然後在調度另一個線程,這裡的兩個線程有點互相謙讓的感覺,最終是由於只有兩個線程,最終還是執行完了兩個任務。
tips:sleep和yield的區別:
當線程調用sleep方法時,調用線程會阻塞掛起指定的時間,在這期間線程調度器不會去調度該線程。而調用yield方法時,線程只是讓出自己剩餘的時間片,並沒有被阻塞掛起,而是出於就緒狀態,線程調度器下一次調度時就可能調度到當前線程執行。
線程中斷
Java中的線程中斷是一種線程間的協作模式。每個線程對象里都有一個boolean類型的標識(通過isInterrupted()方法返回),代表著是否有中斷請求(interrupt()方法)。例如,當線程t1想中斷線程t2,只需要線上程t1中將線程t2對象的中斷標識置為true,然後線程2可以選擇在合適的時候處理該中斷請求,甚至可以不理會該請求,就像這個線程沒有被中斷一樣。
在上面章節中也講到了線程中斷的一些內容,此處就不再用代碼來展開了。
Java併發編程大綱
繼續附上Java編程的系統學習大綱以供參考:
Java併發編程.png
【參考資料】
- 《Java併發編程之美》
本文由微型公眾號【Dali王的技術博客】原創,掃碼關註獲取更多原創技術文章。