版權(quán)申明】非商業(yè)目的可自由轉(zhuǎn)載
博文地址:http://www.reibang.com/p/25675e583943
出自:shusheng007
前言
這個(gè)話題一般比較大较店,如果往深了研究學(xué)問(wèn)可大了伦糯,不僅涉及到操作系統(tǒng)知識(shí)還會(huì)涉及計(jì)算機(jī)硬件的知識(shí)概疆,本文將著眼于應(yīng)用層面行文。有的同學(xué)要說(shuō)了:“講那么多干什么板惑,還不是因?yàn)樽约翰恕保抑荒苷f(shuō):“被你看穿了枫攀,呵呵”。
概述
追求工作效率是人類社會(huì)能夠迅速向前發(fā)展的動(dòng)力株茶,例如老王公司的軟件部門(mén)有大把資金大把項(xiàng)目来涨,但是只有一個(gè)碼農(nóng)小明,而小明計(jì)劃一個(gè)一個(gè)的把項(xiàng)目做完启盛。老王就急了蹦掐,我這分分鐘幾百萬(wàn)的生意,你這做到猴年馬月呢僵闯,于是就又雇了一批碼農(nóng)卧抗,將各個(gè)項(xiàng)目同時(shí)啟動(dòng)。那么我們可以把每一個(gè)碼農(nóng)看成一個(gè)線程(Thread
)鳖粟,這樣就形成了多任務(wù)并發(fā)執(zhí)行了(其實(shí)這個(gè)例子已經(jīng)是并行執(zhí)行了)社裆。
那么由人設(shè)計(jì)的計(jì)算機(jī)操作系統(tǒng)也不例外,它也會(huì)想盡一切辦法提高任務(wù)執(zhí)行效率的向图,于是乎多線程應(yīng)用而生浦马。
進(jìn)程與線程
面過(guò)試的都知道,至于標(biāo)準(zhǔn)答案大家可以網(wǎng)上搜索一下张漂。你只要知道進(jìn)程面向操作系統(tǒng)晶默,線程面向進(jìn)程。進(jìn)程是操作系統(tǒng)實(shí)現(xiàn)多任務(wù)的手段航攒,多個(gè)進(jìn)程會(huì)互相隔離,擁有自己獨(dú)立的地址空間與資源漠畜。而線程存在于進(jìn)程中,隔離不是很?chē)?yán)重憔狞,可以共享同一個(gè)進(jìn)程中的內(nèi)存數(shù)據(jù)。
多線程的作用
- 可以充分利用多CPU的硬件資源瘾敢,提高任務(wù)執(zhí)行效率。
- 可以執(zhí)行后臺(tái)任務(wù)簇抵,當(dāng)使用瀏覽器下載一部小電影的同時(shí),你可以去瀏覽下性感美女的圖片碟摆。
- 提高
GUI
程序的用戶體驗(yàn),你也不希望在手機(jī)上點(diǎn)擊了一個(gè)下載按鈕后典蜕,App就卡死在那里了罗洗。 - 等等...
Java中如何使用多線程
Java 對(duì)多線程的支持非常完善,Java使用Thread
類來(lái)表示線程伙菜,下面的使用均與此類相關(guān)。
繼承Thread類創(chuàng)建線程
繼承Thread
類厢洞,重寫(xiě)其run()
方法即可仇让。啟動(dòng)此線程時(shí)典奉,只需要new MyThread().start();
即可躺翻。
public class MyThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("線程名稱:"+getName());
}
}
使用Runnable創(chuàng)建線程
從源碼可知Thread
存在這樣一個(gè)構(gòu)造函數(shù) public Thread(Runnable target)
,因而我們可以使用實(shí)現(xiàn)Runnable
接口的方式創(chuàng)建線程卫玖。
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("線程名稱:"+Thread.currentThread().getName());
}
}).start();
由于Runnable
接口是一個(gè)函數(shù)接口公你,所以我們可以使用Lambda
表達(dá)式來(lái)實(shí)現(xiàn),如下所示:
new Thread(() -> System.out.println("線程名稱:"+Thread.currentThread().getName())).start();
通過(guò)這種方式多個(gè)線程可以共享線程執(zhí)行體假瞬,但是線程執(zhí)行結(jié)果無(wú)法獲得陕靠,run()
方法沒(méi)有返回值。
使用Callable和Future創(chuàng)建線程
通過(guò)這種方式創(chuàng)建的線程可以有返回值脱茉,此處使用Callable
作為線程的執(zhí)行體剪芥,其包含一個(gè)擁有返回值的方法call()
:
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
上面提到,Thread的構(gòu)造方法需要一個(gè)Runnable
類型的參數(shù)琴许,所以不可以直接使用Callable
來(lái)創(chuàng)建線程税肪。Java提供了一個(gè)叫Futurer
的接口來(lái)表示Callbale
接口中call()
方法的返回值。還為其提供了一個(gè)實(shí)現(xiàn)類FutureTask
榜田,此類實(shí)現(xiàn)了Future
與Runnable
接口益兄,這樣FutureTask
類就可以作為參數(shù)構(gòu)建線程了。
talk is cheap 箭券,show me the code.
private static void startThread()
{
//第一步:創(chuàng)建callable實(shí)現(xiàn)類
Callable<String> c=new Callable<String>() {
@Override
public String call() throws Exception {
//經(jīng)過(guò)大量耗時(shí)運(yùn)算得出結(jié)論
return "總有刁民想害朕";
}
};
//第二步:以c作為參數(shù)創(chuàng)建FutureTask實(shí)例ft
FutureTask<String>ft=new FutureTask<String>(c);
//第三步:以ft為參數(shù)啟動(dòng)線程
new Thread(ft).start();
//第四步:獲取執(zhí)行結(jié)果净捅,get()方法是一個(gè)阻塞方法。
try {
System.out.println("錦衣衛(wèi)調(diào)查謀反結(jié)論:"+ft.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
輸出結(jié)果:錦衣衛(wèi)調(diào)查謀反結(jié)論:總有刁民想害朕
如何使用在代碼注釋中已經(jīng)寫(xiě)的非常清楚了辩块,如果你仍然看不懂蛔六,說(shuō)明你目前水平太差,不適合看這篇文章废亭!
線程的同步
談到多線程古今,首先繞不過(guò)的話題就是線程同步。因?yàn)槎嗑€程會(huì)對(duì)共享資源狀態(tài)產(chǎn)生競(jìng)態(tài)條件(Race condition)滔以,競(jìng)態(tài)條件是指輸出依賴不可控事件發(fā)生的順序或者時(shí)間的行為捉腥,當(dāng)這些不可控事件沒(méi)有按照預(yù)期發(fā)生時(shí),就會(huì)產(chǎn)生bug你画。對(duì)應(yīng)到編程中就是指多個(gè)線程如果沒(méi)有按照預(yù)期的順序或者時(shí)間來(lái)操作共享狀態(tài)時(shí)就會(huì)產(chǎn)生bug抵碟。
假設(shè)我們現(xiàn)在使用兩個(gè)線程Thread1
和Thread2
來(lái)并發(fā)使一個(gè)整數(shù)自增桃漾,我們期望是兩個(gè)線程按照如下的順序執(zhí)行得到正確值2:
而實(shí)際情況是兩個(gè)沒(méi)有加鎖或者同步的線程來(lái)并發(fā)做這件事情的話,很有可能執(zhí)行順序如下圖所示:
很明顯撬统,第二種情況得到了錯(cuò)誤的結(jié)果1敦迄,這種情況之所以發(fā)生就是因?yàn)檎麛?shù)自增操作不是排他(Mutual exclusivity容)的罚屋,在發(fā)生競(jìng)態(tài)條件時(shí)出了錯(cuò)誤。解決上述問(wèn)題就需要線程的同步技術(shù)撕彤。
使用synchronized關(guān)鍵字
同步代碼塊
我們可以使用同步代碼塊將需要同步的資源操作保護(hù)起來(lái)羹铅,如下代碼所示职员。其中obj稱作同步監(jiān)視器跛溉,通常推薦使用可能被并發(fā)訪問(wèn)的共享資源充當(dāng)。
synchronized (obj)
{
...
}
同步方法
我們也可以使用同步方法將需要同步的操作置于此方法中蛛蒙,如下代碼所示牵祟。此實(shí)例方法的同步監(jiān)視器就是調(diào)用此方法的實(shí)例對(duì)象this诺苹。如果是靜態(tài)同步方法雹拄,那么同步監(jiān)視器就是類本身滓玖。
private synchronized void synMethod()
{
...
}
使用同步鎖(Lock)
Java5
提供了另一種同步代碼的方式,鎖(Lock).我們?cè)趯W(xué)習(xí)編程的過(guò)程中翩肌,只要發(fā)現(xiàn)一個(gè)問(wèn)題以前已經(jīng)有一套解決方案念祭,突然在新版本中又提供了另一套解決方案,那么我們立刻可以肯定:在實(shí)際開(kāi)發(fā)中第一套解決方案對(duì)于解決某些特殊場(chǎng)景下的問(wèn)題時(shí)遇到了困難隶糕,才引入第二套解決方案枚驻,第二套解決方案大部分情況下不是用來(lái)完全替換第一套解決方案的测秸,而是其補(bǔ)充和增強(qiáng)灾常。像Lock
就是synchronized
的補(bǔ)充和增強(qiáng)铃拇,在日常大部分的開(kāi)發(fā)場(chǎng)景下synchronized
已經(jīng)足夠了慷荔,Lock
在特殊場(chǎng)景下才會(huì)使用。
synchronized
其實(shí)獲取的是每個(gè)object都有的隱式監(jiān)視器鎖(implicit monitor lock )贷岸,其要求程序獲取和釋放鎖的操作都限定在一個(gè)塊結(jié)構(gòu)里偿警,就是說(shuō)其獲取鎖和釋放鎖這兩個(gè)操作不是很靈活螟蒸,當(dāng)遇到需要這兩個(gè)操作不在同一個(gè)塊結(jié)構(gòu)的場(chǎng)景就無(wú)法適應(yīng)了崩掘。這是引入Lock
的主要原因苞慢,當(dāng)然Lock
比synchronized
的功能更加豐富,例如使用tryLock()
方法嘗試獲取鎖,如果當(dāng)前鎖沒(méi)有釋放绍赛,則返回false
.例如lockInterruptibly()
方法嘗試獲取鎖惹资,但是如果當(dāng)前鎖沒(méi)有釋放,其轉(zhuǎn)入阻塞狀態(tài)猴誊,剛好此時(shí)別的線程中斷了此線程懈叹,則會(huì)拋出異常分扎,不再嘗試獲取鎖畏吓。
下面是官方舉出的一個(gè)需要使用lock
的場(chǎng)景:
For example, some algorithms for traversing concurrently accessed data structures require the use of * "hand-over-hand" or "chain locking": you acquire the lock of node A, then node B, then release A and acquire
C, then release B and acquire D and so on. Implementations of the {@code Lock} interface enable the use of such techniques by allowing a lock to be acquired and released in different scopes, and allowing multiple locks to be acquired and released in any order.
鎖有很多實(shí)現(xiàn)類菲饼,我們這里主要關(guān)注一個(gè)ReentrantLock
的實(shí)現(xiàn)類,使用代碼如下
private final ReentrantLock lock=new ReentrantLock();
private void m()
{
lock.lock();
try {
...
}catch (Exception e)
{
e.printStackTrace();
}finally {
lock.unlock();
}
}
線程的生命周期
線程的生命周期共有5個(gè)狀態(tài):新建(New)镐确、就緒(Runnable)源葫、運(yùn)行(Running)息堂、阻塞(Blocked)和死亡(Dead)芭届,他們的關(guān)系可以看下面的一張圖。
[圖片上傳失敗...(image-ccc912-1530440208607)]
具體解釋如下:
- 新建狀態(tài)(New): 線程對(duì)象被創(chuàng)建后持隧,就進(jìn)入了新建狀態(tài)逃片。
-
就緒狀態(tài)(Runnable): 也被稱為“可執(zhí)行狀態(tài)”。線程對(duì)象被創(chuàng)建后調(diào)用
start()
方法啟動(dòng)線程后其處于就緒狀態(tài)呀狼,隨時(shí)可能被CPU調(diào)度執(zhí)行。 - 運(yùn)行狀態(tài)(Running) : 線程獲取CPU權(quán)限進(jìn)行執(zhí)行绝编。需要注意的是十饥,線程只能從就緒狀態(tài)進(jìn)入到運(yùn)行狀態(tài)。
-
阻塞狀態(tài)(Blocked) : 阻塞狀態(tài)是線程因?yàn)槟撤N原因放棄CPU使用權(quán)逗堵,暫時(shí)停止運(yùn)行蜒秤,直到線程進(jìn)入就緒狀態(tài)亚斋,才有機(jī)會(huì)轉(zhuǎn)到運(yùn)行狀態(tài)。阻塞的情況分三種:
(1) 等待阻塞 -- 通過(guò)調(diào)用線程的wait()方法伞访,讓線程等待某工作的完成轰驳。
(2) 同步阻塞 -- 線程在獲取synchronized同步鎖失敗(因?yàn)殒i被其它線程所占用)级解,它會(huì)進(jìn)入同步阻塞狀態(tài)。
(3) 其他阻塞 -- 通過(guò)調(diào)用線程的sleep()或join()或發(fā)出了I/O請(qǐng)求時(shí)抡爹,線程會(huì)進(jìn)入到阻塞狀態(tài)芒划。當(dāng)sleep()狀態(tài)超時(shí)民逼、join()等 待線程終止或者超時(shí)、或者I/O處理完畢時(shí)笑诅,線程重新轉(zhuǎn)入就緒狀態(tài)。 -
死亡狀態(tài)(Dead) : 線程執(zhí)行完了或者因異常退出了run()方法弦叶,該線程結(jié)束生命周期伤哺。
具體可以參考此博文
線程的控制
既然程序中存在多個(gè)線程者祖,那么我們就需要對(duì)多個(gè)線程執(zhí)行一些控制。
線程等待(Join)
Thread中提供了一個(gè)join()
方法桃序,例如有兩個(gè)線程A和B媒熊,在A的執(zhí)行過(guò)程中調(diào)用了B的join()方法芦鳍,那么A線程就會(huì)被阻塞柠衅,直到B線程執(zhí)行完畢菲宴。
線程睡眠(sleep)
這個(gè)大家一定不陌生喝峦,Thread.sleep(3*1000)
使線程從運(yùn)行狀態(tài)進(jìn)入阻塞狀態(tài)3秒,此期間線程就不會(huì)被CPU
調(diào)度執(zhí)行眉踱。
線程讓步(yield)
當(dāng)我們想讓線程調(diào)度器立刻做一次新的線程調(diào)度時(shí)霜威,可以調(diào)用當(dāng)前執(zhí)行線程的yield()
方法叁执,此方法會(huì)使調(diào)用線程立刻進(jìn)入就緒狀態(tài)谈宛,線程調(diào)度器開(kāi)始一次新的線程調(diào)度吆录。此時(shí)線程優(yōu)先級(jí)就起作用了恢筝,線程調(diào)度器可定是先調(diào)度優(yōu)先級(jí)高的線程執(zhí)行撬槽。
例如有A和B兩個(gè)線程侄柔,A的線程優(yōu)先級(jí)小于等于B線程暂题,那么當(dāng)調(diào)用A.yield()
后薪者,B線程就會(huì)被調(diào)度執(zhí)行。如果A線程的優(yōu)先級(jí)大于B線程攻人,那么即使調(diào)用A.yield()
后贝椿,B線程也得不到執(zhí)行,線程調(diào)度器仍然會(huì)再次調(diào)度線程A來(lái)執(zhí)行瑟蜈。
后臺(tái)線程
Java中有一類線程叫后臺(tái)線程(Daemon Thread),也叫守護(hù)線程宪躯,使用setDaemon(boolean b)
設(shè)置一個(gè)線程是否為后臺(tái)線程访雪。這類線程有一個(gè)特點(diǎn),就是當(dāng)所有前臺(tái)線程都死亡后坝橡,后臺(tái)線程自動(dòng)死亡精置。
線程的通信
未完待續(xù)
線程池
參考Java8 并發(fā)教程之Thread與Executors
總結(jié)
未完待續(xù)