一、并發(fā)
并發(fā)就是讓計(jì)算機(jī)同時(shí)做幾件事情,比如一個(gè)服務(wù)端同時(shí)為多個(gè)客戶端提供服務(wù)。因?yàn)橛?jì)算機(jī)的運(yùn)算速度和存儲(chǔ)/通信子系統(tǒng)的速度相差太大,大量時(shí)間用在磁盤(pán)IO/網(wǎng)絡(luò)通信/數(shù)據(jù)庫(kù)訪問(wèn)崭捍。通常用1秒內(nèi)服務(wù)端響應(yīng)的請(qǐng)求數(shù)目來(lái)衡量服務(wù)器性能;服務(wù)端是java語(yǔ)言最擅長(zhǎng)的領(lǐng)域之一啰脚,虛擬機(jī)通過(guò)多線程的協(xié)調(diào)保證并發(fā)效率:線程間的協(xié)調(diào)好則效率高殷蛇,協(xié)調(diào)不好比如頻繁阻塞/死鎖則效率低。
Amdahl定律通過(guò)系統(tǒng)中并行化與串行化的比重來(lái)描述多處理器能獲得的運(yùn)算加速能力橄浓;摩爾定律則描述處理器晶體管數(shù)量與運(yùn)行效率直接的關(guān)系粒梦。這兩個(gè)定律是硬件發(fā)展從追求處理器頻率到追求多核心并行處理的發(fā)展過(guò)程。
1. 硬件的并發(fā)與一致性
1)高速緩存:計(jì)算機(jī)的存儲(chǔ)設(shè)備與處理器的運(yùn)算速度有幾個(gè)量級(jí)的差距荸实,這也是為什么有接近處理器運(yùn)算速度的高速緩存匀们,緩存就相當(dāng)于中間件。緩存的問(wèn)題在于數(shù)據(jù)一致性准给,多處理器系統(tǒng)中泄朴,每個(gè)處理器有自己的緩存,對(duì)接同一個(gè)主內(nèi)存露氮,當(dāng)緩存數(shù)據(jù)不一致時(shí)祖灰,以誰(shuí)為準(zhǔn)呢?緩存一致性協(xié)議用于解決該問(wèn)題畔规。
2)亂序執(zhí)行/指令重排:CPU允許將多條指令不按程序順序分開(kāi)發(fā)送給電路單元處理夫植;并非隨意打亂順序,CPU處理指令依賴情況以保證最終結(jié)果的一致性油讯。
二、Java內(nèi)存模型JMM
JMM的設(shè)計(jì)是為了屏蔽硬件平臺(tái)/操作系統(tǒng)的內(nèi)存差異延欠,實(shí)現(xiàn)java程序在各平臺(tái)的一致內(nèi)存訪問(wèn)陌兑;JMM定義了變量的訪問(wèn)規(guī)則,包括變量存儲(chǔ)到內(nèi)存/從內(nèi)存取出變量由捎,變量指實(shí)例/靜態(tài)字段/數(shù)組兔综,不包括棧上的局部變量/方法參數(shù),后者線程私有,不存在競(jìng)爭(zhēng)软驰。
1. 主內(nèi)存與工作內(nèi)存
JMM規(guī)定所有變量存儲(chǔ)在主內(nèi)存(類比主內(nèi)存)涧窒,每個(gè)線程有自己的工作內(nèi)存(類比高速緩存),保存該線程用到的變量的主內(nèi)存拷貝锭亏,線程對(duì)變量的操作在工作內(nèi)存進(jìn)行纠吴。線程間的值傳遞通過(guò)主內(nèi)存進(jìn)行。虛擬機(jī)往往讓工作內(nèi)存優(yōu)先存儲(chǔ)于寄存器/高速緩存慧瘤。
JMM定義的內(nèi)存操作包括:
1)鎖定/解鎖:加鎖把主內(nèi)存變量標(biāo)記為線程獨(dú)占戴已;解鎖后才能被其他線程鎖定
2)讀取/存儲(chǔ):把主內(nèi)存變量的值讀取到工作內(nèi)存;把工作內(nèi)存的變量傳送到主內(nèi)存
3)載入/寫(xiě)入:把讀取操作從主內(nèi)存得到的變量放入工作內(nèi)存的變量副本锅减;把從工作內(nèi)存得到的變量值放入主內(nèi)存變量糖儡。
4)使用/賦值:把工作內(nèi)存變量傳遞給執(zhí)行引擎;把執(zhí)行引擎返回值賦給工作內(nèi)存變量
JMM定義的內(nèi)存操作規(guī)則:1)變量在工作內(nèi)存變化后必須同步回主內(nèi)存怔匣;2)新變量只能在主內(nèi)存中誕生握联;3)一個(gè)變量同一時(shí)刻只能被一個(gè)線程加鎖;對(duì)變量加鎖會(huì)清空工作內(nèi)存中此變量的值每瞒;對(duì)變量解鎖前金闽,必須先同步回主內(nèi)存。
2. 并發(fā)安全特性
1)原子性:一個(gè)或多個(gè)語(yǔ)句要么全部執(zhí)行并且執(zhí)行的過(guò)程不被任何因素打斷独泞,要么都不執(zhí)行呐矾。基本數(shù)據(jù)類型的訪問(wèn)讀寫(xiě)是具有原子性的懦砂;更大范圍的原子性通過(guò)加鎖/解鎖實(shí)現(xiàn)(java synchronised)蜒犯,加鎖使得并發(fā)的過(guò)程看起來(lái)像是串行的,生活中的類比可以想一想火車上的廁所荞膘。
2)可見(jiàn)性:當(dāng)線程修改共享變量的值罚随,其他線程能夠立即得知;JMM規(guī)定變量在工作內(nèi)存修改后必須同步回主內(nèi)存羽资;java的volatile/synchronized/final可以保證可見(jiàn)性:1)java volatile保證立即同步到主內(nèi)存淘菩,并且每次使用前從主內(nèi)存刷新,各個(gè)工作內(nèi)存中volatile變量的值可以不同屠升;2)synchronized保證對(duì)變量解鎖前先同步回主內(nèi)存潮改;3)final修飾的字段在構(gòu)造器中初始化完成后,就對(duì)其他線程可見(jiàn)腹暖。
3)有序性:volatile禁止指令重排汇在;Synchronized:變量同一時(shí)刻只允許一個(gè)線程加鎖。
4)先行發(fā)生原則:如果操作A發(fā)生在操作B之前脏答,那么A產(chǎn)生的影響能夠被B觀察到糕殉,包括共享變量的修改亩鬼,發(fā)送消息等。該原則用于判斷是否線程安全阿蝶,是否會(huì)發(fā)生指令重排雳锋。虛擬機(jī)保證以下次序:1. 程序次序規(guī)則:一個(gè)線程內(nèi),按照代碼順序執(zhí)行羡洁;2. 管程鎖定規(guī)則:同一個(gè)鎖玷过,解鎖先行于加鎖;3. volatile規(guī)則:對(duì)volatile的寫(xiě)操作先行發(fā)生于讀操作焚廊;4. 線程啟動(dòng)/終止規(guī)則:Thread對(duì)象的start方法>該線程的所有動(dòng)作>線程的終止檢測(cè)冶匹;5. 線程中斷規(guī)則:對(duì)線程interrupt方法的調(diào)用先行發(fā)生于中斷檢測(cè)代碼如interrupted;6. 對(duì)象終結(jié)原則:對(duì)象初始化 > finalize咆瘟;7. 先行發(fā)生原則具有傳遞性:如果A>B; B>C嚼隘,那么A>C
三、Java與線程
線程是比進(jìn)程更輕量級(jí)的調(diào)度執(zhí)行單位袒餐,是CPU調(diào)度的基本單位飞蛹。Java語(yǔ)言級(jí)別的支持就是Thread類,他的關(guān)鍵方法都聲明為native灸眼。
1.?線程實(shí)現(xiàn)
1)使用內(nèi)核線程實(shí)現(xiàn):內(nèi)核指的是操作系統(tǒng)內(nèi)核卧檐,由OS負(fù)責(zé)線程調(diào)度/切換/把線程交給處理器執(zhí)行;內(nèi)核線程的高級(jí)接口稱為輕量級(jí)進(jìn)程焰宣;2)使用用戶線程實(shí)現(xiàn):用戶線程是和內(nèi)核線程相對(duì)的概念霉囚;沒(méi)有內(nèi)核的支持,實(shí)現(xiàn)復(fù)雜匕积,應(yīng)用少盈罐;3)使用用戶線程加輕量級(jí)進(jìn)程混合實(shí)現(xiàn)
2.?Java線程調(diào)度
線程調(diào)度是系統(tǒng)為線程分配處理器使用權(quán)的過(guò)程。分為:
1)協(xié)同式線程調(diào)度:線程執(zhí)行時(shí)間由線程本身控制闪唆,執(zhí)行完成后通知系統(tǒng)切換到其他線程盅粪,不存在線程同步問(wèn)題;缺點(diǎn)是線程執(zhí)行時(shí)間不可控制悄蕾。
2)搶占式線程調(diào)度(java):系統(tǒng)負(fù)責(zé)分配線程執(zhí)行時(shí)間票顾,線程切換不由線程本身決定。如java中yield方法可以讓出執(zhí)行時(shí)間帆调,但無(wú)法獲取執(zhí)行時(shí)間奠骄。通過(guò)設(shè)置線程優(yōu)先級(jí)給某些線程多分配時(shí)間,其他線程少分配一些番刊,java提供10個(gè)優(yōu)先級(jí)級(jí)別含鳞。但是優(yōu)先級(jí)不一定靠譜,還要看操作系統(tǒng)撵枢。
3. 線程狀態(tài)轉(zhuǎn)換
Java語(yǔ)言定義了5中線程狀態(tài):
1)新建new:創(chuàng)建后尚未啟動(dòng)的線程民晒。Thread t = new Thread(new RunnableTask());
2)運(yùn)行Runnable:正在執(zhí)行或者等待CPU分配執(zhí)行時(shí)間;Thread.isAlive()锄禽;
3)等待分為:1. 無(wú)限期等待:不會(huì)被CPU分配時(shí)間潜必,等待其他線程喚醒notify(),沒(méi)有timeout參數(shù)的wait()/join()/park()方法會(huì)導(dǎo)致該狀態(tài)沃但;2. 限期等待:不會(huì)被CPU分配時(shí)間磁滚,一定時(shí)間后系統(tǒng)喚醒,如TimeUnit.MILLISECONDS.sleep(100)宵晚,設(shè)置timeout的上述方法垂攘。
當(dāng)任務(wù)依賴于另一任務(wù)計(jì)算的值,則調(diào)用join()將異步執(zhí)行的線程變成同步淤刃,在線程A中調(diào)用線程B的join方法晒他,那么線程A會(huì)被掛起,直至線程B執(zhí)行結(jié)束(alive)逸贾。
4)阻塞:和等待的區(qū)別在于阻塞在等待獲取排他鎖synchronized陨仅,而等待是在等待時(shí)間
5)結(jié)束:task執(zhí)行完成/run方法正常結(jié)束;interrupt(類似異常退出線程執(zhí)行/類似于break)铝侵;Thread.cancel()灼伤;
四、java對(duì)象線程安全級(jí)別
對(duì)象在一項(xiàng)工作期間咪鲜,不停的中斷和切換狐赡,對(duì)象的屬性可能會(huì)在中斷期間被修改,從而引發(fā)線程安全問(wèn)題疟丙。當(dāng)多個(gè)線程訪問(wèn)一個(gè)對(duì)象颖侄,如果不用考慮這些線程在運(yùn)行時(shí)的調(diào)度/交替,也不需要額外的同步隆敢,調(diào)用行為都可以獲得正確的結(jié)果发皿,那這個(gè)對(duì)象是線程安全的,其代碼本身封裝了正確性保障手段(互斥同步)拂蝎。線程安全根源在于共享數(shù)據(jù)穴墅。
1)不可變Immutable:不可變的對(duì)象(string/final/Number類型)永遠(yuǎn)不會(huì)在多個(gè)線程處于不一致的狀態(tài)。如果共享數(shù)據(jù)是一個(gè)基本數(shù)據(jù)類型温自,可以聲明為final玄货;對(duì)象類型,可以保證對(duì)象行為不會(huì)對(duì)其狀態(tài)產(chǎn)生影響悼泌,比如把對(duì)象中的狀態(tài)變量都用final修飾松捉。
2)絕對(duì)線程安全:基本不存在。
3)相對(duì)線程安全:通常說(shuō)的線程安全都是相對(duì)線程安全馆里;保證對(duì)象的單獨(dú)操作是線程安全的隘世,但對(duì)于連續(xù)操作可柿,則仍然可能需要調(diào)用端用同步手段保證正確性。如Vector/HashTable丙者,他的add/get/size方法都用synchronized修飾复斥;
4)線程兼容:通常說(shuō)的非線程安全,但通過(guò)調(diào)用端的同步手段能保證正確性械媒,如ArrayList和HashMap目锭。
5)線程對(duì)立:基本不存在,無(wú)論調(diào)用端怎么做纷捞,都無(wú)法在多線程中并發(fā)使用的代碼痢虹。
五、線程安全的實(shí)現(xiàn)方法
1.?互斥同步 mutex(mutual exclusion)&synchronization
常用主儡。同步指變量同一時(shí)刻只被一個(gè)線程使用奖唯;互斥是實(shí)現(xiàn)方式(臨界區(qū)/互斥量/信號(hào)量),是一種悲觀并發(fā)策略。
java通過(guò)synchronized實(shí)現(xiàn)互斥同步,編譯后在同步塊前后形成monitorenter/monitorexit字節(jié)碼指令罢艾。字節(jié)碼指令參數(shù)reference表示鎖定對(duì)象。若synchronised明確指定對(duì)象參數(shù)瓢阴,則該對(duì)象作為reference;如果synchronized修飾的是實(shí)例/類方法健无,則相應(yīng)的對(duì)象實(shí)例/class對(duì)象就是鎖對(duì)象荣恐。執(zhí)行monitorenter指令時(shí),嘗試獲取對(duì)象的鎖累贤,如果對(duì)象沒(méi)有被鎖定或者當(dāng)前線程已經(jīng)持有對(duì)象鎖(可重入)叠穆,就把鎖的計(jì)數(shù)器加1;執(zhí)行monitorexit時(shí)鎖計(jì)數(shù)器減1臼膏,計(jì)數(shù)為0時(shí)釋放鎖硼被。獲取對(duì)象鎖失敗,則當(dāng)前線程阻塞渗磅,直到對(duì)象鎖被另一個(gè)線程釋放嚷硫。
互斥同步是重量級(jí)操作,阻塞/喚醒線程需要從用戶態(tài)切換到內(nèi)核態(tài)始鱼。確實(shí)必要才使用synchronized仔掸,虛擬機(jī)優(yōu)化如在通知OS阻塞線程之前加入自旋等待。
2.?非阻塞同步
隨著硬件指令集發(fā)展医清,基于沖突檢測(cè)的樂(lè)觀并發(fā)策略:先操作起暮,沒(méi)有其他線程爭(zhēng)用共享數(shù)據(jù)則操作成功;有競(jìng)爭(zhēng)則采取補(bǔ)償措施如重試会烙。
1)testAndSet负懦;2)getAndIncrement:調(diào)用了CAS筒捺,原子類具有該方法,原子類提供的加減操作都是線程安全的纸厉;3)swap焙矛;4)compareAndSwap/CAS:3個(gè)操作數(shù):內(nèi)存位置V/舊的預(yù)期值A(chǔ)/新值B。CAS指令執(zhí)行時(shí)残腌,如果V==A,則V=B贫导,返回V舊值抛猫;否則不更新,返回V舊值孩灯。具有ABA問(wèn)題闺金,如果A改為B后又改回A,則無(wú)法檢測(cè)到修改峰档;5)加載鏈接/條件存儲(chǔ)
3.?無(wú)同步方案
不涉及共享數(shù)據(jù)的方法就不需要同步保證安全败匹。
1)可重入代碼/純代碼:可在代碼執(zhí)行的任何時(shí)刻中斷,轉(zhuǎn)而執(zhí)行另一段代碼讥巡,而在控制權(quán)返回后不會(huì)出現(xiàn)錯(cuò)誤掀亩;用到的狀態(tài)量都由參數(shù)傳入。
2)線程本地存儲(chǔ):如果一段代碼中需要的數(shù)據(jù)必須和其他代碼共享欢顷,那就看這些代碼能否在一個(gè)線程中執(zhí)行槽棍,比如消費(fèi)隊(duì)列的架構(gòu)模式/web交互模型的一個(gè)請(qǐng)求對(duì)應(yīng)一個(gè)服務(wù)器線程;線程提供ThreadLocal用于存儲(chǔ)本線程對(duì)象抬驴。
六炼七、鎖優(yōu)化
1)適應(yīng)性自旋:讓等待鎖的線程稍等一下,不放棄處理器的執(zhí)行時(shí)間布持,執(zhí)行一個(gè)忙循環(huán)(自旋)豌拙;鎖占用時(shí)間短的情況下優(yōu)化效果好。自適應(yīng)意味著自旋時(shí)間不固定题暖,而是由上一個(gè)鎖上的自旋時(shí)間和鎖擁有者的狀態(tài)來(lái)決定按傅。
2)鎖消除:對(duì)代碼上要求鎖,但經(jīng)過(guò)逃逸分析判斷數(shù)據(jù)不會(huì)被其他線程訪問(wèn)/競(jìng)爭(zhēng)的鎖進(jìn)行消除芙委,發(fā)生在JIT優(yōu)化逞敷。
3)鎖粗化:編寫(xiě)代碼時(shí),推薦同步塊盡可能小灌侣,從而等待鎖的線程能盡快拿到鎖推捐。對(duì)于反復(fù)加鎖/釋放鎖的操作,會(huì)采用鎖粗化的優(yōu)化方式侧啼。
此外還有輕量級(jí)鎖和偏向鎖牛柒。