本文主要介紹線程的定義恶导,創(chuàng)建浆竭,使用,停止惨寿,狀態(tài)圖和常用方法邦泄。
主要用于概念掃盲和梳理。
多進(jìn)程是指操作系統(tǒng)能同時(shí)運(yùn)行多個(gè)任務(wù)(程序)裂垦。
多線程是指在同一程序中有多個(gè)順序流在執(zhí)行顺囊。
先說并發(fā)
1. 為什么要用到并發(fā)
一直以來,硬件的發(fā)展極其迅速蕉拢,也有一個(gè)很著名的"摩爾定律"特碳,可能會奇怪明明討論的是并發(fā)編程為什么會扯到了硬件的發(fā)展诚亚,這其中的關(guān)系應(yīng)該是多核CPU的發(fā)展為并發(fā)編程提供的硬件基礎(chǔ)。摩爾定律并不是一種自然法則或者是物理定律午乓,它只是基于認(rèn)為觀測數(shù)據(jù)后站宗,對未來的一種預(yù)測。
按照所預(yù)測的速度益愈,我們的計(jì)算能力會按照指數(shù)級別的速度增長梢灭,不久以后會擁有超強(qiáng)的計(jì)算能力,正是在暢想未來的時(shí)候蒸其,2004年敏释,Intel宣布4GHz芯片的計(jì)劃推遲到2005年,然后在2004年秋季摸袁,Intel宣布徹底取消4GHz的計(jì)劃钥顽,也就是說摩爾定律的有效性超過了半個(gè)世紀(jì)戛然而止。
但是靠汁,聰明的硬件工程師并沒有停止研發(fā)的腳步蜂大,他們?yōu)榱诉M(jìn)一步提升計(jì)算速度,而不是再追求單獨(dú)的計(jì)算單元膀曾,而是將多個(gè)計(jì)算單元整合到了一起,也就是形成了多核CPU阳啥。短短十幾年的時(shí)間添谊,家用型CPU,比如Intel i7就可以達(dá)到4核心甚至8核心。而專業(yè)服務(wù)器則通巢斐伲可以達(dá)到幾個(gè)獨(dú)立的CPU斩狱,每一個(gè)CPU甚至擁有多達(dá)8個(gè)以上的內(nèi)核。因此扎瓶,摩爾定律似乎在CPU核心擴(kuò)展上繼續(xù)得到體驗(yàn)所踊。因此,多核的CPU的背景下概荷,催生了并發(fā)編程的趨勢秕岛,通過并發(fā)編程的形式可以將多核CPU的計(jì)算能力發(fā)揮到極致,性能得到提升误证。
頂級計(jì)算機(jī)科學(xué)家Donald Ervin Knuth如此評價(jià)這種情況:在我看來继薛,這種現(xiàn)象(并發(fā))或多或少是由于硬件設(shè)計(jì)者無計(jì)可施了導(dǎo)致的,他們將摩爾定律的責(zé)任推給了軟件開發(fā)者愈捅。
另外遏考,在特殊的業(yè)務(wù)場景下先天的就適合于并發(fā)編程。比如在圖像處理領(lǐng)域蓝谨,一張1024X768像素的圖片灌具,包含達(dá)到78萬6千多個(gè)像素青团。即時(shí)將所有的像素遍歷一邊都需要很長的時(shí)間,面對如此復(fù)雜的計(jì)算量就需要充分利用多核的計(jì)算的能力咖楣。又比如當(dāng)我們在網(wǎng)上購物時(shí)督笆,為了提升響應(yīng)速度,需要拆分截歉,減庫存胖腾,生成訂單等等這些操作,就可以進(jìn)行拆分利用多線程的技術(shù)完成瘪松。面對復(fù)雜業(yè)務(wù)模型咸作,并行程序會比串行程序更適應(yīng)業(yè)務(wù)需求,而并發(fā)編程更能吻合這種業(yè)務(wù)拆分 宵睦。
2. 并發(fā)編程缺點(diǎn)
- 頻繁的上下文切換
時(shí)間片是CPU分配給各個(gè)線程的時(shí)間记罚,因?yàn)闀r(shí)間非常短,所以CPU不斷通過切換線程壳嚎,讓我們覺得多個(gè)線程是同時(shí)執(zhí)行的桐智,時(shí)間片
一般是幾十毫秒。而每次切換時(shí)烟馅,需要保存當(dāng)前的狀態(tài)起來说庭,以便能夠進(jìn)行恢復(fù)先前狀態(tài),而這個(gè)切換時(shí)非常損耗性能郑趁,過于頻繁反而無法發(fā)揮出多線程編程的優(yōu)勢刊驴。
通常減少上下文切換可以采用無鎖并發(fā)編程,CAS算法寡润,使用最少的線程和使用協(xié)程捆憎。
- 無鎖并發(fā)編程:可以參照concurrentHashMap鎖分段的思想,不同的線程處理不同段的數(shù)據(jù)梭纹,這樣在多線程競爭的條件下躲惰,可以減少上下文切換的時(shí)間。
- CAS算法变抽,利用Atomic下使用CAS算法來更新數(shù)據(jù)础拨,使用了樂觀鎖,可以有效的減少一部分不必要的鎖競爭帶來的上下文切換
- 使用最少線程:避免創(chuàng)建不需要的線程绍载,比如任務(wù)很少太伊,但是創(chuàng)建了很多的線程,這樣會造成大量的線程都處于等待狀態(tài)
- 協(xié)程:在單線程里實(shí)現(xiàn)多任務(wù)的調(diào)度逛钻,并在單線程里維持多個(gè)任務(wù)間的切換
- 線程安全
3. 一些概念
3.1 同步與異步
同步和異步通常用來形容一次方法調(diào)用
僚焦。同步方法調(diào)用一開始,調(diào)用者必須等待被調(diào)用的方法結(jié)束后曙痘,調(diào)用者后面的代碼才能執(zhí)行芳悲。而異步調(diào)用立肘,指的是,調(diào)用者不用管被調(diào)用方法是否完成名扛,都會繼續(xù)執(zhí)行后面的代碼谅年,當(dāng)被調(diào)用的方法完成后會通知調(diào)用者。
比如肮韧,在超時(shí)購物融蹂,如果一件物品沒了,你得等倉庫人員跟你調(diào)貨弄企,直到倉庫人員跟你把貨物送過來超燃,你才能繼續(xù)去收銀臺付款,這就類似同步調(diào)用拘领。而異步調(diào)用了意乓,就像網(wǎng)購,你在網(wǎng)上付款下單后约素,什么事就不用管了届良,該干嘛就干嘛去了,當(dāng)貨物到達(dá)后你收到通知去取就好圣猎。
3.2 并發(fā)與并行
并發(fā)和并行是十分容易混淆的概念士葫。并發(fā)指的是多個(gè)任務(wù)交替進(jìn)行,而并行則是指真正意義上的“同時(shí)進(jìn)行”送悔。實(shí)際上慢显,如果系統(tǒng)內(nèi)只有一個(gè)CPU,而使用多線程時(shí)放祟,那么真實(shí)系統(tǒng)環(huán)境下不能并行鳍怨,只能通過切換時(shí)間片的方式交替進(jìn)行呻右,而成為并發(fā)執(zhí)行任務(wù)跪妥。
真正的并行也只能出現(xiàn)在擁有多個(gè)CPU的系統(tǒng)中。
3.3 阻塞和非阻塞
阻塞和非阻塞通常用來形容多線程間的相互影響
声滥,比如一個(gè)線程占有了臨界區(qū)資源眉撵,那么其他線程需要這個(gè)資源就必須進(jìn)行等待該資源的釋放,會導(dǎo)致等待的線程掛起落塑,這種情況就是阻塞纽疟,而非阻塞就恰好相反,它強(qiáng)調(diào)沒有一個(gè)線程可以阻塞其他線程憾赁,所有的線程都會嘗試地往前運(yùn)行污朽。
3.4 臨界區(qū)
臨界區(qū)用來表示一種公共資源或者說是共享數(shù)據(jù),可以被多個(gè)線程使用龙考。但是每個(gè)線程使用時(shí)蟆肆,一旦臨界區(qū)資源被一個(gè)線程占有矾睦,那么其他線程必須等待。
進(jìn)程與線程
進(jìn)程--資源分配的最小單位
- 每個(gè)進(jìn)程都有獨(dú)立的代碼和數(shù)據(jù)空間(進(jìn)程上下文)
- 一個(gè)進(jìn)程包含多個(gè)線程
- 進(jìn)程間的切換會有較大的開銷
線程--cpu調(diào)度的最小單位
- 同一類線程共享代碼和數(shù)據(jù)空間炎功,
- 每個(gè)線程有獨(dú)立的運(yùn)行棧和程序計(jì)數(shù)器(PC)
- 線程切換開銷小
其他
- 多進(jìn)程是指操作系統(tǒng)能同時(shí)運(yùn)行多個(gè)任務(wù)(程序)枚冗。
- 多線程是指在同一程序中有多個(gè)順序流在執(zhí)行。
- 線程和進(jìn)程一樣分為五個(gè)階段:創(chuàng)建蛇损、就緒赁温、運(yùn)行、阻塞淤齐、終止股囊。
線程創(chuàng)建、使用床玻、停止
創(chuàng)建線程
- 在Java中要想實(shí)現(xiàn)多線程毁涉,有三種手段,一種是繼承Thread類锈死,另外一種是實(shí)現(xiàn)Runnable接口贫堰,三是實(shí)現(xiàn)Callable 接口。
實(shí)現(xiàn) Runnable 和 Callable 接口的類只能當(dāng)做一個(gè)可以在線程中運(yùn)行的任務(wù)待牵,不是真正意義上的線程其屏,因此最后還需要通過 Thread 來調(diào)用∮Ц茫可以說任務(wù)是通過線程驅(qū)動從而執(zhí)行的偎行。
1. 繼承Thread 類,覆蓋run方法(推薦使用Runable)
- run()方法是多線程程序的一個(gè)約定贰拿,所有的多線程代碼都在run方法里面蛤袒。
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
- 我們復(fù)寫的是run()方法,但是執(zhí)行的卻是start()方法膨更。
- start()方法的調(diào)用后并不是立即執(zhí)行多線程代碼妙真,而是使得該線程變?yōu)榭蛇\(yùn)行態(tài)(Runnable),什么時(shí)候運(yùn)行是由操作系統(tǒng)決定的荚守。
PrimeThread p = new PrimeThread(143);
p.start();
2. 實(shí)現(xiàn)Runnable接口珍德,覆蓋run方法
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
在啟動的多線程的時(shí)候,需要先通過Thread類的構(gòu)造方法Thread(Runnable target)
構(gòu)造出對象矗漾,然后調(diào)用Thread對象的start()方法來運(yùn)行多線程代碼锈候。
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
或
new Thread(new PrimeRun(143)).start();
實(shí)際上所有的多線程代碼都是通過運(yùn)行Thread的start()方法來運(yùn)行的。因此敞贡,不管是擴(kuò)展Thread類還是實(shí)現(xiàn)Runnable接口來實(shí)現(xiàn)多線程泵琳,最終還是通過Thread的對象的API來控制線程的,熟悉Thread類的API是進(jìn)行多線程編程的基礎(chǔ)。
3. 實(shí)現(xiàn) Callable 接口
與 Runnable 相比获列,Callable 可以有返回值琳钉,返回值通過FutureTask
進(jìn)行封裝。
public class MyCallable implements Callable<Integer> {
public Integer call() {
// ...
}
public static void main(String[] args) {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
}
Thread和Runnable的區(qū)別
實(shí)現(xiàn)Runnable接口比繼承Thread類所具有的優(yōu)勢:
- 適合多個(gè)相同的程序代碼的線程去處理同一個(gè)資源
- 可以避免java中的
單繼承的限制
- 增加程序的健壯性蛛倦,代碼可以被多個(gè)線程共享歌懒,代碼和數(shù)據(jù)獨(dú)立
-
線程池
只能放入實(shí)現(xiàn)Runable或callable類線程,不能直接放入繼承Thread的類
使用線程
方法
類別 | 方法簽名 | 簡介 |
---|---|---|
構(gòu)造方法 | thread() | - |
- | thread(String name) | - |
- | thread(Runnable target) | - |
- | thread(Runnable target,String name) | - |
常用方法 | void start() | 啟動線程 |
- | static void sleep(long millis) | 休眠 |
- | static void sleep(long millis,int nanos) | 休眠 |
- | void join() | 使其他線程等待當(dāng)前線程終止 |
- | void join(long millis) | 使其他線程等待當(dāng)前線程終止 |
- | void join(long millis,int nanos) | 使其他線程等待當(dāng)前線程終止 |
- | static void yield( ) | 當(dāng)前運(yùn)行線程釋放處理器資源 |
獲取線程引用 | static Thread currendThread() | 返回當(dāng)前運(yùn)行的線程引用 |
注意事項(xiàng)
- main方法其實(shí)也是一個(gè)線程溯壶。在java中所以的線程都是同時(shí)啟動的及皂,至于什么時(shí)候,哪個(gè)先執(zhí)行且改,完全看誰先得到CPU的資源验烧。
- 在java中,每次程序運(yùn)行至少啟動2個(gè)線程又跛。一個(gè)是main線程碍拆,一個(gè)是垃圾收集線程。
停止線程
錯(cuò)誤方法
- stop()方法慨蓝。
它會使線程戛然而止感混,我們不知道它完成了什么工作,沒有完成什么工作礼烈,因此是錯(cuò)誤的弧满。 - interrupt()方法。
不要使用interrupt()方法此熬,因?yàn)?strong>在線程run()中調(diào)用sleep(),join(),yeild()方法時(shí)庭呜,中斷狀態(tài)會被清除,isinterrupe=false
如何正確的停止線程
使用退出標(biāo)志犀忱。
volatile boolean keepRunning = true;
代碼示例
public class ActorThread extends Thread {//繼承Thread創(chuàng)建線程
// 覆蓋run------------------------------------------------------
public void run() {
System.out.println(getName() + "是一個(gè)演員募谎!");
int count = 0;
boolean keepRunning = true;
while (keepRunning) {
System.out.println(getName() + "登臺演出:" + (++count));
//標(biāo)注法停止線程-----------------------------------------------
if (count == 15) {
keepRunning = false;
}
//測試sleep()-----------------------
if (count % 5 == 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(getName() + "的演出結(jié)束了!");
}
public static void main(String[] args) {
Thread actor = new ActorThread();
actor.setName("Mr. Thread");
actor.start();
Thread actressThread = new Thread(new Actress(), "Ms. Runnable");
actressThread.start();
}
}
class Actress implements Runnable {//實(shí)現(xiàn)Runnable接口創(chuàng)建線程
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "是一個(gè)演員阴汇!");
int count = 0;
boolean keepRunning = true;
while (keepRunning) {
System.out.println(Thread.currentThread().getName() + "登臺演出:" + (++count));
if (count == 15) {
keepRunning = false;
}
if (count % 5 == 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(Thread.currentThread().getName() + "的演出結(jié)束了数冬!");
}
}
輸出
線程狀態(tài)轉(zhuǎn)換
- 新建狀態(tài)(New):新創(chuàng)建了一個(gè)線程對象。
- 就緒狀態(tài)(Runnable):線程對象創(chuàng)建后鲫寄,其他線程調(diào)用了該對象的start()方法吉执。該狀態(tài)的線程位于可運(yùn)行線程池中疯淫,變得可運(yùn)行地来,等待獲取CPU的使用權(quán)。
- 運(yùn)行狀態(tài)(Running):就緒狀態(tài)的線程獲取了CPU熙掺,執(zhí)行程序代碼未斑。
- 阻塞狀態(tài)(Blocked):阻塞狀態(tài)是線程因?yàn)槟撤N原因放棄CPU使用權(quán),暫時(shí)停止運(yùn)行币绩。直到線程進(jìn)入就緒狀態(tài)蜡秽,才有機(jī)會轉(zhuǎn)到運(yùn)行狀態(tài)府阀。阻塞的情況分三種:
- 等待阻塞:運(yùn)行的線程執(zhí)行wait()方法,JVM會把該線程放入等待池中芽突。(wait會釋放持有的鎖)
- 同步阻塞:運(yùn)行的線程在獲取對象的同步鎖時(shí)试浙,若該同步鎖被別的線程占用,則JVM會把該線程放入鎖池中寞蚌。
- 其他阻塞:運(yùn)行的線程執(zhí)行sleep()或join()方法田巴,或者發(fā)出了I/O請求時(shí),JVM會把該線程置為阻塞狀態(tài)挟秤。當(dāng)sleep()狀態(tài)超時(shí)壹哺、join()等待線程終止或者超時(shí)、或者I/O處理完畢時(shí)艘刚,線程重新轉(zhuǎn)入就緒狀態(tài)管宵。(sleep是不會釋放持有的鎖)
- 死亡狀態(tài)(Dead):線程執(zhí)行完了或者因異常退出了run()方法,該線程結(jié)束生命周期攀甚。
常用函數(shù)說明
sleep(long millis):線程暫停執(zhí)行
在指定的毫秒數(shù)內(nèi)讓當(dāng)前正在執(zhí)行的線程休眠箩朴。
sleep()
可能會拋出 InterruptedException。因?yàn)楫惓2荒芸缇€程傳播回 main() 中秋度,因此必須在本地進(jìn)行處理隧饼。線程中拋出的其它異常也同樣需要在本地進(jìn)行處理。
join(long millis):指等待t線程一段時(shí)間(或終止)
- join是Thread類的一個(gè)方法静陈,啟動線程后直接調(diào)用燕雁,即join()的作用是:“等待該線程(終止)”。
- 目的
在很多情況下鲸拥,主線程生成并起動了子線程拐格,如果子線程里要進(jìn)行大量的耗時(shí)的運(yùn)算,主線程往往將于子線程之前結(jié)束刑赶,但是如果主線程處理完其他的事務(wù)后捏浊,需要用到子線程的處理結(jié)果,也就是主線程需要等待子線程執(zhí)行完成之后再結(jié)束撞叨,這個(gè)時(shí)候就要用到j(luò)oin()方法了金踪。
yield():暫停當(dāng)前正在執(zhí)行的線程對象,并執(zhí)行其他線程牵敷。
- yield()應(yīng)該做的是讓當(dāng)前運(yùn)行線程回到可運(yùn)行狀態(tài)胡岔,以允許具有相同優(yōu)先級的其他線程獲得運(yùn)行機(jī)會。
- 目的
使用yield()的目的是讓相同優(yōu)先級的線程之間能適當(dāng)?shù)妮嗈D(zhuǎn)執(zhí)行枷餐。但是靶瘸,實(shí)際中無法保證yield()達(dá)到讓步目的,因?yàn)樽尣降木€程還有可能被線程調(diào)度程序再次選中。
wait()
本小節(jié)轉(zhuǎn)載自使用wait/notify/notifyAll實(shí)現(xiàn)線程間通信的幾點(diǎn)重要說明
用于線程間通信
在Java中怨咪,可以通過配合調(diào)用Object對象的wait()
方法和notify()
方法或notifyAll()
方法來實(shí)現(xiàn)線程間的通信屋剑。
語義
在線程中調(diào)用wait()
方法,將阻塞等待其他線程的通知(其他線程調(diào)用notify()
方法或notifyAll()
方法)诗眨,在線程中調(diào)用notify()
方法或notifyAll()
方法唉匾,將通知其他線程從wait()
方法處返回。
Object是所有類的超類匠楚,它有5個(gè)方法組成了等待/通知機(jī)制的核心:notify()肄鸽、notifyAll()、wait()油啤、wait(long)和wait(long典徘,int)。在Java中益咬,所有的類都從Object繼承而來逮诲,因此,所有的類都擁有這些共有方法可供使用幽告。而且梅鹦,由于他們都被聲明為final,因此在子類中不能覆寫任何一個(gè)方法冗锁。
使用中需要注意
-
wait()
--- "我去等待了"
public final void wait() throws InterruptedException,IllegalMonitorStateException
- 該方法用來將當(dāng)前線程置入休眠狀態(tài)齐唆,直到接到通知或被中斷為止。在調(diào)用wait()之前冻河,線程必須要獲得該對象的對象級別鎖箍邮,即只能在同步方法或同步塊中調(diào)用wait()方法。
- 進(jìn)入wait()方法后叨叙,當(dāng)前線程釋放鎖锭弊。在從wait()返回前,線程與其他線程競爭重新獲得鎖擂错。如果調(diào)用wait()時(shí)味滞,沒有持有適當(dāng)?shù)逆i,則拋出
IllegalMonitorStateException
钮呀,它是RuntimeException的一個(gè)子類剑鞍,因此,不需要try-catch結(jié)構(gòu)爽醋。
-
notify()
---"大家來爭我"
public final native void notify() throws IllegalMonitorStateException
- 該方法也要在同步方法或同步塊中調(diào)用蚁署,即在調(diào)用前,線程也必須要獲得該對象的對象級別鎖子房,的如果調(diào)用notify()時(shí)沒有持有適當(dāng)?shù)逆i形用,也會拋出
IllegalMonitorStateException
- 該方法用來通知那些可能等待該對象的對象鎖的其他線程。如果有多個(gè)線程等待证杭,則線程規(guī)劃器任意挑選出其中一個(gè)wait()狀態(tài)的線程來發(fā)出通知田度,并使它等待獲取該對象的對象鎖(notify后,當(dāng)前線程不會馬上釋放該對象鎖解愤,wait所在的線程并不能馬上獲取該對象鎖镇饺,要等到程序退出synchronized代碼塊后,當(dāng)前線程才會釋放鎖送讲,wait所在的線程也才可以獲取該對象鎖)奸笤,但不驚動其他同樣在等待被該對象notify的線程們。當(dāng)?shù)谝粋€(gè)獲得了該對象鎖的wait線程運(yùn)行完畢以后哼鬓,它會釋放掉該對象鎖监右,此時(shí)如果該對象沒有再次使用notify語句,則即便該對象已經(jīng)空閑异希,其他wait狀態(tài)等待的線程由于沒有得到該對象的通知健盒,會繼續(xù)阻塞在wait狀態(tài),直到這個(gè)對象發(fā)出一個(gè)notify或notifyAll称簿。這里需要注意:它們等待的是被notify或notifyAll扣癣,而不是鎖。這與下面的notifyAll()方法執(zhí)行后的情況不同憨降。
notifyAll()
public final native void notifyAll() throws IllegalMonitorStateException
該方法與notify()方法的工作方式相同父虑,重要的一點(diǎn)差異是:
notifyAll使所有原來在該對象上wait的線程統(tǒng)統(tǒng)退出wait的狀態(tài)(即全部被喚醒,不再等待notify或notifyAll授药,但由于此時(shí)還沒有獲取到該對象鎖士嚎,因此還不能繼續(xù)往下執(zhí)行),變成等待獲取該對象上的鎖悔叽,一旦該對象鎖被釋放(notifyAll線程退出調(diào)用了notifyAll的synchronized代碼塊的時(shí)候)航邢,他們就會去競爭。如果其中一個(gè)線程獲得了該對象鎖骄蝇,它就會繼續(xù)往下執(zhí)行膳殷,在它退出synchronized代碼塊,釋放鎖后九火,其他的已經(jīng)被喚醒的線程將會繼續(xù)競爭獲取該鎖赚窃,一直進(jìn)行下去,直到所有被喚醒的線程都執(zhí)行完畢岔激。
- wait(long)和wait(long,int)
顯然勒极,這兩個(gè)方法是設(shè)置等待超時(shí)時(shí)間的,后者在超值時(shí)間上加上ns虑鼎,精度也難以達(dá)到辱匿,因此键痛,該方法很少使用。對于前者匾七,如果在等待線程接到通知或被中斷之前絮短,已經(jīng)超過了指定的毫秒數(shù),則它通過競爭重新獲得鎖昨忆,并從wait(long)返回丁频。另外,需要知道邑贴,如果設(shè)置了超時(shí)時(shí)間席里,當(dāng)wait()返回時(shí),我們不能確定它是因?yàn)榻拥搅送ㄖ€是因?yàn)槌瑫r(shí)而返回的拢驾,因?yàn)閣ait()方法不會返回任何相關(guān)的信息奖磁。但一般可以通過設(shè)置標(biāo)志位來判斷,在notify之前改變標(biāo)志位的值繁疤,在wait()方法后讀取該標(biāo)志位的值來判斷署穗,當(dāng)然為了保證notify不被遺漏,我們還需要另外一個(gè)標(biāo)志位來循環(huán)判斷是否調(diào)用wait()方法嵌洼。 - 深入理解:
如果線程調(diào)用了對象的wait()方法案疲,那么線程便會處于該對象的
等待池中,等待池中的線程不會去競爭該對象的鎖麻养。
當(dāng)有線程調(diào)用了對象的notifyAll()方法(喚醒所有wait線程)或notify()方法(只隨機(jī)喚醒一個(gè)wait線程)褐啡,被喚醒的的線程便會進(jìn)入該對象的鎖池中,鎖池中的線程會去競爭該對象鎖鳖昌。
優(yōu)先級高的線程競爭到對象鎖的概率大备畦,假若某線程沒有競爭到該對象鎖,它還會留在鎖池中许昨,唯有線程再次調(diào)用wait()方法懂盐,它才會重新回到等待池中。而競爭到對象鎖的線程則繼續(xù)往下執(zhí)行糕档,直到執(zhí)行完了synchronized代碼塊莉恼,它會釋放掉該對象鎖,這時(shí)鎖池中的線程會繼續(xù)競爭該對象鎖速那。
wait和sleep區(qū)別
共同點(diǎn):
- 他們都是在多線程的環(huán)境下俐银,都可以在程序的調(diào)用處阻塞指定的毫秒數(shù),并返回
- wait()和sleep()都可以通過interrupt()方法 打斷線程的暫停狀態(tài) 端仰,從而使線程立刻拋出InterruptedException捶惜。 (不建議使用該方法)
- 如果線程A希望立即結(jié)束線程B,則可以對線程B對應(yīng)的Thread實(shí)例調(diào)用interrupt方法荔烧。如果此刻線程B正在wait/sleep /join吱七,則線程B會立刻拋出InterruptedException汽久,在catch() {} 中直接return即可安全地結(jié)束線程。
- 需要注意的是踊餐,InterruptedException是線程自己從內(nèi)部拋出的景醇,并不是interrupt()方法拋出的。對某一線程調(diào)用 interrupt()時(shí)市袖,如果該線程正在執(zhí)行普通的代碼啡直,那么該線程根本就不會拋出InterruptedException烁涌。但是苍碟,一旦該線程進(jìn)入到 wait()/sleep()/join()后,就會立刻拋出InterruptedException 撮执。
不同點(diǎn):
- 所屬的類
Thread類的方法:sleep(),yield()等
Object的方法:wait()和notify()等 - 是否釋放鎖
每個(gè)對象都有一個(gè)鎖來控制同步訪問微峰。Synchronized關(guān)鍵字可以和對象的鎖交互,來實(shí)現(xiàn)線程的同步抒钱。
sleep方法沒有釋放鎖蜓肆,而wait方法釋放了鎖,使得其他線程可以使用同步控制塊或者方法谋币。 - 使用限制
wait,notify和notifyAll只能在同步控制方法或者同步控制塊里面使用,而sleep可以在任何地方使用 线定。
sleep()和yield()的區(qū)別
- sleep 方法使當(dāng)前運(yùn)行中的線程睡眼一段時(shí)間罗岖,進(jìn)入不可運(yùn)行狀態(tài),這段時(shí)間的長短是由程序設(shè)定的诅蝶,yield 方法使當(dāng)前線程讓出 CPU 占有權(quán)退个,但讓出的時(shí)間是不可設(shè)定的。實(shí)際上调炬,yield()方法對應(yīng)了如下操作:先檢測當(dāng)前是否有相同優(yōu)先級的線程處于同可運(yùn)行狀態(tài)语盈,如有,則把 CPU 的占有權(quán)交給此線程缰泡,否則刀荒,繼續(xù)運(yùn)行原來的線程。所以yield()方法稱為“退讓”棘钞,它把運(yùn)行機(jī)會讓給了同等優(yōu)先級的其他線程
- 另外照棋,sleep 方法允許較低優(yōu)先級的線程獲得運(yùn)行機(jī)會,但 yield() 方法執(zhí)行時(shí)武翎,當(dāng)前線程仍處在可運(yùn)行狀態(tài)烈炭,所以,不可能讓出較低優(yōu)先級的線程些時(shí)獲得 CPU 占有權(quán)宝恶。在一個(gè)運(yùn)行系統(tǒng)中符隙,如果較高優(yōu)先級的線程沒有調(diào)用 sleep 方法趴捅,又沒有受到 I\O 阻塞,那么霹疫,較低優(yōu)先級線程只能等待所有較高優(yōu)先級的線程運(yùn)行結(jié)束拱绑,才有機(jī)會運(yùn)行。
三線程打印ABC
要求
java實(shí)現(xiàn)三個(gè)線程A B C:A線程打印10次A丽蝎,B線程打印10次B,C線程打印10次C猎拨,要求線程同時(shí)運(yùn)行,交替打印10次ABC屠阻。
方法一.使用synchronized() + wait(),nitify()
- 題意
問題為三線程間的同步喚醒操作红省,主要的目的就是ThreadA->ThreadB->ThreadC→ThreadA……循環(huán)執(zhí)行三個(gè)線程。為了控制線程執(zhí)行的順序国觉,那么就必須要確定喚醒吧恃、等待的順序,所以每一個(gè)線程必須同時(shí)持有兩個(gè)對象鎖麻诀,才能繼續(xù)執(zhí)行痕寓。一個(gè)對象鎖是prev,就是前一個(gè)線程所持有的對象鎖蝇闭。還有一個(gè)就是自身對象鎖呻率。 - 思路
主要的思想就是,為了控制執(zhí)行的順序呻引,必須要先持有prev鎖礼仗,也就是前一個(gè)線程要釋放自身對象鎖,再去申請自身對象鎖苞七,兩者兼?zhèn)鋾r(shí)打印字母藐守,之后首先調(diào)用self.notifyAll()釋放自身對象鎖,喚醒下一個(gè)等待線程蹂风,再調(diào)用prev.wait()釋放prev對象鎖卢厂,終止當(dāng)前線程,等待循環(huán)結(jié)束后再次被喚醒惠啄。
程序運(yùn)行的主要過程就是
A線程最先運(yùn)行慎恒,持有C,A對象鎖,后釋放A,C鎖撵渡,喚醒B融柬;
線程B等待A鎖,再申請B鎖趋距,后打印B粒氧,再釋放B,A鎖,喚醒C节腐;
線程C等待B鎖外盯,再申請C鎖摘盆,后打印C,再釋放C,B鎖饱苟,喚醒A……
- 注意
為了避免JVM啟動ThreadA孩擂、ThreadB、ThreadC三個(gè)線程順序的不確定性箱熬。需要讓A,B,C三個(gè)線程以確定的順序啟動类垦,中間加一段sleep()
確保前一個(gè)線程已啟動。
代碼實(shí)現(xiàn)
public class ABC {
public static class ThreadPrinter implements Runnable {
private String name;
private Object prev;//前一個(gè)對象的鎖
private Object self;//自己的鎖
private ThreadPrinter(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
@Override
public void run() {
int count = 10;
while (count > 0) {// 多線程并發(fā)城须,不能用if蚤认,必須用循環(huán)測試等待條件,避免虛假喚醒
synchronized (prev) { // 先獲取 prev 鎖
synchronized (self) {// 再獲取 self 鎖
System.out.print(name);
count--;
self.notifyAll();// 先釋放 self酿傍,喚醒其他線程競爭self鎖
}
try {
prev.wait(); // 再釋放 prev烙懦,休眠等待喚醒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) throws Exception {
Object a = new Object();
Object b = new Object();
Object c = new Object();
ThreadPrinter pa = new ThreadPrinter("A", c, a);
ThreadPrinter pb = new ThreadPrinter("B", a, b);
ThreadPrinter pc = new ThreadPrinter("C", b, c);
new Thread(pa).start();
Thread.sleep(10);
new Thread(pb).start();
Thread.sleep(10);
new Thread(pc).start();
Thread.sleep(10);
}
}
輸出
方法二. 使用Lock
代碼實(shí)現(xiàn)
import java.util.concurrent.locks.*;
public class ABC_Lock {
private static Lock lock = new ReentrantLock();//通過Lock鎖來保證線程的訪問的互斥
private static int state = 0;//控制ABC的打印語句是否執(zhí)行驱入,獲取Lock之后的判斷
static class ThreadA extends Thread {
@Override
public void run() {
int count = 10;
while(count > 0) {
try {
lock.lock();
while (state % 3 == 0) {//多線程并發(fā)赤炒,不能用if,必須用循環(huán)測試等待條件亏较,避免虛假喚醒
System.out.print("A");
state++;
count--;
}
} finally {
lock.unlock();// lock()和unlock()操作結(jié)合try/catch使用
}
}
}
}
static class ThreadB extends Thread {
@Override
public void run() {
int count = 10;
while(count > 0) {
try {
lock.lock();
while (state % 3 == 1) {//多線程并發(fā)莺褒,不能用if,必須用循環(huán)測試等待條件雪情,避免虛假喚醒
System.out.print("B");
state++;
count--;
}
} finally {
lock.unlock();// lock()和unlock()操作結(jié)合try/catch使用
}
}
}
}
static class ThreadC extends Thread {
@Override
public void run() {
int count = 10;
while(count > 0){
try {
lock.lock();
while (state % 3 == 2) {//多線程并發(fā)遵岩,不能用if,必須用循環(huán)測試等待條件巡通,避免虛假喚醒
System.out.print("C");
state++;
count--;
}
} finally {
lock.unlock();// lock()和unlock()操作結(jié)合try/catch使用
}
}
}
}
public static void main(String[] args) {
new ThreadA().start();
new ThreadB().start();
new ThreadC().start();
}
}
輸出
參考文章
并發(fā)編程的優(yōu)缺點(diǎn)
java多線程入門學(xué)習(xí)(一)
Java多線程學(xué)習(xí)(吐血超詳細(xì)總結(jié))
oracle官方文檔