修復(fù)多個(gè)線程訪問(wèn)同一個(gè)可變的狀態(tài)變量沒(méi)有使用合適的同步滨巴,所產(chǎn)生的問(wèn)題:
不在線程之間共享該狀態(tài)變量
將狀態(tài)變量修改為不可變的變量
在訪問(wèn)狀態(tài)變量時(shí)使用同步
線程安全性
當(dāng)多個(gè)線程訪問(wèn)某個(gè)類(lèi)時(shí)矮男,不管運(yùn)行時(shí)環(huán)境采用何種調(diào)度方式或者這些線程將如何交替進(jìn)行湿硝,并且在主調(diào)代碼中不需要任何額外的同步或協(xié)同拆融,這個(gè)類(lèi)都能表現(xiàn)出正確的行為者吁,那么就稱這個(gè)類(lèi)是線程安全的刀闷。
原子性
原子性是指一個(gè)操作是不可中斷的根时,要么全部執(zhí)行成功要么全部執(zhí)行失敗,有著“同生共死”的感覺(jué)垛耳。及時(shí)在多個(gè)線程一起執(zhí)行的時(shí)候栅屏,一個(gè)操作一旦開(kāi)始飘千,就不會(huì)被其他線程所干擾堂鲜。我們先來(lái)看看哪些是原子操作,哪些不是原子操作护奈,有一個(gè)直觀的印象:
int a = 10; //1 a++; //2 int b=a; //3 a = a+1; //4
上面這四個(gè)語(yǔ)句中只有第1個(gè)語(yǔ)句是原子操作缔莲,將10賦值給線程工作內(nèi)存的變量a,而語(yǔ)句2(a++),實(shí)際上包含了三個(gè)操作:1. 讀取變量a的值霉旗;2:對(duì)a進(jìn)行加一的操作痴奏;3.將計(jì)算后的值再賦值給變量a,而這三個(gè)操作無(wú)法構(gòu)成原子操作厌秒。對(duì)語(yǔ)句3,4的分析同理可得這兩條語(yǔ)句不具備原子性读拆。當(dāng)然,java內(nèi)存模型中定義了8中操作都是原子的鸵闪,不可再分的檐晕。
- lock(鎖定):作用于主內(nèi)存中的變量,它把一個(gè)變量標(biāo)識(shí)為一個(gè)線程獨(dú)占的狀態(tài)蚌讼;
- unlock(解鎖):作用于主內(nèi)存中的變量辟灰,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái),釋放后的變量才可以被其他線程鎖定
- read(讀却凼):作用于主內(nèi)存的變量芥喇,它把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便后面的load動(dòng)作使用凰萨;
- load(載入):作用于工作內(nèi)存中的變量继控,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存中的變量副本
- use(使用):作用于工作內(nèi)存中的變量械馆,它把工作內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用到變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作武通;
- assign(賦值):作用于工作內(nèi)存中的變量狱杰,它把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作厅须;
- store(存儲(chǔ)):作用于工作內(nèi)存的變量仿畸,它把工作內(nèi)存中一個(gè)變量的值傳送給主內(nèi)存中以便隨后的write操作使用;
- write(操作):作用于主內(nèi)存的變量朗和,它把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中错沽。
上面的這些指令操作是相當(dāng)?shù)讓拥模梢宰鳛閿U(kuò)展知識(shí)面掌握下眶拉。那么如何理解這些指令了?比如千埃,把一個(gè)變量從主內(nèi)存中復(fù)制到工作內(nèi)存中就需要執(zhí)行read,load操作,將工作內(nèi)存同步到主內(nèi)存中就需要執(zhí)行store,write操作忆植。注意的是:java內(nèi)存模型只是要求上述兩個(gè)操作是順序執(zhí)行的并不是連續(xù)執(zhí)行的放可。也就是說(shuō)read和load之間可以插入其他指令,store和writer可以插入其他指令朝刊。比如對(duì)主內(nèi)存中的a,b進(jìn)行訪問(wèn)就可以出現(xiàn)這樣的操作順序:read a,read b, load b,load a耀里。
由原子性變量操作read,load,use,assign,store,write,可以大致認(rèn)為基本數(shù)據(jù)類(lèi)型的訪問(wèn)讀寫(xiě)具備原子性(例外就是long和double的非原子性協(xié)定)
synchronized
上面一共有八條原子操作拾氓,其中六條可以滿足基本數(shù)據(jù)類(lèi)型的訪問(wèn)讀寫(xiě)具備原子性冯挎,還剩下lock和unlock兩條原子操作。如果我們需要更大范圍的原子性操作就可以使用lock和unlock原子操作咙鞍。盡管jvm沒(méi)有把lock和unlock開(kāi)放給我們使用房官,但jvm以更高層次的指令monitorenter和monitorexit指令開(kāi)放給我們使用,反應(yīng)到j(luò)ava代碼中就是---synchronized關(guān)鍵字续滋,也就是說(shuō)synchronized滿足原子性翰守。
volatile
我們先來(lái)看這樣一個(gè)例子:
public class VolatileExample {
private static volatile int counter = 0;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; I++)
counter++;
}
});
thread.start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter);
}
}
開(kāi)啟10個(gè)線程,每個(gè)線程都自加10000次疲酌,如果不出現(xiàn)線程安全的問(wèn)題最終的結(jié)果應(yīng)該就是:10*10000 = 100000;可是運(yùn)行多次都是小于100000的結(jié)果蜡峰,問(wèn)題在于 volatile并不能保證原子性,在前面說(shuō)過(guò)counter++這并不是一個(gè)原子操作徐勃,包含了三個(gè)步驟:1.讀取變量counter的值事示;2.對(duì)counter加一;3.將新值賦值給變量counter僻肖。如果線程A讀取counter到工作內(nèi)存后肖爵,其他線程對(duì)這個(gè)值已經(jīng)做了自增操作后,那么線程A的這個(gè)值自然而然就是一個(gè)過(guò)期的值臀脏,因此劝堪,總結(jié)果必然會(huì)是小于100000的冀自。
如果讓volatile保證原子性,必須符合以下兩條規(guī)則:
- 運(yùn)算結(jié)果并不依賴于變量的當(dāng)前值秒啦,或者能夠確保只有一個(gè)線程修改變量的值熬粗;
- 變量不需要與其他的狀態(tài)變量共同參與不變約束 .
競(jìng)態(tài)條件
競(jìng)態(tài)條件(Race Condition):計(jì)算的正確性取決于多個(gè)線程的交替執(zhí)行時(shí)序時(shí),就會(huì)發(fā)生競(jìng)態(tài)條件余境。
最常見(jiàn)的競(jìng)態(tài)條件為:
一驻呐,先檢測(cè)后執(zhí)行。執(zhí)行依賴于檢測(cè)的結(jié)果芳来,而檢測(cè)結(jié)果依賴于多個(gè)線程的執(zhí)行時(shí)序含末,而多個(gè)線程的執(zhí)行時(shí)序通常情況下是不固定不可判斷的,從而導(dǎo)致執(zhí)行結(jié)果出現(xiàn)各種問(wèn)題即舌。
競(jìng)態(tài)條件(Race Condition):計(jì)算的正確性取決于多個(gè)線程的交替執(zhí)行時(shí)序時(shí)佣盒,就會(huì)發(fā)生競(jìng)態(tài)條件。
最常見(jiàn)的競(jìng)態(tài)條件為:
一顽聂,先檢測(cè)后執(zhí)行肥惭。執(zhí)行依賴于檢測(cè)的結(jié)果,而檢測(cè)結(jié)果依賴于多個(gè)線程的執(zhí)行時(shí)序紊搪,而多個(gè)線程的執(zhí)行時(shí)序通常情況下是不固定不可判斷的蜜葱,從而導(dǎo)致執(zhí)行結(jié)果出現(xiàn)各種問(wèn)題。
對(duì)于main線程嗦明,如果文件a不存在笼沥,則創(chuàng)建文件a蚪燕,但是在判斷文件a不存在之后娶牌,Task線程創(chuàng)建了文件a,這時(shí)候先前的判斷結(jié)果已經(jīng)失效馆纳,(main線程的執(zhí)行依賴了一個(gè)錯(cuò)誤的判斷結(jié)果)此時(shí)文件a已經(jīng)存在了诗良,但是main線程還是會(huì)繼續(xù)創(chuàng)建文件a,導(dǎo)致Task線程創(chuàng)建的文件a被覆蓋鲁驶、文件中的內(nèi)容丟失等等問(wèn)題鉴裹。
多線程環(huán)境中對(duì)同一個(gè)文件的操作要加鎖。
二钥弯,延遲初始化(最典型即為單例)
public class ObjFactory {
private Obj instance;
public Obj getInstance(){
if(instance == null){
instance = new Obj();
}
return instance;
}
}
線程a和線程b同時(shí)執(zhí)行g(shù)etInstance()径荔,線程a看到instance為空,創(chuàng)建了一個(gè)新的Obj對(duì)象脆霎,此時(shí)線程b也需要判斷instance是否為空总处,此時(shí)的instance是否為空取決于不可預(yù)測(cè)的時(shí)序:包括線程a創(chuàng)建Obj對(duì)象需要多長(zhǎng)時(shí)間以及線程的調(diào)度方式,如果b檢測(cè)時(shí)睛蛛,instance為空鹦马,那么b也會(huì)創(chuàng)建一個(gè)instance對(duì)象
復(fù)合操作
實(shí)際情況中胧谈,應(yīng)盡可能使用現(xiàn)有的線程安全對(duì)象來(lái)管理類(lèi)的狀態(tài)。與非線程安全的對(duì)象相比荸频,判斷安全對(duì)象的可能狀態(tài)及其狀態(tài)換情況要更為容易菱肖,從而也更容易維護(hù)和驗(yàn)證線程安全性。
(例如AcomicLong) java.util.concurrent.atomic
一個(gè)類(lèi)中很多安全的狀態(tài)變量旭从,并不一定線程安全稳强。
要保持狀態(tài)的一致性,就需要在單個(gè)原子操作中更新所有相關(guān)的狀態(tài)變量和悦。
內(nèi)置鎖
同步代碼塊:
一個(gè)作為鎖的對(duì)象引
一個(gè)作為由這個(gè)鎖保護(hù)的代碼塊
以關(guān)鍵字synchronized來(lái)修飾的方法就是一種橫跨整個(gè)方法體的同步代碼塊键袱,其中該同步代碼塊的鎖就是方法調(diào)用所在的對(duì)象。靜態(tài)的synchronized方法以Class對(duì)象作為鎖摹闽。
synchronized (lock){
//訪問(wèn)或修改由鎖保護(hù)的共享狀態(tài)
}
每個(gè)java對(duì)象都可以用作一個(gè)實(shí)現(xiàn)同步的鎖蹄咖,這些鎖被稱為內(nèi)置鎖或監(jiān)視器鎖。線程在進(jìn)入同步代碼塊之前會(huì)自動(dòng)獲得鎖付鹿,并且在退出同步代碼塊時(shí)自動(dòng)釋放鎖澜汤,而無(wú)論通過(guò)正常的控制途徑退出,還是通過(guò)從代碼塊中拋出異常退出舵匾,獲得內(nèi)置鎖唯一的途徑就是進(jìn)入這個(gè)鎖保護(hù)的同步代碼塊或同步方法俊抵。
java的內(nèi)置鎖相當(dāng)于一種互斥體(互斥鎖),這意味著最多只有一個(gè)線程能持有這種鎖坐梯,當(dāng)線程A嘗試獲取一個(gè)線程b持有的鎖時(shí)徽诲,線程A必須等待或者阻塞,知道線程b釋放這個(gè)鎖吵血。如果b永遠(yuǎn)不釋放鎖谎替,那么A也將永遠(yuǎn)的等待下去。
由于每次只能有一個(gè)線程執(zhí)行內(nèi)置鎖所保護(hù)的代碼蹋辅,因此钱贯,由這個(gè)鎖保護(hù)的同步代碼塊會(huì)以原子方式執(zhí)行,多個(gè)線程在執(zhí)行該代碼塊時(shí)也不會(huì)相互干擾侦另。并發(fā)環(huán)境中的原子性與事物應(yīng)用程序中原子性有著相同的含義- 一組語(yǔ)句作為一個(gè)不可分割的單元被執(zhí)行秩命,任何一個(gè)執(zhí)行同步代碼塊的線程,都不可能看到有其他線程正在執(zhí)行由同一個(gè)鎖保護(hù)的同步代碼塊褒傅。