0 前言
在單線程中不會出現(xiàn)線程安全問題路呜,而在多線程編程中迷捧,有可能會出現(xiàn)同時訪問同一個 共享、可變資源 的情況胀葱,這種資源可以是:一個變量漠秋、一個對象、一個文件等抵屿。特別注意兩點:
- 共享: 意味著該資源可以由多個線程同時訪問庆锦;
- 可變: 意味著該資源可以在其生命周期內(nèi)被修改;
簡單的說轧葛,如果你的代碼在單線程下執(zhí)行和在多線程下執(zhí)行永遠(yuǎn)都能獲得一樣的結(jié)果搂抒,那么你的代碼就是線程安全的。那么尿扯,當(dāng)進(jìn)行多線程編程時求晶,我們又會面臨哪些線程安全的要求呢?又是要如何去解決的呢衷笋?
1 線程安全特性
1.1 原子性
跟數(shù)據(jù)庫事務(wù)的原子性概念差不多芳杏,即一個操作(有可能包含有多個子操作)要么全部執(zhí)行(生效),要么全部都不執(zhí)行(都不生效)辟宗。
關(guān)于原子性爵赵,一個非常經(jīng)典的例子就是銀行轉(zhuǎn)賬問題:
比如:A和B同時向C轉(zhuǎn)賬10萬元。如果轉(zhuǎn)賬操作不具有原子性慢蜓,A在向C轉(zhuǎn)賬時亚再,讀取了C的余額為20萬,然后加上轉(zhuǎn)賬的10萬晨抡,計算出此時應(yīng)該有30萬氛悬,但還未來及將30萬寫回C的賬戶则剃,此時B的轉(zhuǎn)賬請求過來了,B發(fā)現(xiàn)C的余額為20萬如捅,然后將其加10萬并寫回棍现。然后A的轉(zhuǎn)賬操作繼續(xù)——將30萬寫回C的余額。這種情況下C的最終余額為30萬镜遣,而非預(yù)期的40萬己肮。
1.2 可見性
可見性是指,當(dāng)多個線程并發(fā)訪問共享變量時悲关,一個線程對共享變量的修改谎僻,其它線程能夠立即看到≡⑷瑁可見性問題是好多人忽略或者理解錯誤的一點艘绍。
CPU從主內(nèi)存中讀數(shù)據(jù)的效率相對來說不高,現(xiàn)在主流的計算機(jī)中秫筏,都有幾級緩存诱鞠。每個線程讀取共享變量時,都會將該變量加載進(jìn)其對應(yīng)CPU的高速緩存里这敬,修改該變量后航夺,CPU會立即更新該緩存,但并不一定會立即將其寫回主內(nèi)存(實際上寫回主內(nèi)存的時間不可預(yù)期)崔涂。此時其它線程(尤其是不在同一個CPU上執(zhí)行的線程)訪問該變量時阳掐,從主內(nèi)存中讀到的就是舊的數(shù)據(jù),而非第一個線程更新后的數(shù)據(jù)冷蚂。
這一點是操作系統(tǒng)或者說是硬件層面的機(jī)制锚烦,所以很多應(yīng)用開發(fā)人員經(jīng)常會忽略。
1.3 有序性
有序性指的是帝雇,程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。以下面這段代碼為例:
boolean started = false; // 語句1
long counter = 0L; // 語句2
counter = 1; // 語句3
started = true; // 語句4
從代碼順序上看蛉拙,上面四條語句應(yīng)該依次執(zhí)行尸闸,但實際上JVM真正在執(zhí)行這段代碼時,并不保證它們一定完全按照此順序執(zhí)行孕锄。
處理器為了提高程序整體的執(zhí)行效率遏片,可能會對代碼進(jìn)行優(yōu)化氓皱,其中的一項優(yōu)化方式就是調(diào)整代碼順序,按照更高效的順序執(zhí)行代碼。
講到這里缩膝,有人要著急了——什么,CPU不按照我的代碼順序執(zhí)行代碼膜楷,那怎么保證得到我們想要的效果呢?實際上抡砂,大家大可放心,CPU雖然并不保證完全按照代碼順序執(zhí)行恬涧,但它會保證程序最終的執(zhí)行結(jié)果和代碼順序執(zhí)行時的結(jié)果一致注益。
2 線程安全問題
2.1 競態(tài)條件與臨界區(qū)
線程之間共享堆空間,在編程的時候就要格外注意避免競態(tài)條件溯捆。危險在于多個線程同時訪問相同的資源并進(jìn)行讀寫操作丑搔。當(dāng)其中一個線程需要根據(jù)某個變量的狀態(tài)來相應(yīng)執(zhí)行某個操作的之前,該變量很可能已經(jīng)被其它線程修改提揍。
也就是說啤月,當(dāng)兩個線程競爭同一資源時,如果對資源的訪問順序敏感劳跃,就稱存在 競態(tài)條件谎仲。導(dǎo)致竟態(tài)條件發(fā)生的代碼稱作 臨界區(qū)。
/**
* 以下這段代碼就存在競態(tài)條件售碳,其中return ++count就是臨界區(qū)强重。
*/
public class Obj
{
private int count;
public int incr()
{
return ++count;
}
}
2.2 死鎖
死鎖:指兩個或兩個以上的進(jìn)程(或線程)在執(zhí)行過程中,因爭奪資源而造成的一種互相等待的現(xiàn)象贸人,若無外力作用间景,它們都將無法推進(jìn)下去。此時稱系統(tǒng)處于死鎖狀態(tài)或系統(tǒng)產(chǎn)生了死鎖艺智,這些永遠(yuǎn)在互相等待的進(jìn)程稱為死鎖進(jìn)程倘要。
關(guān)于死鎖發(fā)生的條件:
- 互斥條件:線程對資源的訪問是排他性的,如果一個線程對占用了某資源十拣,那么其他線程必須處于等待狀態(tài)封拧,直到資源被釋放。
- 請求和保持條件:線程T1至少已經(jīng)保持了一個資源R1占用夭问,但又提出對另一個資源R2請求泽西,而此時,資源R2被其他線程T2占用缰趋,于是該線程T1也必須等待捧杉,但又對自己保持的資源R1不釋放。
- 不剝奪條件:線程已獲得的資源秘血,在未使用完之前味抖,不能被其他線程剝奪,只能在使用完以后由自己釋放灰粮。
- 環(huán)路等待條件:在死鎖發(fā)生時仔涩,必然存在一個“進(jìn)程-資源環(huán)形鏈”,即:
{p0,p1,p2,...pn}
粘舟,進(jìn)程p0(或線程)等待p1占用的資源熔脂,p1等待p2占用的資源佩研,pn等待p0占用的資源。(最直觀的理解是锤悄,p0等待p1占用的資源韧骗,而p1而在等待p0占用的資源,于是兩個進(jìn)程就相互等待)零聚。
2.3 活鎖
活鎖:是指線程1可以使用資源袍暴,但它很禮貌,讓其他線程先使用資源隶症,線程2也可以使用資源政模,但它很紳士,也讓其他線程先使用資源蚂会。這樣你讓我淋样,我讓你,最后兩個線程都無法使用資源胁住。
關(guān)于“死鎖與活鎖”的比喻:
死鎖:迎面開來的汽車A和汽車B過馬路趁猴,汽車A得到了半條路的資源(滿足死鎖發(fā)生條件1:資源訪問是排他性的,我占了路你就不能上來彪见,除非你爬我頭上去)儡司,汽車B占了汽車A的另外半條路的資源,A想過去必須請求另一半被B占用的道路(死鎖發(fā)生條件2:必須整條車身的空間才能開過去余指,我已經(jīng)占了一半捕犬,尼瑪另一半的路被B占用了),B若想過去也必須等待A讓路酵镜,A是輛蘭博基尼碉碉,B是開奇瑞QQ的屌絲,A素質(zhì)比較低開窗對B狂罵:快給老子讓開淮韭,B很生氣垢粮,你媽逼的,老子就不讓(死鎖發(fā)生條件3:在未使用完資源前靠粪,不能被其他線程剝奪)足丢,于是兩者相互僵持一個都走不了(死鎖發(fā)生條件4:環(huán)路等待條件),而且導(dǎo)致整條道上的后續(xù)車輛也走不了庇配。
活鎖:馬路中間有條小橋,只能容納一輛車經(jīng)過绍些,橋兩頭開來兩輛車A和B捞慌,A比較禮貌,示意B先過柬批,B也比較禮貌啸澡,示意A先過袖订,結(jié)果兩人一直謙讓誰也過不去。
2.4 饑餓
饑餓:是指如果線程T1占用了資源R嗅虏,線程T2又請求封鎖R洛姑,于是T2等待。T3也請求資源R皮服,當(dāng)T1釋放了R上的封鎖后楞艾,系統(tǒng)首先批準(zhǔn)了T3的請求,T2仍然等待龄广。然后T4又請求封鎖R硫眯,當(dāng)T3釋放了R上的封鎖之后,系統(tǒng)又批準(zhǔn)了T4的請求......择同,T2可能永遠(yuǎn)等待两入。
也就是,如果一個線程因為CPU時間全部被其他線程搶走而得不到CPU運行時間敲才,這種狀態(tài)被稱之為“饑餓”裹纳。而該線程被“饑餓致死”正是因為它得不到CPU運行時間的機(jī)會。
關(guān)于“饑餓”的比喻:
在“首堵”北京的某一天紧武,天氣陰沉剃氧,空氣中充斥著霧霾和地溝油的味道,某個苦逼的臨時工交警正在處理塞車脏里,有兩條道A和B上都堵滿了車輛她我,其中A道堵的時間最長,B相對堵的時間較短迫横,這時番舆,前面道路已疏通,交警按照最佳分配原則矾踱,示意B道上車輛先過恨狈,B道路上過了一輛又一輛,A道上排隊時間最長的卻沒法通過呛讲,只能等B道上沒有車輛通過的時候再等交警發(fā)指令讓A道依次通過禾怠,這也就是ReentrantLock顯示鎖里提供的不公平鎖機(jī)制(當(dāng)然了,ReentrantLock也提供了公平鎖的機(jī)制贝搁,由用戶根據(jù)具體的使用場景而決定到底使用哪種鎖策略)吗氏,不公平鎖能夠提高吞吐量但不可避免的會造成某些線程的饑餓。
在Java中雷逆,下面三個常見的原因會導(dǎo)致線程饑餓弦讽,如下:
-
高優(yōu)先級線程吞噬所有的低優(yōu)先級線程的CPU時間
你能為每個線程設(shè)置獨自的線程優(yōu)先級,優(yōu)先級越高的線程獲得的CPU時間越多,線程優(yōu)先級值設(shè)置在1到10之間往产,而這些優(yōu)先級值所表示行為的準(zhǔn)確解釋則依賴于你的應(yīng)用運行平臺被碗。對大多數(shù)應(yīng)用來說,你最好是不要改變其優(yōu)先級值仿村。
-
線程被永久堵塞在一個等待進(jìn)入同步塊的狀態(tài)锐朴,因為其他線程總是能在它之前持續(xù)地對該同步塊進(jìn)行訪問
Java的同步代碼區(qū)也是一個導(dǎo)致饑餓的因素。Java的同步代碼區(qū)對哪個線程允許進(jìn)入的次序沒有任何保障蔼囊。這就意味著理論上存在一個試圖進(jìn)入該同步區(qū)的線程處于被永久堵塞的風(fēng)險焚志,因為其他線程總是能持續(xù)地先于它獲得訪問,這即是“饑餓”問題压真,而一個線程被“饑餓致死”正是因為它得不到CPU運行時間的機(jī)會娩嚼。
-
線程在等待一個本身(在其上調(diào)用wait())也處于永久等待完成的對象,因為其他線程總是被持續(xù)地獲得喚醒
如果多個線程處在wait()方法執(zhí)行上滴肿,而對其調(diào)用notify()不會保證哪一個線程會獲得喚醒岳悟,任何線程都有可能處于繼續(xù)等待的狀態(tài)。因此存在這樣一個風(fēng)險:一個等待線程從來得不到喚醒泼差,因為其他等待線程總是能被獲得喚醒贵少。
2.5 公平
解決饑餓的方案被稱之為“公平性” – 即所有線程均能公平地獲得運行機(jī)會。在Java中實現(xiàn)公平性方案堆缘,需要:
- 使用鎖滔灶,而不是同步塊;
- 使用公平鎖吼肥;
- 注意性能方面录平;
在Java中實現(xiàn)公平性,雖Java不可能實現(xiàn)100%的公平性缀皱,依然可以通過同步結(jié)構(gòu)在線程間實現(xiàn)公平性的提高斗这。
首先來學(xué)習(xí)一段簡單的同步態(tài)代碼:
public class Synchronizer{
public synchronized void doSynchronized () {
// do a lot of work which takes a long time
}
}
如果有多個線程調(diào)用doSynchronized()方法,在第一個獲得訪問的線程未完成前啤斗,其他線程將一直處于阻塞狀態(tài)表箭,而且在這種多線程被阻塞的場景下,接下來將是哪個線程獲得訪問是沒有保障的钮莲。
改為 使用鎖方式替代同步塊免钻,為了提高等待線程的公平性,我們使用鎖方式來替代同步塊:
public class Synchronizer{
Lock lock = new Lock();
public void doSynchronized() throws InterruptedException{
this.lock.lock();
//critical section, do a lot of work which takes a long time
this.lock.unlock();
}
}
注意到doSynchronized()不再聲明為synchronized崔拥,而是用lock.lock()和lock.unlock()來替代极舔。下面是用Lock類做的一個實現(xiàn):
public class Lock{
private boolean isLocked = false;
private Thread lockingThread = null;
public synchronized void lock() throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
lockingThread = Thread.currentThread();
}
public synchronized void unlock(){
if(this.lockingThread != Thread.currentThread()){
throw new IllegalMonitorStateException("Calling thread has not locked this lock");
}
isLocked = false;
lockingThread = null;
notify();
}
}
注意到上面對Lock的實現(xiàn),如果存在多線程并發(fā)訪問lock()链瓦,這些線程將阻塞在對lock()方法的訪問上姆怪。另外,如果鎖已經(jīng)鎖上(校對注:這里指的是isLocked等于true時),這些線程將阻塞在while(isLocked)循環(huán)的wait()調(diào)用里面稽揭。要記住的是,當(dāng)線程正在等待進(jìn)入lock() 時肥卡,可以調(diào)用wait()釋放其鎖實例對應(yīng)的同步鎖溪掀,使得其他多個線程可以進(jìn)入lock()方法,并調(diào)用wait()方法步鉴。
這回看下doSynchronized()揪胃,你會注意到在lock()和unlock()之間的注釋:在這兩個調(diào)用之間的代碼將運行很長一段時間。進(jìn)一步設(shè)想氛琢,這段代碼將長時間運行喊递,和進(jìn)入lock()并調(diào)用wait()來比較的話。這意味著大部分時間用在等待進(jìn)入鎖和進(jìn)入臨界區(qū)的過程是用在wait()的等待中阳似,而不是被阻塞在試圖進(jìn)入lock()方法中骚勘。
在早些時候提到過,同步塊不會對等待進(jìn)入的多個線程誰能獲得訪問做任何保障撮奏,同樣當(dāng)調(diào)用notify()時俏讹,wait()也不會做保障一定能喚醒線程。因此這個版本的Lock類和doSynchronized()那個版本就保障公平性而言畜吊,沒有任何區(qū)別泽疆。
但我們能夠改變這種情況,如下:
當(dāng)前的Lock類版本調(diào)用自己的wait()方法玲献,如果每個線程在不同的對象上調(diào)用wait()殉疼,那么只有一個線程會在該對象上調(diào)用wait(),Lock類可以決定哪個對象能對其調(diào)用notify()捌年,因此能做到有效的選擇喚醒哪個線程瓢娜。
下面將上面Lock類轉(zhuǎn)變?yōu)楣芥iFairLock。你會注意到新的實現(xiàn)和之前的Lock類中的同步和wait()/notify()稍有不同延窜。重點是恋腕,每一個調(diào)用lock()的線程都會進(jìn)入一個隊列,當(dāng)解鎖時逆瑞,只有隊列里的第一個線程被允許鎖住FairLock實例荠藤,所有其它的線程都將處于等待狀態(tài),直到他們處于隊列頭部获高。如下:
public class FairLock {
private boolean isLocked = false;
private Thread lockingThread = null;
private List<QueueObject> waitingThreads = new ArrayList<QueueObject>();
public void lock() throws InterruptedException{
// 當(dāng)前線程創(chuàng)建“令牌”
QueueObject queueObject = new QueueObject();
boolean isLockedForThisThread = true;
synchronized(this){
// 所有線程的queueObject令牌哈肖,入隊
waitingThreads.add(queueObject);
}
while(isLockedForThisThread){
synchronized(this){
// 1. 判斷是否已被鎖住:是否已有線程獲得鎖念秧,正在執(zhí)行同步代碼塊
// 2. 判斷頭部令牌與當(dāng)前線程令牌是否一致:也就是只鎖住頭部令牌對應(yīng)的線程淤井;
isLockedForThisThread = isLocked || waitingThreads.get(0) != queueObject;
if(!isLockedForThisThread){
isLocked = true;
// 移除頭部令牌
waitingThreads.remove(queueObject);
lockingThread = Thread.currentThread();
return;
}
}
try{
// 其他線程執(zhí)行doWait(),進(jìn)行等待
queueObject.doWait();
}catch(InterruptedException e){
synchronized(this) { waitingThreads.remove(queueObject); }
throw e;
}
}
}
public synchronized void unlock(){
if(this.lockingThread != Thread.currentThread()){
throw new IllegalMonitorStateException("Calling thread has not locked this lock");
}
isLocked = false;
lockingThread = null;
if(waitingThreads.size() > 0) {
// 喚醒頭部令牌對應(yīng)的線程,可以執(zhí)行
waitingThreads.get(0).doNotify();
}
}
}
public class QueueObject {
private boolean isNotified = false;
public synchronized void doWait() throws InterruptedException {
while(!isNotified){
this.wait();
}
this.isNotified = false;
}
public synchronized void doNotify() {
this.isNotified = true;
this.notify();
}
public boolean equals(Object o) {
return this == o;
}
}
首先注意到lock()方法不在聲明為synchronized币狠,取而代之的是對必需同步的代碼游两,在synchronized中進(jìn)行嵌套。
FairLock新創(chuàng)建了一個QueueObject的實例漩绵,并對每個調(diào)用lock()的線程進(jìn)行入隊操作贱案。調(diào)用unlock()的線程將從隊列頭部獲取QueueObject,并對其調(diào)用doNotify()止吐,以喚醒在該對象上等待的線程宝踪。通過這種方式,在同一時間僅有一個等待線程獲得喚醒碍扔,而不是所有的等待線程瘩燥。這也是實現(xiàn)FairLock公平性的核心所在。
還需注意到不同,QueueObject實際是一個semaphore厉膀。doWait()和doNotify()方法在QueueObject中保存著信號。這樣做以避免一個線程在調(diào)用queueObject.doWait()之前被另一個線程調(diào)用unlock()并隨之調(diào)用queueObject.doNotify()的線程重入套鹅,從而導(dǎo)致信號丟失站蝠。queueObject.doWait()調(diào)用放置在synchronized(this)塊之外,以避免被monitor嵌套鎖死卓鹿,所以另外的線程可以解鎖菱魔,只要當(dāng)沒有線程在lock方法的synchronized(this)塊中執(zhí)行即可。
最后吟孙,注意到queueObject.doWait()在try – catch塊中是怎樣調(diào)用的澜倦。在InterruptedException拋出的情況下,線程得以離開lock()杰妓,并需讓它從隊列中移除藻治。
3 如何確保線程安全特性
3.1 如何確保原子性
3.1.1 鎖和同步
常用的保證Java操作原子性的工具是 鎖和同步方法(或者同步代碼塊)。使用鎖巷挥,可以保證同一時間只有一個線程能拿到鎖桩卵,也就保證了同一時間只有一個線程能執(zhí)行申請鎖和釋放鎖之間的代碼。
public void testLock () {
lock.lock();
try{
int j = i;
i = j + 1;
} finally {
lock.unlock();
}
}
與鎖類似的是同步方法或者同步代碼塊倍宾。使用非靜態(tài)同步方法時雏节,鎖住的是當(dāng)前實例;使用靜態(tài)同步方法時高职,鎖住的是該類的Class對象钩乍;使用靜態(tài)代碼塊時,鎖住的是synchronized關(guān)鍵字后面括號內(nèi)的對象怔锌。下面是同步代碼塊示例:
public void testLock () {
synchronized (anyObject){
int j = i;
i = j + 1;
}
}
無論使用鎖還是synchronized寥粹,本質(zhì)都是一樣变过,通過鎖或同步來實現(xiàn)資源的排它性,從而實際目標(biāo)代碼段同一時間只會被一個線程執(zhí)行涝涤,進(jìn)而保證了目標(biāo)代碼段的原子性媚狰。這是一種以犧牲性能為代價的方法。
3.1.2 CAS(compare and swap)
基礎(chǔ)類型變量自增(i++)是一種常被新手誤以為是原子操作而實際不是的操作阔拳。Java中提供了對應(yīng)的原子操作類來實現(xiàn)該操作哈雏,并保證原子性,其本質(zhì)是利用了CPU級別的CAS指令衫生。由于是CPU級別的指令,其開銷比需要操作系統(tǒng)參與的鎖的開銷小土浸。AtomicInteger使用方法如下:
AtomicInteger atomicInteger = new AtomicInteger();
for(int b = 0; b < numThreads; b++) {
new Thread(() -> {
for(int a = 0; a < iteration; a++) {
atomicInteger.incrementAndGet();
}
}).start();
}
3.2 如何確弊镎耄可見性
Java提供了volatile關(guān)鍵字來保證可見性。當(dāng)使用volatile修飾某個變量時黄伊,它會保證對該變量的修改會立即被更新到內(nèi)存中泪酱,并且將其它線程緩存中對該變量的緩存設(shè)置成無效,因此其它線程需要讀取該值時必須從主內(nèi)存中讀取还最,從而得到最新的值墓阀。
volatile適用場景:volatile適用于不需要保證原子性,但卻需要保證可見性的場景拓轻。一種典型的使用場景是用它修飾用于停止線程的狀態(tài)標(biāo)記斯撮。如下所示:
boolean isRunning = false;
public void start () {
new Thread( () -> {
while(isRunning) {
someOperation();
}
}).start();
}
public void stop () {
isRunning = false;
}
在這種實現(xiàn)方式下,即使其它線程通過調(diào)用stop()方法將isRunning設(shè)置為false扶叉,循環(huán)也不一定會立即結(jié)束勿锅。可以通過volatile關(guān)鍵字,保證while循環(huán)及時得到isRunning最新的狀態(tài)從而及時停止循環(huán)枣氧,結(jié)束線程溢十。
3.3 如何確保有序性
上文講過編譯器和處理器對指令進(jìn)行重新排序時,會保證重新排序后的執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果一致达吞,所以重新排序過程并不會影響單線程程序的執(zhí)行张弛,卻可能影響多線程程序并發(fā)執(zhí)行的正確性。
Java中可通過volatile在一定程序上保證順序性酪劫,另外還可以通過synchronized和鎖來保證順序性吞鸭。
synchronized和鎖保證順序性的原理和保證原子性一樣,都是通過保證同一時間只會有一個線程執(zhí)行目標(biāo)代碼段來實現(xiàn)的契耿。
除了從應(yīng)用層面保證目標(biāo)代碼段執(zhí)行的順序性外瞒大,JVM還通過被稱為happens-before原則隱式地保證順序性。兩個操作的執(zhí)行順序只要可以通過happens-before推導(dǎo)出來搪桂,則JVM會保證其順序性透敌,反之JVM對其順序性不作任何保證盯滚,可對其進(jìn)行任意必要的重新排序以獲取高效率。
happens-before原則(先行發(fā)生原則)酗电,如下:
- 傳遞規(guī)則:如果操作1在操作2前面魄藕,而操作2在操作3前面,則操作1肯定會在操作3前發(fā)生撵术。該規(guī)則說明了happens-before原則具有傳遞性背率。
- 鎖定規(guī)則:一個unlock操作肯定會在后面對同一個鎖的lock操作前發(fā)生。這個很好理解嫩与,鎖只有被釋放了才會被再次獲取寝姿。
- volatile變量規(guī)則:對一個被volatile修飾的寫操作先發(fā)生于后面對該變量的讀操作。
- 程序次序規(guī)則:一個線程內(nèi)划滋,按照代碼順序執(zhí)行饵筑。
- 線程啟動規(guī)則:Thread對象的start()方法先發(fā)生于此線程的其它動作。
- 線程終結(jié)原則:線程的終止檢測后發(fā)生于線程中其它的所有操作处坪。
- 線程中斷規(guī)則: 對線程interrupt()方法的調(diào)用先發(fā)生于對該中斷異常的獲取根资。
- 對象終結(jié)規(guī)則:一個對象構(gòu)造先于它的finalize發(fā)生。
4 關(guān)于線程安全的幾個為什么
-
平時項目中使用鎖和synchronized比較多同窘,而很少使用volatile玄帕,難道就沒有保證可見性?
鎖和synchronized即可以保證原子性想邦,也可以保證可見性裤纹。都是通過保證同一時間只有一個線程執(zhí)行目標(biāo)代碼段來實現(xiàn)的。
-
鎖和synchronized為何能保證可見性案狠?
根據(jù)JDK 7的Java doc中對
concurrent
包的說明服傍,一個線程的寫結(jié)果保證對另外線程的讀操作可見,只要該寫操作可以由happen-before
原則推斷出在讀操作之前發(fā)生骂铁。 -
既然鎖和synchronized即可保證原子性也可保證可見性吹零,為何還需要volatile?
synchronized和鎖需要通過操作系統(tǒng)來仲裁誰獲得鎖拉庵,開銷比較高灿椅,而volatile開銷小很多。因此在只需要保證可見性的條件下钞支,使用volatile的性能要比使用鎖和synchronized高得多茫蛹。
-
既然鎖和synchronized可以保證原子性,為什么還需要AtomicInteger這種的類來保證原子操作烁挟?
鎖和synchronized需要通過操作系統(tǒng)來仲裁誰獲得鎖婴洼,開銷比較高,而AtomicInteger是通過CPU級的CAS操作來保證原子性撼嗓,開銷比較小柬采。所以使用AtomicInteger的目的還是為了提高性能欢唾。
-
還有沒有別的辦法保證線程安全?
有粉捻。盡可能避免引起非線程安全的條件——共享變量礁遣。如果能從設(shè)計上避免共享變量的使用,即可避免非線程安全的發(fā)生肩刃,也就無須通過鎖或者synchronized以及volatile解決原子性祟霍、可見性和順序性的問題。
-
synchronized即可修飾非靜態(tài)方式盈包,也可修飾靜態(tài)方法沸呐,還可修飾代碼塊,有何區(qū)別呢燥?
synchronized修飾非靜態(tài)同步方法時垂谢,鎖住的是當(dāng)前實例;synchronized修飾靜態(tài)同步方法時疮茄,鎖住的是該類的Class對象;synchronized修飾靜態(tài)代碼塊時根暑,鎖住的是synchronized關(guān)鍵字后面括號內(nèi)的對象力试。