掌握高併發、高可用架構
第二課 併發編程
從本課開始學習併發編程的內容。主要介紹併發編程的基礎知識、鎖、內存模型、線程池、各種併發容器的使用。
第二節 線程
併發編程
併發基礎
進程
線程
線程通信
上一節學習了進程和線程的關係,CPU和線程的關係。在程序開發過程中,最主要的還是線程,畢竟它是用來執行任務的。所以就需要知道,如何啓動和停止線程;線程的狀態;線程間如何通信。
線程的啓動
- 實現
Runnable
接口,然後當成Thread
的構造參數生成線程對象,調用t.start()
方法
public class MyThread implements Runnable {
@Override
public void run() {
System.out.println("thread02");
}
public static void main(String[] args) {
Thread t = new Thread(new MyThread());
t.start();
}
}
這是線程最本質的實現。Thread
類實現了Runnable
接口,在執行t.start()
時,會調用Thread
的run()
方法,從而間接調用target.run()
。
Thread類實現Runnalbe接口:
public class Thread implements Runnable {
private Runnable target;
public void run() {
if (target != null) {
target.run();
}
}
}
2 繼承Thread類,然後調用start()
方法
public class MyThread extends Thread {
public void run() {
System.out.println("thread01");
}
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
由於Thread
實現了Runnable
,所以繼承Thread
來重寫run()
方法的本質依然是實現Runnable
接口的定義。此時,由於target
對象爲null
,所以Thread
的run()
方法不會執行target.run()
,而是直接執行自定義的run()
方法。
3 實現Callable
接口,並通過FutureTask
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return null;
}
public static void main(String[] args) {
MyCallable m = new MyCallable();
FutureTask<String> f = new FutureTask<>(m);
Thread t = new Thread(f);
t.start();
String result = f.get(); // 同步獲取任務執行結果
System.out.println(result);
}
}
由於FutureTask
實現了RunnableTask
接口,而RunnableTask
又實現了Runnable
和Future
接口,因此在構造Thread
時,FutureTask
還是被轉型爲Runnable
來使用了。
前兩種方法只能執行任務,而不能得到任務的結果;第三種方法可以通過FutureTask
的get()
方法同步的獲取任務結果。當任務執行中時,其會阻塞直到任務完成。
4 匿名內部類
public class DemoThread {
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
//...
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
//...
}
}).start();
}
}
5 Lambda表達式
public class Demo {
public static void main(String[] args) {
new Thread(() -> System.out.println("running")).start();
}
}
6 線程池
public class MyThreadPool implements Runnable {
@Override
public void run() {
// TODO
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
MyThreadPool m = new MyThreadPool();
exec.execute(m);
}
}
把任務的執行交給ExecutorService
去處理,最終還是利用Thread
創建線程。優點是線程的複用,省去了每個線程的創建和銷燬過程,從而提高效率。
7 定時器
public class MyTimer {
public static void main(String[] args) {
Timer t = new Timer();
t.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
// TODO
}
}, 2000, 1000);
}
}
TimerTask實現了Runnable
接口,Timer
內部有個TimerThread
繼承了Thread
,所以還是Thread
+Runnable
。
線程的停止
- 當線程的
run
方法執行完成後,線程自動釋放資源進而終止。 - 在另外的線程中調用
interrupt
來中斷某個線程。這是線程間通信,我們後續再講
線程的狀態
先上圖(借用CSDN博主 潘建南 的圖)。
所以,線程的狀態一共有6種。下面咱們來詳細講解。
- 初始狀態 NEW
通過實現Runnable
或繼承Thread
得到一個線程類,並使用new
創建出一個線程對象,就進入了初始狀態。此時,還未調用start
方法。
- 運行狀態 RUNNABLE
JAVA中將 就緒(READY)和 運行中(RUNNING)兩種狀態統稱爲“運行”狀態。
就緒 READY:就是說線程有資格運行,但此時調度程序還未選擇線程。當以下行爲發生時,線程進入就緒狀態。
- 調用線程的
start
方法 - 當前線程的
sleep
結束 - 其他線程
join
結束 - 等待用戶輸入,但用戶輸入完畢
- 線程拿到對象鎖
- 當前線程的時間片用完了
- 調用當前線程的
yield
方法
運行中 RUNNING:調度程序從就緒的線程池中選擇一個線程使其成爲當前線程,此時線程處於的狀態就是運行中。
- 阻塞 BLOCKED
阻塞狀態是線程在獲取對象的同步鎖synchorized
時,因爲該鎖被其他線程佔用而放棄CPU使用權,暫時停止運行的狀態。此時的線程會被JVM放入鎖池中。
- 等待 WAITING
運行的線程執行wait()
方法,會釋放線程佔用的所有資源,並進入等待池中。此時,線程是不能自動喚醒的,必須依靠其他線程調用notify()
或notifyAll()
方法才能喚醒。
- 超時等待 TIMED_WAITING
運行的線程執行sleep()
或join()
方法,或者發出I/O請求時的狀態。此時線程會放棄CPU使用權。當sleep()
超時、join()
等待線程終止或超時、I/O處理完畢時,重新轉入就緒。
- 終止 TERMINATED
線程執行完成或因異常而退出run
方法體的狀態。
線程各個狀態之間的跳轉,可以仔細看圖。
線程間通信
- 通過共享變量通信
在共享對象的變量中設置信號量。線程A修改信號量的值,線程B根據信號量來做不同的處理。
- 通過
wait()
、notify()
、notifyAll()
來通信
JAVA要求wait()
、notify()
、notifyAll()
必須在同步代碼塊中使用。就是說,必須要獲得對象鎖。所以wait()
、notify()
、notifyAll()
經常和sychronized
搭配使用。
執行了鎖定對象的wait()
方法後,當前線程會釋放獲得的對象鎖,進入鎖定對象的等待池
在執行同步代碼塊的過程中,如果調用Thread.sleep()
或Thread.yield()
,當前線程只是放棄CPU,並不會釋放對象鎖
JOIN
作用:讓 主線程 等待 子線程 執行完成再繼續運行。
// 主線程
public class Father extends Thread {
public void run() {
Son son = new Son();
son.start();
son.join();
...
}
}
// 子線程
public class Son extends Thread {
public void run() {
...
}
}
在Father主線程中,先啓動Son子線程,然後調用son.join()
,此時,Father主線程會一直等待,直到子線程執行完成,才能繼續運行。
分析源碼可以知道,JOIN的實現原理是:只要子線程是活動的,就一直觸發主線程的wait()
方法,使其一直處於等待狀態。
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");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
yield
調用yield()
方法,意思是放棄CPU使用權,回到就緒狀態。