多線程對于 Android 開發(fā)者來說是基礎(chǔ)。而且這類知識在計算機(jī)里也是很重要的一環(huán)音比,所以很有必要整理一番俭尖。
- 文章來源:itsCoder 的 WeeklyBolg 項目
- itsCoder主頁:http://itscoder.com/
- 作者:謝三弟
- 審閱者:Jaeger
目錄
多線程的實現(xiàn)
來上代碼:
// 最常見的兩種方法啟動新的線程
public static void startThread() {
// 覆蓋 run 方法
new Thread() {
@Override
public void run() {
// 耗時操作
}
}.start();
// 傳入 Runnable 對象
new Thread(new Runnable() {
public void run() {
// 耗時操作
}
}).start();
}
其實第一個就是在 Thread 里覆寫了 run()
函數(shù),第二個是給 Thread 傳了一個 Runnable 對象,在 Runnable 對象 run()
方法里進(jìn)行耗時操作稽犁。
以前沒有怎么考慮過他們兩者的關(guān)系焰望,今天我們來具體看看到底是什么鬼?
Thread 源碼
進(jìn)入 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);
}
}
源碼很長已亥,我進(jìn)行了一點分割熊赖。一點一點的來解析看看。
我們首先知道 Thread 也是一個 Runnable 虑椎,它實現(xiàn)了 Runnable 接口震鹉,并且在 Thread 類中有一個 Runnable 類型的 target 對象。
構(gòu)造方法里我們都會調(diào)用 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 參數(shù)如果為 null ,則獲得當(dāng)前線程的 group(線程組)
if (g == null) {
g = parent.getThreadGroup();
}
// 代碼省略
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
// 設(shè)置 target( Runnable 類型 )
this.target = target;
}
public synchronized void start() {
// 將當(dāng)前線程加入線程組
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();
}
}
從上我們可以明白泥技,最終被線程執(zhí)行的任務(wù)是 Runnable 浆兰,Thread 只是對 Runnable 的一個包裝,并且通過一些狀態(tài)對 Thread 進(jìn)行管理和調(diào)度珊豹。
當(dāng)啟動一個線程時簸呈,如果 Thread 的 target 不為空,則會在子線程中執(zhí)行這個 target 的 run()
函數(shù)店茶,否則虛擬機(jī)就會執(zhí)行該線程自身的 run()
函數(shù)蜕便。
線程的幾個重要的函數(shù)
- wait()
當(dāng)一個線程執(zhí)行到 wait() 方法時,它就進(jìn)入到一個和該對象相關(guān)的等待池中贩幻,同時失去(釋放)了對象的機(jī)鎖轿腺,使得其他線程可以訪問。用戶可以使用 notify 段直、notifyAll 或者指定睡眠時間來喚醒當(dāng)前等待池中的線程吃溅。
注意:wait() notify() notifyAll()
必須放在synchronized
block 中,否則會拋出異常鸯檬。 - sleep()
該函數(shù)是 Thread 的靜態(tài)函數(shù)决侈,作用是使調(diào)用線程進(jìn)入睡眠狀態(tài)。因為sleep()
是 Thread 類的靜態(tài)方法喧务,因此他不能改變對象的機(jī)鎖赖歌。所以,當(dāng)在一個synchronized
塊中調(diào)用sleep()
方法時功茴,線程雖然休眠了庐冯,但是對象的機(jī)鎖并沒有被釋放,其他線程無法訪問這個對象坎穿。 - join()
等待目標(biāo)線程執(zhí)行完成之后繼續(xù)執(zhí)行展父。 - yield()
線程禮讓返劲。目前線程由運(yùn)行狀態(tài)轉(zhuǎn)換為就緒狀態(tài),也就是讓出執(zhí)行權(quán)限栖茉,讓其他線程得以優(yōu)先執(zhí)行篮绿,但其他線程能否優(yōu)先執(zhí)行未知。
在源碼中吕漂,查看 Thread 里的 State 亲配,對幾種狀態(tài)解釋的很清楚。
NEW 狀態(tài)是指線程剛創(chuàng)建惶凝,尚未啟動
RUNNABLE 狀態(tài)是線程正在正常運(yùn)行中吼虎,當(dāng)然可能會有某種耗時計算 / IO 等待的操作 / CPU 時間片切換等, 這個狀態(tài)下發(fā)生的等待一般是其他系統(tǒng)資源, 而不是鎖, Sleep 等
BLOCKED 這個狀態(tài)下,是在多個線程有同步操作的場景, 比如正在等待另一個線程的 synchronized 塊的執(zhí)行釋放苍鲜,或者可重入的 synchronized 塊里別人調(diào)用 wait() 方法思灰,也就是這時線程在等待進(jìn)入臨界區(qū)
WAITING 這個狀態(tài)下是指線程擁有了某個鎖之后,調(diào)用了他的 wait 方法坡贺,等待其他線程 / 鎖擁有者調(diào)用 notify / notifyAll 一遍該線程可以繼續(xù)下一步操作官辈,這里要區(qū)分 BLOCKED 和 WATING 箱舞,一個是在臨界點外面等待進(jìn)入遍坟, 一個是在臨界點里面 wait 等待別人 notify , 線程調(diào)用了 join 方法 進(jìn)入另外的線程的時候, 也會進(jìn)入 WAITING 狀態(tài)晴股,等待被他 join 的線程執(zhí)行結(jié)束
TIMED_WAITING 這個狀態(tài)就是有限的 (時間限制) 的 WAITING愿伴, 一般出現(xiàn)在調(diào)用
wait(long), join(long)
等情況下,另外电湘,一個線程 sleep 后, 也會進(jìn)入 TIMED_WAITING 狀態(tài)TERMINATED 這個狀態(tài)下表示 該線程的 run 方法已經(jīng)執(zhí)行完畢了, 基本上就等于死亡了 (當(dāng)時如果線程被持久持有, 可能不會被回收)
Wait() 的實踐
我們來看一段隔节,wait()
的用途和效果。
static void waitAndNotifyAll() {
System.out.println("主線程運(yùn)行");
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("主線程繼續(xù) —-> 等待耗時:" + timeMs + " ms");
}
static class WaitThread extends Thread {
@Override
public void run() {
try {
synchronized (sLockOject) {
System.out.println("進(jìn)入子線程");
Thread.sleep(3000);
System.out.println("喚醒主線程");
sLockOject.notifyAll();
}
} catch (Exception e) {
}
}
}
在 waitAndNotifyAll()
函數(shù)里寂呛,會啟動一個 WaitThread 線程怎诫,在該線程中將會調(diào)用 sleep 函數(shù)睡眠 3 秒。線程啟動之后在主線程調(diào)用 sLockOject 的 wait()
函數(shù)贷痪,使主線程進(jìn)入等待狀態(tài)幻妓,此時將不會繼續(xù)執(zhí)行。等 WaitThread 在 run()
函數(shù)沉睡了 3 秒后會調(diào)用 sLockOject 的 notifyAll()
函數(shù)劫拢,此時就會重新喚醒正在等待中的主線程肉津,因此會繼續(xù)往下執(zhí)行。
結(jié)果如下:
主線程運(yùn)行
主線程等待
進(jìn)入子線程
喚醒主線程
主線程繼續(xù) —-> 等待耗時:3005 ms
wait()舱沧、notify()
機(jī)制通常用于等待機(jī)制的實現(xiàn)妹沙,當(dāng)條件未滿足時調(diào)用 wait 進(jìn)入等待狀態(tài),一旦條件滿足熟吏,調(diào)用 notify
或 notifyAll
喚醒等待的線程繼續(xù)執(zhí)行距糖。
對于這里細(xì)節(jié)可能會有一些疑問玄窝。</br>
在子線程啟動的時候,
run()
函數(shù)里面已經(jīng)持有了該對象鎖悍引。</br>但是真實環(huán)境下哆料,其實是主線程先持有對象鎖,然后調(diào)用
wait()
進(jìn)入等待區(qū)并且釋放鎖等待喚醒吗铐。
這個問題涉及到 JNI 代碼东亦,目前我只能從理論上來解釋這個問題。
我們都知道一個線程 start()
并不是馬上啟動唬渗,而是需要 CPU 分配資源的典阵,根據(jù)目前運(yùn)行來看,分配資源的時間大于 Java 虛擬機(jī)運(yùn)行指令的時間镊逝,所以主線程比子線程先拿到鎖壮啊。
我們還可以知道一點,控制臺打印出的時間是 3005 ms 撑蒜,在代碼里我們只等待了 3s 多出來的 5ms (這個數(shù)字會浮動)我們可以推斷是歹啼,子線程獲取 CPU 的時間加上喚醒主線程的時間。
上述只是自己的一個猜測座菠,能力還有欠缺狸眼,準(zhǔn)備深入學(xué)習(xí)。
不過推薦大家看看這篇文章 Synchnornized 在 JVM 下的實現(xiàn) - 簡書浴滴。
Join() 的實踐
join()
的注釋上面寫著:
Waits for this thread to die.
意思是拓萌,阻塞當(dāng)前調(diào)用 join()
函數(shù)所在的線程,直到接收線程執(zhí)行完畢之后再繼續(xù)升略。
我們來看看實踐代碼:
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 {
// 調(diào)用 worker1 的 join 函數(shù)微王,主線,程會阻塞直到 woker1 執(zhí)行完成
worker1.join();
System.out.println("啟動線程 2");
// 再啟動線程 2 品嚣,并且調(diào)用線程 2 的 join 函數(shù)炕倘,主線程會阻塞直到 woker2 執(zhí)行完成
worker2.start();
worker2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主線程繼續(xù)執(zhí)行");
}
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());
}
}
}
運(yùn)行之后我們得到:
啟動線程 1
work in work-1
啟動線程 2
work in work-2
主線程繼續(xù)執(zhí)行
在 joinDemo()
方法里我們創(chuàng)建兩個子線程,然后啟動了 work1 線程翰撑,下一步調(diào)用了 woker1 的 join()
函數(shù)罩旋。此時,主線程會進(jìn)入阻塞狀態(tài)额嘿,直到 work1 執(zhí)行完畢之后才開始繼續(xù)執(zhí)行瘸恼。因為 Worker 的 run()
方法里會休眠 2 秒,因此線程每次調(diào)用了 join()
方法實際上都會阻塞 2 秒册养,直到 run()
方法執(zhí)行完畢再繼續(xù)东帅。
所以,上述代碼邏輯其實就是:
啟動線程1 —-> 等待線程 1 執(zhí)行完畢 —-> 啟動線程2 —-> 等待線程 2 執(zhí)行完畢 —-> 繼續(xù)執(zhí)行主線程代碼
Yield() 的實踐
yield()
是 Thread 的靜態(tài)方法球拦,注釋上說:
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.
大致意思是說:當(dāng)前線程讓出執(zhí)行時間給其他的線程靠闭。
我們都知道帐我,線程的執(zhí)行是有時間片的,每個線程輪流占用 CPU 固定時間愧膀,執(zhí)行周期到了之后讓出執(zhí)行權(quán)給其他線程拦键。
yield()
就是主動讓出執(zhí)行權(quán)給其他線程。
來看看我們實踐的代碼:
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 優(yōu)先級為 [%d] -------> %d\n", this.getName(), this.getPriority(), i);
// 當(dāng) i 為 2 時檩淋,調(diào)用當(dāng)前線程的 yield 函數(shù)
if (i == 2) {
Thread.yield();
}
}
}
}
}
在 main()
方法里創(chuàng)建了兩個 YieldTread 線程芬为,控制臺輸出結(jié)果如下:
thread-1 優(yōu)先級為 [5] -------> 0
thread-1 優(yōu)先級為 [5] -------> 1
thread-1 優(yōu)先級為 [5] -------> 2thread-2 優(yōu)先級為 [5] -------> 0
thread-2 優(yōu)先級為 [5] -------> 1
thread-2 優(yōu)先級為 [5] -------> 2thread-1 優(yōu)先級為 [5] -------> 3
thread-1 優(yōu)先級為 [5] -------> 4
thread-2 優(yōu)先級為 [5] -------> 3
thread-2 優(yōu)先級為 [5] -------> 4
通常情況下 t1 首先執(zhí)行,讓 t1 的 run()
函數(shù)執(zhí)行到了 i 等于 2 時讓出當(dāng)前線程的執(zhí)行時間蟀悦。所以我們看到前三行都是 t1 在執(zhí)行媚朦,讓出執(zhí)行時間后 t2 開始執(zhí)行。后面邏輯簡單思考下就得知了日戈,這里也不做過多詮釋奉狈。
因此括饶,調(diào)用 yield()
就是讓出當(dāng)前線程的執(zhí)行權(quán),這樣一來讓其他線程得到優(yōu)先執(zhí)行醉箕。
總結(jié)與參考
本章內(nèi)容屬于線程的基礎(chǔ)押框,本系列會更新到線程池相關(guān)冻记。
這章內(nèi)容也及其重要匀们,因為它是后面的基礎(chǔ)献汗。
正確理解才能讓我們對各種線程問題有方向和思路。
參考讀物: