1.線程安全
如果一個(gè)對象能安全地被多個(gè)線程同時(shí)使用渡嚣,那么它就是線程安全的。
當(dāng)多個(gè)線程訪問同一個(gè)對象時(shí),如果不需要考慮這些線程在運(yùn)行時(shí)環(huán)境下的調(diào)度和交替執(zhí)行识椰,也不需要進(jìn)行額外的同步绝葡,或者在調(diào)用方進(jìn)行任何其他的協(xié)調(diào)操作,調(diào)用這個(gè)對象的行為都能得到正確的結(jié)果腹鹉,那這個(gè)對象就是是線程安全的藏畅。
2.Java語言中的線程安全
2.1不可變
JDK1.5后,不可變(Immutable)對象一定是線程安全的种蘸,注意final修飾的基本數(shù)據(jù)類型是不可變的墓赴,但是引用類型只能保證一級不可變竞膳,即當(dāng)前引用不可再賦值航瞭,但引用的對象的非final屬性是可以修改的。
2.2絕對線程安全
無論怎么使用坦辟,都是線程安全的刊侯,滿足上面第二個(gè)定義。
2.3相對線程安全
比如Vector锉走,雖然它的方法加了synchronized關(guān)鍵字滨彻,但并不能說它就是絕對安全的。
2.4線程兼容
對象本身不是線程安全的挪蹭,但是可以通過一定的同步來實(shí)現(xiàn)線程安全亭饵,例如synchronized。
2.5線程對立
如 Thread.suspend()和Thread.resume()
3.線程安全的實(shí)現(xiàn)方法
同步是指在多線程并發(fā)訪問共享數(shù)據(jù)時(shí)梁厉,保證同一時(shí)刻只被一個(gè)(或者是一些辜羊,使用信號(hào)量時(shí))線程使用。
3.1互斥同步
互斥是實(shí)現(xiàn)同步的手段词顾,臨界區(qū)八秃、互斥量、信號(hào)量都是主要的互斥實(shí)現(xiàn)方式肉盹。Java中最基本的互斥手段就是synchronized關(guān)鍵字昔驱,synchronized關(guān)鍵字在編譯后,會(huì)在同步塊前后分別形成monitorenter和monitorexit指令上忍。這兩個(gè)指令需要一個(gè)reference類型的參數(shù)來指明要鎖定和解鎖的對象骤肛。如果synchronized明確指定了對象參數(shù),那就是這個(gè)對象的reference窍蓝,如果沒有指定腋颠,那就要區(qū)分是實(shí)例方法還是類方法,取當(dāng)前對象或者這個(gè)類對應(yīng)的Class對象它抱。
3.2非阻塞同步
互斥同步的主要問題是進(jìn)行線程的阻塞和喚醒所帶來的性能問題秕豫,因?yàn)檫@種方式會(huì)阻塞其他線程,因此也可以稱為阻塞同步。從處理方式上來看混移,屬于悲觀的并發(fā)策略祠墅,即不管有沒有競爭,都進(jìn)行同步歌径。
隨著硬件指令系統(tǒng)的發(fā)展(需要一些原子操作指令的支持毁嗦,例如CAS,早期的計(jì)算機(jī)指令系統(tǒng)可能沒有這樣的指令)回铛,有了另一個(gè)選擇狗准,基于沖突檢測的樂觀并發(fā)策略,即先進(jìn)形操作茵肃,如果沒有其他線程爭用共享數(shù)據(jù)腔长,那就操作成功了,如果存在競爭验残,再采取必要的補(bǔ)救措施捞附,比如不斷地重試,直到成功為止您没。
4.鎖優(yōu)化
4.1自旋鎖與自適應(yīng)自旋
由于掛起線程和喚醒線程需要切換的內(nèi)核狀態(tài)進(jìn)行鸟召,這是個(gè)不小的開銷。虛擬機(jī)的開發(fā)團(tuán)隊(duì)研究發(fā)現(xiàn)許多應(yīng)用氨鹏,共享鎖的鎖定轉(zhuǎn)態(tài)通常是持續(xù)很短的時(shí)間欧募,所以可以讓在等待獲鎖的線程不進(jìn)入阻塞狀態(tài),而是繼續(xù)執(zhí)行自旋操作仆抵,稍等一下就能獲得鎖跟继,這樣做在一定程度上避免用戶態(tài)與內(nèi)核態(tài)的切換,但自旋的線程會(huì)繼續(xù)占用CPU時(shí)間片肢础。
自旋時(shí)間的選擇也是很關(guān)鍵的还栓,自旋多久后仍然沒有獲得鎖就進(jìn)入阻塞狀態(tài)?所以在JDK1.6后传轰,引入了自適應(yīng)的自旋鎖剩盒,由前一次在同一個(gè)鎖上的自旋時(shí)間和鎖的擁有線程的狀態(tài)決定。
4.2鎖消除
虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí)慨蛙,對一些代碼上要求同步辽聊,但被檢測到不可能存在數(shù)據(jù)競爭的鎖進(jìn)行清除。這種通常不是開發(fā)人員自己寫出來的期贫,舉一個(gè)例子:
public String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
由于String是一個(gè)不可變的類跟匆,在字符串連接時(shí)都是創(chuàng)建一個(gè)新的String對象來完成的,因此通砍,javac編譯器對String對象連接做了自動(dòng)化玛臂。在JDK1.5以前烤蜕,是通過StringBuffer來完成,而JDK1.5及之后迹冤,是通過StringBuilder來完成讽营。那JDK1.5之前上面的代碼就會(huì)變成:
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
而StringBuffer的append()方法是加了synchronized關(guān)鍵字的同步方法,但是很顯然這種情況下的同步是完全沒有必要的泡徙,所以虛擬機(jī)將這種鎖清除掉以提高性能橱鹏。
4.3鎖粗化
同步有一個(gè)原則是,讓同步塊盡量小一下堪藐,一般情況下是正確的莉兰,但如果一系列的操作都對同一個(gè)對象進(jìn)行加鎖解鎖,會(huì)帶來不小的性能開銷礁竞,這種情況還不如把同步范圍擴(kuò)大至一系列操作之前糖荒,這樣只需要加/解鎖一次就行了。
4.4輕量鎖
前面《運(yùn)行時(shí)數(shù)據(jù)區(qū)》一文中提到對象的內(nèi)存布局包括3部分:
對象頭(Header)
實(shí)例數(shù)據(jù)(Instance Data)
對齊填充(Padding)非必須
HotSpot虛擬機(jī)的對象頭(Object Header)包含兩部分的信息:
- 第一部分用戶存儲(chǔ)對象自身的運(yùn)行時(shí)數(shù)據(jù)苏章,如 HashCode寂嘉、GC分代年齡(Generational GC Age)等奏瞬,這部分?jǐn)?shù)據(jù)在32bit和64bit的虛擬機(jī)中分別為32bit和64bit枫绅,官方稱它為“Mark Word”,它是輕量級鎖和偏向鎖的關(guān)鍵硼端;
- 另外一部分用于存儲(chǔ)方法區(qū)對象類型的引用并淋,如果是數(shù)組對象的話,還會(huì)有一個(gè)額外的部分用于存儲(chǔ)數(shù)組的長度珍昨。
32bit HotSpot虛擬機(jī)下的對象狀態(tài)县耽,為鎖定狀態(tài)下Mark Word 32bit中,25bit是HashCode镣典,4bit是對象分代年齡兔毙,2bit是標(biāo)記(例如未鎖定是01),1bit固定為0兄春。
存儲(chǔ)內(nèi)容 | 標(biāo)記位 | 狀態(tài) |
---|---|---|
對象哈希碼澎剥,分代年齡 | 01 | 未鎖定 |
指向鎖記錄的指針 | 00 | 輕量級鎖定 |
指向重量級鎖的指針 | 10 | 膨脹(重量級鎖定) |
空,不需要記錄信息 | 11 | GC標(biāo)記 |
偏向線程ID赶舆,偏向時(shí)間戳哑姚,對象分代年齡 | 01 | 可偏向 |
- 在代碼進(jìn)入同步代碼塊時(shí),如果對象是未鎖定狀態(tài)芜茵,虛擬機(jī)會(huì)首先會(huì)在當(dāng)前線程的棧幀中創(chuàng)建一個(gè)鎖記錄(Locked Record)空間叙量,用于存儲(chǔ)鎖對象當(dāng)前的Mark Word拷貝,叫Displaced Mark Word;
- 采用CAS操作將鎖對象的Mark Word修改為指向鎖記錄的指針九串,如果更新成功绞佩,那線程就擁有了該鎖對象,并且對象的Mark Word的標(biāo)記位(最后2bit)修改為00。
輕量級鎖提升性能的依據(jù)是:對于絕大部分的鎖品山,同步過程是不存在競爭的析既。如果沒有競爭,那輕量級的CAS操作避免了互斥量的開銷谆奥,但如果存在競爭眼坏,那性能反而傳統(tǒng)的重量級鎖慢(CAS+互斥信號(hào)量)。
4.5偏量鎖
如果說輕量級鎖是在無競爭條件下酸些,通過CAS操作去消除的同步使用互斥信號(hào)量宰译,那偏向鎖就是在無競爭條件下把整個(gè)同步都消除掉,連CAS也不用做魄懂。
“偏”指的是這個(gè)鎖對象會(huì)偏向第一個(gè)獲取它的線程沿侈,如果在接下來的的執(zhí)行過程中,該鎖沒有被其他線程獲取市栗,則持有該偏向鎖的線程將永遠(yuǎn)不再需要同步缀拭。
當(dāng)鎖對象第一次被獲取時(shí),標(biāo)記為被設(shè)為“01”填帽,即偏向模式蛛淋,并且把獲取到這個(gè)鎖的線程ID記錄在Mark Word中,后面持有偏向鎖的線程在進(jìn)入同步代碼塊篡腌,就不需要再同步了褐荷。