多線程對于 Android 開發者來說是基礎。而且這類知識在計算機里也是很重要的一環,所以很有必要整理一番。
- 文章來源:itsCoder 的 WeeklyBolg 項目
- itsCoder主頁:http://itscoder.com/
- 作者:謝三弟
- 審閱者:Jaeger
目錄
多線程的實現
來上代碼:
// 最常見的兩種方法啟動新的線程
public static void startThread() {
// 覆蓋 run 方法
new Thread() {
@Override
public void run() {
// 耗時操作
}
}.start();
// 傳入 Runnable 對象
new Thread(new Runnable() {
public void run() {
// 耗時操作
}
}).start();
}
其實第一個就是在 Thread 里覆寫了 run()
函數,第二個是給 Thread 傳了一個 Runnable 對象,在 Runnable 對象 run()
方法里進行耗時操作。
以前沒有怎么考慮過他們兩者的關系,今天我們來具體看看到底是什么鬼?
Thread 源碼
進入 Thread 源碼我們看看:
public class Thread implements Runnable {
/* What will be run. */
private Runnable target;
/* The group of this thread */
private ThreadGroup group;
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
}
源碼很長,我進行了一點分割。一點一點的來解析看看。
我們首先知道 Thread 也是一個 Runnable ,它實現了 Runnable 接口,并且在 Thread 類中有一個 Runnable 類型的 target 對象。
構造方法里我們都會調用 init()
方法,接下來看看在該方法里做了如何的初始化配置。
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
// group 參數如果為 null ,則獲得當前線程的 group(線程組)
if (g == null) {
g = parent.getThreadGroup();
}
// 代碼省略
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
// 設置 target( Runnable 類型 )
this.target = target;
}
public synchronized void start() {
// 將當前線程加入線程組
group.add(this);
boolean started = false;
try {
// 啟動 native 方法啟動新的線程
start0();
started = true;
} finally {
// 代碼省略
}
private native void start0();
@Override
public void run() {
if (target != null) {
target.run();
}
}
從上我們可以明白,最終被線程執行的任務是 Runnable ,Thread 只是對 Runnable 的一個包裝,并且通過一些狀態對 Thread 進行管理和調度。
當啟動一個線程時,如果 Thread 的 target 不為空,則會在子線程中執行這個 target 的 run()
函數,否則虛擬機就會執行該線程自身的 run()
函數。
線程的幾個重要的函數
- wait()
當一個線程執行到 wait() 方法時,它就進入到一個和該對象相關的等待池中,同時失去(釋放)了對象的機鎖,使得其他線程可以訪問。用戶可以使用 notify 、notifyAll 或者指定睡眠時間來喚醒當前等待池中的線程。
注意:wait() notify() notifyAll()
必須放在synchronized
block 中,否則會拋出異常。 - sleep()
該函數是 Thread 的靜態函數,作用是使調用線程進入睡眠狀態。因為sleep()
是 Thread 類的靜態方法,因此他不能改變對象的機鎖。所以,當在一個synchronized
塊中調用sleep()
方法時,線程雖然休眠了,但是對象的機鎖并沒有被釋放,其他線程無法訪問這個對象。 - join()
等待目標線程執行完成之后繼續執行。 - yield()
線程禮讓。目前線程由運行狀態轉換為就緒狀態,也就是讓出執行權限,讓其他線程得以優先執行,但其他線程能否優先執行未知。
在源碼中,查看 Thread 里的 State ,對幾種狀態解釋的很清楚。
NEW 狀態是指線程剛創建,尚未啟動
RUNNABLE 狀態是線程正在正常運行中,當然可能會有某種耗時計算 / IO 等待的操作 / CPU 時間片切換等, 這個狀態下發生的等待一般是其他系統資源, 而不是鎖, Sleep 等
BLOCKED 這個狀態下,是在多個線程有同步操作的場景, 比如正在等待另一個線程的 synchronized 塊的執行釋放,或者可重入的 synchronized 塊里別人調用 wait() 方法,也就是這時線程在等待進入臨界區
WAITING 這個狀態下是指線程擁有了某個鎖之后,調用了他的 wait 方法,等待其他線程 / 鎖擁有者調用 notify / notifyAll 一遍該線程可以繼續下一步操作,這里要區分 BLOCKED 和 WATING ,一個是在臨界點外面等待進入, 一個是在臨界點里面 wait 等待別人 notify , 線程調用了 join 方法 進入另外的線程的時候, 也會進入 WAITING 狀態,等待被他 join 的線程執行結束
TIMED_WAITING 這個狀態就是有限的 (時間限制) 的 WAITING, 一般出現在調用
wait(long), join(long)
等情況下,另外,一個線程 sleep 后, 也會進入 TIMED_WAITING 狀態TERMINATED 這個狀態下表示 該線程的 run 方法已經執行完畢了, 基本上就等于死亡了 (當時如果線程被持久持有, 可能不會被回收)
Wait() 的實踐
我們來看一段,wait()
的用途和效果。
static void waitAndNotifyAll() {
System.out.println("主線程運行");
Thread thread = new WaitThread();
thread.start();
long startTime = System.currentTimeMillis();
try {
synchronized (sLockOject) {
System.out.println("主線程等待");
sLockOject.wait();
}
} catch (Exception e) {
}
long timeMs = System.currentTimeMillis() - startTime;
System.out.println("主線程繼續 —-> 等待耗時:" + timeMs + " ms");
}
static class WaitThread extends Thread {
@Override
public void run() {
try {
synchronized (sLockOject) {
System.out.println("進入子線程");
Thread.sleep(3000);
System.out.println("喚醒主線程");
sLockOject.notifyAll();
}
} catch (Exception e) {
}
}
}
在 waitAndNotifyAll()
函數里,會啟動一個 WaitThread 線程,在該線程中將會調用 sleep 函數睡眠 3 秒。線程啟動之后在主線程調用 sLockOject 的 wait()
函數,使主線程進入等待狀態,此時將不會繼續執行。等 WaitThread 在 run()
函數沉睡了 3 秒后會調用 sLockOject 的 notifyAll()
函數,此時就會重新喚醒正在等待中的主線程,因此會繼續往下執行。
結果如下:
主線程運行
主線程等待
進入子線程
喚醒主線程
主線程繼續 —-> 等待耗時:3005 ms
wait()、notify()
機制通常用于等待機制的實現,當條件未滿足時調用 wait 進入等待狀態,一旦條件滿足,調用 notify
或 notifyAll
喚醒等待的線程繼續執行。
對于這里細節可能會有一些疑問。</br>
在子線程啟動的時候,
run()
函數里面已經持有了該對象鎖。</br>但是真實環境下,其實是主線程先持有對象鎖,然后調用
wait()
進入等待區并且釋放鎖等待喚醒。
這個問題涉及到 JNI 代碼,目前我只能從理論上來解釋這個問題。
我們都知道一個線程 start()
并不是馬上啟動,而是需要 CPU 分配資源的,根據目前運行來看,分配資源的時間大于 Java 虛擬機運行指令的時間,所以主線程比子線程先拿到鎖。
我們還可以知道一點,控制臺打印出的時間是 3005 ms ,在代碼里我們只等待了 3s 多出來的 5ms (這個數字會浮動)我們可以推斷是,子線程獲取 CPU 的時間加上喚醒主線程的時間。
上述只是自己的一個猜測,能力還有欠缺,準備深入學習。
不過推薦大家看看這篇文章 Synchnornized 在 JVM 下的實現 - 簡書。
Join() 的實踐
join()
的注釋上面寫著:
Waits for this thread to die.
意思是,阻塞當前調用 join()
函數所在的線程,直到接收線程執行完畢之后再繼續。
我們來看看實踐代碼:
public class JoinThread {
public static void main(String[] args) {
joinDemo();
}
public static void joinDemo() {
Worker worker1 = new Worker("work-1");
Worker worker2 = new Worker("work-2");
worker1.start();
System.out.println("啟動線程 1 ");
try {
// 調用 worker1 的 join 函數,主線,程會阻塞直到 woker1 執行完成
worker1.join();
System.out.println("啟動線程 2");
// 再啟動線程 2 ,并且調用線程 2 的 join 函數,主線程會阻塞直到 woker2 執行完成
worker2.start();
worker2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主線程繼續執行");
}
static class Worker extends Thread {
public Worker(String name) {
super(name);
}
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("work in " + getName());
}
}
}
運行之后我們得到:
啟動線程 1
work in work-1
啟動線程 2
work in work-2
主線程繼續執行
在 joinDemo()
方法里我們創建兩個子線程,然后啟動了 work1 線程,下一步調用了 woker1 的 join()
函數。此時,主線程會進入阻塞狀態,直到 work1 執行完畢之后才開始繼續執行。因為 Worker 的 run()
方法里會休眠 2 秒,因此線程每次調用了 join()
方法實際上都會阻塞 2 秒,直到 run()
方法執行完畢再繼續。
所以,上述代碼邏輯其實就是:
啟動線程1 —-> 等待線程 1 執行完畢 —-> 啟動線程2 —-> 等待線程 2 執行完畢 —-> 繼續執行主線程代碼
Yield() 的實踐
yield()
是 Thread 的靜態方法,注釋上說:
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.
大致意思是說:當前線程讓出執行時間給其他的線程。
我們都知道,線程的執行是有時間片的,每個線程輪流占用 CPU 固定時間,執行周期到了之后讓出執行權給其他線程。
yield()
就是主動讓出執行權給其他線程。
來看看我們實踐的代碼:
public class YieldThreadTest {
public static void main(String[] args) {
YieldTread t1 = new YieldTread("thread-1");
YieldTread t2 = new YieldTread("thread-2");
t1.start();
t2.start();
}
public static class YieldTread extends Thread {
public YieldTread(String name) {
super(name);
}
public synchronized void run() {
for (int i = 0; i < 5; i++) {
System.out.printf("%s 優先級為 [%d] -------> %d\n", this.getName(), this.getPriority(), i);
// 當 i 為 2 時,調用當前線程的 yield 函數
if (i == 2) {
Thread.yield();
}
}
}
}
}
在 main()
方法里創建了兩個 YieldTread 線程,控制臺輸出結果如下:
thread-1 優先級為 [5] -------> 0
thread-1 優先級為 [5] -------> 1
thread-1 優先級為 [5] -------> 2thread-2 優先級為 [5] -------> 0
thread-2 優先級為 [5] -------> 1
thread-2 優先級為 [5] -------> 2thread-1 優先級為 [5] -------> 3
thread-1 優先級為 [5] -------> 4
thread-2 優先級為 [5] -------> 3
thread-2 優先級為 [5] -------> 4
通常情況下 t1 首先執行,讓 t1 的 run()
函數執行到了 i 等于 2 時讓出當前線程的執行時間。所以我們看到前三行都是 t1 在執行,讓出執行時間后 t2 開始執行。后面邏輯簡單思考下就得知了,這里也不做過多詮釋。
因此,調用 yield()
就是讓出當前線程的執行權,這樣一來讓其他線程得到優先執行。
總結與參考
本章內容屬于線程的基礎,本系列會更新到線程池相關。
這章內容也及其重要,因為它是后面的基礎。
正確理解才能讓我們對各種線程問題有方向和思路。
參考讀物: