2020/9/1
多任務(wù)(multitasking)是操作系統(tǒng)的一項能力橄碾,看起來可以在同一時間運行多個程序年局。
實際上逻谦,并發(fā)執(zhí)行的進(jìn)程數(shù)目并不受CPU數(shù)量限制颖变,操作系統(tǒng)會為每個進(jìn)程分配CPU時間片生均,給人并行處理的感覺。
多線程(multithreaded)則是更低一層的概念腥刹,單個程序看起來在同一時間完成多個任務(wù)马胧,每個任務(wù)在一個線程(thread)中執(zhí)行。線程是控制線程的簡稱衔峰。
多線程與多進(jìn)程的區(qū)別在于佩脊,每個進(jìn)程都有自己的一整套變量蛙粘,而線程則共享數(shù)據(jù)。因此威彰,線程之間存在風(fēng)險出牧,但是線程之間的通信更有效,更容易歇盼。線程“更輕量級”舔痕,創(chuàng)建撤銷的開銷都比進(jìn)程小。
并發(fā)和并行的區(qū)別
1豹缀、并發(fā)(Concurrent):指兩個或多個事件在同一時間間隔內(nèi)發(fā)生赵讯,即交替做不同事的能力,多線程是并發(fā)的一種形式耿眉。例如垃圾回收時边翼,用戶線程與垃圾收集線程同時執(zhí)行(但不一定是并行的,可能會交替執(zhí)行)鸣剪,用戶程序在繼續(xù)運行组底,而垃圾收集程序運行于另一個CPU上。
2筐骇、并行(Parallel):指兩個或者多個事件在同一時刻發(fā)生债鸡,即同時做不同事的能力。例如垃圾回收時铛纬,多條垃圾收集線程并行工作厌均,但此時用戶線程仍然處于等待狀態(tài)。
什么是線程
一個簡單的創(chuàng)建一個線程的過程:
1告唆,將任務(wù)代碼放在一個類的run方法中棺弊,這個類需要實現(xiàn)一個函數(shù)式接口Runnable。
public interface Runnable{
????void run();
}
2,從這個Runnable構(gòu)造一個Thread對象:
var t = new Thread(r);
3擒悬,啟動線程:
t.start();
綜上模她,一個轉(zhuǎn)賬線程如下:
Runnable?r?=?()?->{
????try{
????????for?(int?i?=?0;?i?<?STEPS;?i++){
????????????double?amount?=?MAX_AMOUNT?*?Math.random();
????????????bank.transfer(0,?1,?amount);
????????????Thread.sleep((int)(DELAY?*?Math.random()));
????????}
????}
????catch(InterruptedException?e){};
};
Thread?t?=?new?Thread(r);
t.start();
也可以通過建立一個Thread的子類來定義線程:
class?MyThread?extends?Thread{
????public?void?run(){
????????task?code
????}
}
然后構(gòu)造一個實例(構(gòu)造這個實例的過程其實就是分配了一個線程),并調(diào)用start方法來運行懂牧,不要調(diào)用run方法侈净,run方法只會在本線程中執(zhí)行這個任務(wù)。
現(xiàn)在不再推薦這種方法僧凤,應(yīng)當(dāng)把并行運行的任務(wù)與運行機(jī)制解耦畜侦。
線程狀態(tài)
有以下六種狀態(tài):
New(新建)
Runnable(可運行)
Blocked(阻塞)
Waiting(等待)
Timed waiting(計時等待)
Terminated(終止)
新建線程
當(dāng)用new Thread(r)創(chuàng)建一個新線程時,這個線程還沒有運行躯保,處于新建狀態(tài)旋膳。
可運行線程
一旦調(diào)用start方法,線程就處于可運行(runnable)狀態(tài)吻氧∧缬牵可運行代表進(jìn)入了排隊咏连,并不表示正在運行盯孙,可能在運行也可能沒有在運行鲁森。
操作系統(tǒng)可能提供搶占式調(diào)度系統(tǒng),它會給每一個可運行線程一個時間片來執(zhí)行任務(wù)振惰。當(dāng)時間片用完歌溉,操作系統(tǒng)就會剝奪該線程的運行權(quán),并給另一個線程一個機(jī)會來運行骑晶。
像手機(jī)這樣的小型設(shè)備痛垛,可能會使用協(xié)作式調(diào)度:一個線程只有再調(diào)用yield方法或者被阻塞或等待時才失去控制權(quán)。
阻塞和等待線程
阻塞和等待線程會退出排隊桶蛔,需要線程調(diào)度器重新激活才會進(jìn)入到排隊匙头。兩個區(qū)別在于怎樣退出排隊
阻塞線程:當(dāng)一個線程視圖獲取一個內(nèi)部的對象的鎖,但是鎖被其他線程占有仔雷,這個線程就會被阻塞蹂析。當(dāng)所有的線程都釋放了鎖,并且線程調(diào)度器允許這個線程持有這個鎖碟婆,它將變成可運行狀態(tài)电抚。
等待線程:當(dāng)線程等待另一個線程通知調(diào)度器出現(xiàn)一個條件時,這個線程就會進(jìn)入等待狀態(tài)竖共。
計時等待:有幾個方法有超時參數(shù)蝙叛,調(diào)用這些方法,線程就會進(jìn)入計時等待狀態(tài)公给。例如Tread.sleep()
終止線程
run方法正常推出或者出現(xiàn)了一個沒有被捕獲的異常而意外終止借帘。
線程屬性
線程屬性
中斷線程
線程有一個中斷狀態(tài),可以調(diào)用interrupt方法來設(shè)置它淌铐。書上講的不是很好姻蚓,這里引用別人的文章。
守護(hù)線程
可以通過調(diào)用
t.setDaemon(boolean isDaemon)匣沼;
將一個線程轉(zhuǎn)換為守護(hù)線程(daemon thread)狰挡。這樣一個線程沒什么用。唯一用途是為其它線程提供服務(wù)释涛。
在Java中有兩類線程:用戶線程 (User Thread)加叁、守護(hù)線程 (Daemon Thread)。
所謂守護(hù) 線程唇撬,是指在程序運行的時候在后臺提供一種通用服務(wù)的線程它匕,比如垃圾回收線程就是一個很稱職的守護(hù)者,并且這種線程并不屬于程序中不可或缺的部分窖认。因此豫柬,當(dāng)所有的非守護(hù)線程結(jié)束時告希,程序也就終止了,同時會殺死進(jìn)程中的所有守護(hù)線程烧给。反過來說燕偶,只要任何非守護(hù)線程還在運行,程序就不會終止础嫡。
用戶線程和守護(hù)線程兩者幾乎沒有區(qū)別指么,唯一的不同之處就在于虛擬機(jī)的離開:如果用戶線程已經(jīng)全部退出運行了,只剩下守護(hù)線程存在了榴鼎,虛擬機(jī)也就退出了伯诬。 因為沒有了被守護(hù)者,守護(hù)線程也就沒有工作可做了巫财,也就沒有繼續(xù)運行程序的必要了盗似。
線程名
這里的線程名不是指線程實例的標(biāo)識符,可以通過
t.setName("Web crawler");
來設(shè)置名字平项,這在線程轉(zhuǎn)儲時可能很有用赫舒。
未捕獲異常的處理器
我們經(jīng)常使用try..catch進(jìn)行異常處理,但是對于Uncaught Exception是沒辦法捕獲的葵礼。對于這類異常如何處理呢号阿?
回顧一下thread的run方法,有個特別之處鸳粉,它不會拋出任何檢查型異常扔涧,但異常會導(dǎo)致線程終止運行。這非常糟糕届谈,我們必須要“感知”到異常的發(fā)生枯夜。比如某個線程在處理重要的事務(wù),當(dāng)thread異常終止艰山,我必須要收到異常的報告(email或者短信)湖雹。
在jdk 1.5之前貌似無法直接設(shè)置thread的Uncaught Exception Handler(具體未驗證過),但從1.5開始可以對thread設(shè)置handler曙搬。
1. As the handler for a particular thread
當(dāng) uncaught exception 發(fā)生時, JVM尋找這個線程的異常處理器. 可以使用如下的方法為當(dāng)前線程設(shè)置處理器
public class MyRunnable implements Runnable{
????public void run(){
????????// Other Code
????????Thread.currentThread().setUncaughtExceptionHandler(myHandler);
????????// Other Code
????}
}
2. As the handler for a particular thread group
如果這個線程屬于一個線程組摔吏,且線程級別并未指定異常處理器(像上節(jié)As the handler for a particular thread中那樣),jvm則試圖調(diào)用線程組的異常處理器纵装。
線程組(ThreadGroup)實現(xiàn)了Thread.UncaughtExceptionHandler接口征讲,所以我們只需要在ThreadGroup子類中重寫實現(xiàn)即可
public class ThreadGroup implements Thread.UncaughtExceptionHandler
java.lang.ThreadGroup 類uncaughtException默認(rèn)實現(xiàn)的邏輯如下:
如果父線程組存在, 則調(diào)用它的uncaughtException方法.
如果父線程組不存在, 但指定了默認(rèn)處理器 (下節(jié)中的As the default handler for the application), 則調(diào)用默認(rèn)的處理器
如果默認(rèn)處理器沒有設(shè)置, 則寫錯誤日志.但如果 exception是ThreadDeath實例的話, 忽略。
對應(yīng)JDK的源碼:
public void uncaughtException(Thread t, Throwable e) {
????if (parent!=null) {
????????parent.uncaughtException(t, e);
????}else{? ? ? ? ? ?
????????Thread.UncaughtExceptionHandler ueh = Thread.getDefaultUncaughtExceptionHandler();
????????if(ueh !=null) {? ? ? ? ? ? ? ?
????????????ueh.uncaughtException(t, e);? ? ? ? ? ?
? ? ? ? ?}elseif(!(einstanceofThreadDeath)) {
? ? ? ? ? ? System.err.print("Exception in thread \""+ t.getName() +"\" ");? ? ? ? ? ? ? ? ????????????e.printStackTrace(System.err);? ? ? ? ? ?
????????}? ? ? ?
????}? ?
}
3. As the default handler for the application (JVM)
Thread.setDefaultUncaughtExceptionHandler(myHandler);
線程優(yōu)先級
在Java程序設(shè)計語言中橡娄,每一個線程都有一個優(yōu)先級诗箍。默認(rèn)情況下,一個線程會繼承創(chuàng)建它的那個線程的優(yōu)先級挽唉÷俗妫可以用setPriority方法來設(shè)置線程筷狼。
Java中優(yōu)先級可以設(shè)置為MIN_PRIORITY(1)與MAX_PRIORITY(10)和NORM_PRIORITY(5)。
但是匠童,優(yōu)先級高度依賴于操作系統(tǒng)埂材,不同的操作系統(tǒng)有不同個數(shù)的優(yōu)先級,虛擬機(jī)要映射到平臺的優(yōu)先級俏让。因此現(xiàn)在不要使用優(yōu)先級楞遏。
同步
在大多數(shù)程序中茬暇,會有多個線程共享統(tǒng)一數(shù)據(jù)首昔,如果多個線程對其進(jìn)行修改,就會導(dǎo)致數(shù)據(jù)被破壞糙俗,這種情況叫做競態(tài)條件勒奇。
為了避免多線程破壞共享數(shù)據(jù),就需要學(xué)會同步存取
有個常見但是容易出錯的例子:
多個線程執(zhí)行
i += 1;
問題在于巧骚,這條語句并不是一個原子操作赊颠,它的執(zhí)行步驟分為三布:
1,首先取出i的值劈彪,將其加載到寄存器
2竣蹦,執(zhí)行i+1
3,將結(jié)果寫回 i 沧奴。
相當(dāng)于 i = i +1
當(dāng)一個線程執(zhí)行1痘括,2兩步,這時運行權(quán)被轉(zhuǎn)移滔吠,另一個線程開始執(zhí)行纲菌,就會出現(xiàn)問題。會有一條線程覆蓋結(jié)果導(dǎo)致數(shù)據(jù)破壞疮绷。
在后面的voliate關(guān)鍵詞時會提到翰舌。
鎖對象
有兩種機(jī)制可以防止并發(fā)訪問代碼塊:synchronized關(guān)鍵詞和ReentranLock類。
這里先說ReentranLock類冬骚,基本結(jié)構(gòu)如下
myLock.lock() ;//myLock是一個ReentranLock類的實例
try {
????critical section
}
finally {
myLock.unlock() ;
}
可以用來保護(hù)一個類的方法椅贱,如下
public?class?Bank{
????private?ReentrantLock?banklock?=?new?ReentrantLock();
????...
????public?void?transfer(int?from,?int?to,?int?amount){
????????banklock.lock();
????????try{
????????????...
????????}
????????finally{
????????????banklock.unlock();
????????}
????}
}
每個線程再調(diào)用transfer之前,都要查看是否有鎖只冻。
ReentrantLock類稱為重用(reentrant)鎖庇麦,因為可以在一段有鎖的代碼中調(diào)用另一段有鎖的代碼。鎖有一個持有計數(shù)來跟蹤對lock方法的嵌套調(diào)用属愤。
例如女器,transfer方法調(diào)用getTotalBalance方法,第二個方法也會封鎖bankLock對象住诸,這是bankLock的持有計數(shù)就會加一驾胆,變成2(因為bankLock是Bank類中的私有變量)涣澡。直到鎖的計數(shù)變?yōu)榱悖艜尫沛i丧诺。
條件對象
可以使用一個條件對象來管理那些已經(jīng)獲得了一個鎖卻不能做有用工作的線程入桂。由于歷史原因條件對象經(jīng)常被稱為條件變量。
可以給鎖對象關(guān)聯(lián)一個條件對象condition驳阎。這個對象的功能是可以將一個線程進(jìn)入等待狀態(tài)抗愁,如果滿足條件就可以在其他線程中恢復(fù)等待的線程,使用方法如下:
public?class?Bank{
? ? ...
???private?Lock?bankLock;
???private?Condition?sufficientFunds;
???public?Bank(int?n,?double?initialBalance) {
? ? ? ?...
??????bankLock?=?new?ReentrantLock();
??????sufficientFunds?=?bankLock.newCondition();
???}
???public?void?transfer(int?from,?int?to,?double?amount)?throws?InterruptedException {
??????bankLock.lock();
??????try{
?????????while?(accounts[from]?<?amount)
????????????sufficientFunds.await();
? ? ? ? ...
?????????sufficientFunds.signalAll();
??????}
??????finally {
?????????bankLock.unlock();
??????}
???}
synchronized關(guān)鍵字
先對鎖進(jìn)行總結(jié):
·鎖用來保護(hù)代碼片段呵晚,一次只能有一個線程執(zhí)行被保護(hù)的片段蜘腌。
·鎖可以管理試圖進(jìn)入被保護(hù)代碼段的線程
·一個鎖可以一個或多個相關(guān)聯(lián)的條件對象
·每個條件對象,管理那些已經(jīng)進(jìn)入被保護(hù)代碼段但還不能與逆行的線程饵隙。
Lock和Condition兩個接口已經(jīng)允許程序員充分控制鎖定撮珠,但是大部分情況下這樣比較麻煩〗鹈可以使用一種Java語言的內(nèi)置機(jī)制芯急,synchronized關(guān)鍵字。
Java中的每個對象都有一個內(nèi)部鎖驶俊,這個鎖只有一個關(guān)聯(lián)條件娶耍,可以直接調(diào)用wait()和notifyAll來代替condition.await()和condition.signalAll();
原理是一樣的,用synchronized的內(nèi)部鎖來封鎖整個方法饼酿。
結(jié)構(gòu)類似榕酒。
內(nèi)部鎖和條件存在一些限制:
·不能中斷一個正在嘗試獲得鎖的線程
·不能指定嘗試獲得鎖時的超時時間。
·每個鎖僅有一個條件可能是不夠的
在代碼中使用哪種做法有一些建議:
·最好既不使Lock/Condition也不使用synchronized關(guān)鍵字嗜湃。在許多情況下奈应,可以使用java.util.concurrent包中的某種機(jī)制,它會為你處理所有的鎖定购披。例如杖挣,使用阻塞隊列來同步完成一個共同任務(wù)的線程。還應(yīng)當(dāng)研究并行流
·如果synchronized關(guān)鍵字適合你的程序刚陡,那么盡量使用這種做法惩妇,這樣可以減少編寫的代碼量。
·如果特別需要Lock/Condition結(jié)構(gòu)提供的額外能力則使用Lock/Condition
同步塊
區(qū)別:
同步方法默認(rèn)用this或者當(dāng)前類class對象作為鎖筐乳;
同步代碼塊可以選擇以什么來加鎖歌殃,比同步方法要更細(xì)顆粒度,我們可以選擇只同步會發(fā)生同步問題的部分代碼而不是整個方法蝙云;
同步方法使用關(guān)鍵字 synchronized修飾方法氓皱,而同步代碼塊主要是修飾需要進(jìn)行同步的代碼,用 synchronized(object){代碼內(nèi)容}進(jìn)行修飾;
同步代碼塊波材,即有synchronized修飾符修飾的語句塊股淡,被該關(guān)鍵詞修飾的語句塊,將加上內(nèi)置鎖廷区。實現(xiàn)同步唯灵。
例:synchronized(Object o ){}
同步是高開銷的操作,因此盡量減少同步的內(nèi)容隙轻。通常沒有必要同步整個方法埠帕,同步部分代碼塊即可。
同步方法默認(rèn)用this或者當(dāng)前類class對象作為鎖玖绿。
同步代碼塊可以選擇以什么來加鎖敛瓷,比同步方法要更顆粒化镰矿,我們可以選擇只同步會發(fā)生問題的部分代碼而不是整個方法琐驴。
監(jiān)視器概念
監(jiān)視器(monitor)是一個相互排斥且具備同步能力的對象俘种。監(jiān)視器中的一個時間點上秤标,只能有一個線程執(zhí)行一個方法。線程通過獲取監(jiān)視器上的鎖進(jìn)入監(jiān)視器宙刘,并且通過釋放鎖退出監(jiān)視器苍姜。任意對象都可能是一個監(jiān)視器。一旦一個線程鎖住對象悬包,該對象就成為監(jiān)視器衙猪。加鎖是通過在方法或塊上使用synchronized關(guān)鍵字來實現(xiàn)的。在執(zhí)行同步方法或塊之前布近,線程必須獲得鎖垫释。如果條件不適合線程繼續(xù)在監(jiān)視器內(nèi)執(zhí)行,縣城可能在監(jiān)視器中等待撑瞧】闷可以對監(jiān)視器調(diào)用wait()方法來釋放鎖,這樣其他的一些監(jiān)視器中的線程就可以獲取它预伺,也就有可能改變監(jiān)視器中的狀態(tài)订咸。當(dāng)條件滿足時,另一線程可以調(diào)用notify()方法或notifyAll()方法來通知一個或所有的等待線程重新獲取鎖并且恢復(fù)執(zhí)行酬诀。
關(guān)鍵詞volatile
也可以用volatile來進(jìn)行同步脏嚷,但是,volatile并不保證原子性瞒御。
volatile可以用來修飾線程之間共享的變量父叙,如果只是對這個變量進(jìn)行原子性操作,例如賦值,就可以用這個來避免使用鎖趾唱。
這里的原子性:
volatile int i = 1屿岂;
...
i = 2;//是原子性操作
i += 1;//不是原子性操作,分三步
i = atomiclong.incrementAndGet();//是原子性鲸匿,這是一個用機(jī)器碼寫的自增方法爷怀,沒有用鎖。
final變量
final某些方面也可以保證能安全的訪問一個字段
final var accounts = new HashMap<String, Double>();
上面這個聲明的作用带欢,是保證一個線程在創(chuàng)建accounts時运授,其他線程不會讀到null,只會訪問到新構(gòu)造的HashMap乔煞。但這不是線程安全的吁朦,注意。
死鎖
例如轉(zhuǎn)賬程序渡贾,有幾種可能會導(dǎo)致死鎖的情況逗宜。
比如各賬戶轉(zhuǎn)賬,但是每個賬戶的余額都不夠空骚;或者signalAll方法改為signal纺讲,隨機(jī)喚醒一個等待中的線程,但是這個線程的條件仍然不滿足囤屹,繼續(xù)轉(zhuǎn)賬導(dǎo)致錢不夠熬甚,運行的線程也等待,導(dǎo)致所有線程等待肋坚。
線程局部變量