鎖機制:synchronized、Lock、Condition

1、synchronized

把代碼塊聲明為?synchronized,有兩個重要后果,通常是指該代碼具有?原子性(atomicity)和?可見性(visibility)

1.1 原子性

原子性意味著個時刻,只有一個線程能夠執行一段代碼,這段代碼通過一個monitor object保護。從而防止多個線程在更新共享狀態時相互沖突。

1.2 可見性

可見性則更為微妙,它要對付內存緩存和編譯器優化的各種反常行為。它必須確保釋放鎖之前對共享數據做出的更改對于隨后獲得該鎖的另一個線程是可見的 。

作用:如果沒有同步機制提供的這種可見性保證,線程看到的共享變量可能是修改前的值或不一致的值,這將引發許多嚴重問題。

原理:當對象獲取鎖時,它首先使自己的高速緩存無效,這樣就可以保證直接從主內存中裝入變量。 同樣,在對象釋放鎖之前,它會刷新其高速緩存,強制使已做的任何更改都出現在主內存中。 這樣,會保證在同一個鎖上同步的兩個線程看到在 synchronized 塊內修改的變量的相同值。

一般來說,線程以某種不必讓其他線程立即可以看到的方式(不管這些線程在寄存器中、在處理器特定的緩存中,還是通過指令重排或者其他編譯器優化),不受緩存變量值的約束,但是如果開發人員使用了同步,那么運行庫將確保某一線程對變量所做的更新先于對現有synchronized?塊所進行的更新,當進入由同一監控器(lock)保護的另一個synchronized塊時,將立刻可以看到這些對變量所做的更新。類似的規則也存在于volatile變量上。

——volatile只保證可見性,不保證原子性!

1.3 何時要同步?

可見性同步的基本規則是在以下情況中必須同步:?

讀取上一次可能是由另一個線程寫入的變量?

寫入下一次可能由另一個線程讀取的變量

一致性同步:當修改多個相關值時,您想要其它線程原子地看到這組更改—— 要么看到全部更改,要么什么也看不到。

這適用于相關數據項(如粒子的位置和速率)和元數據項(如鏈表中包含的數據值和列表自身中的數據項的鏈)。

在某些情況中,您不必用同步來將數據從一個線程傳遞到另一個,因為 JVM 已經隱含地為您執行同步。這些情況包括:

由靜態初始化器(在靜態字段上或 static{} 塊中的初始化器)

初始化數據時?

訪問 final 字段時 ——final對象呢?

在創建線程之前創建對象時?

線程可以看見它將要處理的對象時

1.4 synchronize的限制

synchronized是不錯,但它并不完美。它有一些功能性的限制:

它無法中斷一個正在等候獲得鎖的線程;

也無法通過投票得到鎖,如果不想等下去,也就沒法得到鎖;

同步還要求鎖的釋放只能在與獲得鎖所在的堆棧幀相同的堆棧幀中進行,多數情況下,這沒問題(而且與異常處理交互得很好),但是,確實存在一些非塊結構的鎖定更合適的情況。

2、ReentrantLock

java.util.concurrent.lock?中的Lock?框架是鎖定的一個抽象,它允許把鎖定的實現作為 Java 類,而不是作為語言的特性來實現。這就為Lock?的多種實現留下了空間,各種實現可能有不同的調度算法、性能特性或者鎖定語義。

ReentrantLock?類實現了Lock?,它擁有與synchronized?相同的并發性和內存語義,但是添加了類似鎖投票定時鎖等候可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用情況下更佳的性能。(換句話說,當許多線程都想訪問共享資源時,JVM 可以花更少的時候來調度線程,把更多時間用在執行線程上。)

class?Outputter1?{????

private?Lock?lock?=?new?ReentrantLock();//?鎖對象? ??

public?void?output(String?name)?{???????????

lock.lock();//?得到鎖? ??

try?{????

for(int?i?=?0;?i?<?name.length();?i++)?{????

????????????????System.out.print(name.charAt(i));????

????????????}????

}finally?{????

lock.unlock();//?釋放鎖????

????????}????

????}????

}????

區別:

需要注意的是,用sychronized修飾的方法或者語句塊在代碼執行完之后鎖自動釋放,而是用Lock需要我們手動釋放鎖,所以為了保證鎖最終被釋放(發生異常情況),要把互斥區放在try內,釋放鎖放在finally內!!

3、讀寫鎖ReadWriteLock

上例中展示的是和synchronized相同的功能,那Lock的優勢在哪里?

例如一個類對其內部共享數據data提供了get()和set()方法,如果用synchronized,則代碼如下:

class?syncData?{????????

private?int?data;//?共享數據????????

public?synchronized?void?set(int?data)?{????

System.out.println(Thread.currentThread().getName()?+"準備寫入數據");????

try?{????

Thread.sleep(20);????

}catch?(InterruptedException?e)?{????

????????????e.printStackTrace();????

????????}????

this.data?=?data;????

System.out.println(Thread.currentThread().getName()?+"寫入"?+?this.data);????

????}???????

public?synchronized??void?get()?{????

System.out.println(Thread.currentThread().getName()?+"準備讀取數據");????

try?{????

Thread.sleep(20);????

}catch?(InterruptedException?e)?{????

????????????e.printStackTrace();????

????????}????

System.out.println(Thread.currentThread().getName()?+"讀取"?+?this.data);????

????}????

}????

然后寫個測試類來用多個線程分別讀寫這個共享數據:

public?static?void?main(String[]?args)?{????

//????????final?Data?data?=?new?Data();????

final?syncData?data?=?new?syncData();????

//????????final?RwLockData?data?=?new?RwLockData();????


//寫入??

for?(int?i?=?0;?i?<?3;?i++)?{????

Thread?t?=new?Thread(new?Runnable()?{????

@Override??

public?void?run()?{????

for?(int?j?=?0;?j?<?5;?j++)?{????

data.set(new?Random().nextInt(30));????

????????????????????}????

????????????????}????

????????????});??

t.setName("Thread-W"?+?i);??

????????????t.start();??

????????}????

//讀取??

for?(int?i?=?0;?i?<?3;?i++)?{????

Thread?t?=new?Thread(new?Runnable()?{????

@Override??

public?void?run()?{????

for?(int?j?=?0;?j?<?5;?j++)?{????

????????????????????????data.get();????

????????????????????}????

????????????????}????

????????????});????

t.setName("Thread-R"?+?i);??

????????????t.start();??

????????}????

????}????

運行結果:

Thread-W0準備寫入數據??

Thread-W0寫入0??

Thread-W0準備寫入數據??

Thread-W0寫入1??

Thread-R1準備讀取數據??

Thread-R1讀取1??

Thread-R1準備讀取數據??

Thread-R1讀取1??

Thread-R1準備讀取數據??

Thread-R1讀取1??

Thread-R1準備讀取數據??

Thread-R1讀取1??

Thread-R1準備讀取數據??

Thread-R1讀取1??

Thread-R2準備讀取數據??

Thread-R2讀取1??

Thread-R2準備讀取數據??

Thread-R2讀取1??

Thread-R2準備讀取數據??

Thread-R2讀取1??

Thread-R2準備讀取數據??

Thread-R2讀取1??

Thread-R2準備讀取數據??

Thread-R2讀取1??

Thread-R0準備讀取數據?//R0和R2可以同時讀取,不應該互斥!??

Thread-R0讀取1??

Thread-R0準備讀取數據??

Thread-R0讀取1??

Thread-R0準備讀取數據??

Thread-R0讀取1??

Thread-R0準備讀取數據??

Thread-R0讀取1??

Thread-R0準備讀取數據??

Thread-R0讀取1??

Thread-W1準備寫入數據??

Thread-W1寫入18??

Thread-W1準備寫入數據??

Thread-W1寫入16??

Thread-W1準備寫入數據??

Thread-W1寫入19??

Thread-W1準備寫入數據??

Thread-W1寫入21??

Thread-W1準備寫入數據??

Thread-W1寫入4??

Thread-W2準備寫入數據??

Thread-W2寫入10??

Thread-W2準備寫入數據??

Thread-W2寫入4??

Thread-W2準備寫入數據??

Thread-W2寫入1??

Thread-W2準備寫入數據??

Thread-W2寫入14??

Thread-W2準備寫入數據??

Thread-W2寫入2??

Thread-W0準備寫入數據??

Thread-W0寫入4??

Thread-W0準備寫入數據??

Thread-W0寫入20??

Thread-W0準備寫入數據??

Thread-W0寫入29??

現在一切都看起來很好!各個線程互不干擾!等等。。讀取線程和寫入線程互不干擾是正常的,但是兩個讀取線程是否需要互不干擾??

對!讀取線程不應該互斥!

我們可以用讀寫鎖ReadWriteLock實現:

import java.util.concurrent.locks.ReadWriteLock;

import java.util.concurrent.locks.ReentrantReadWriteLock;

class?Data?{????????

private?int?data;//?共享數據????

private?ReadWriteLock?rwl?=?new?ReentrantReadWriteLock();???????

public?void?set(int?data)?{????

rwl.writeLock().lock();//?取到寫鎖????

try?{????

System.out.println(Thread.currentThread().getName()?+"準備寫入數據");????

try?{????

Thread.sleep(20);????

}catch?(InterruptedException?e)?{????

????????????????e.printStackTrace();????

????????????}????

this.data?=?data;????

System.out.println(Thread.currentThread().getName()?+"寫入"?+?this.data);????

}finally?{????

rwl.writeLock().unlock();//?釋放寫鎖????

????????}????

????}???????


public?void?get()?{????

rwl.readLock().lock();//?取到讀鎖????

try?{????

System.out.println(Thread.currentThread().getName()?+"準備讀取數據");????

try?{????

Thread.sleep(20);????

}catch?(InterruptedException?e)?{????

????????????????e.printStackTrace();????

????????????}????

System.out.println(Thread.currentThread().getName()?+"讀取"?+?this.data);????

}finally?{????

rwl.readLock().unlock();//?釋放讀鎖????

????????}????

????}????

}????

測試結果:

Thread-W1準備寫入數據??

Thread-W1寫入9??

Thread-W1準備寫入數據??

Thread-W1寫入24??

Thread-W1準備寫入數據??

Thread-W1寫入12??

Thread-W0準備寫入數據??

Thread-W0寫入22??

Thread-W0準備寫入數據??

Thread-W0寫入15??

Thread-W0準備寫入數據??

Thread-W0寫入6??

Thread-W0準備寫入數據??

Thread-W0寫入13??

Thread-W0準備寫入數據??

Thread-W0寫入0??

Thread-W2準備寫入數據??

Thread-W2寫入23??

Thread-W2準備寫入數據??

Thread-W2寫入24??

Thread-W2準備寫入數據??

Thread-W2寫入24??

Thread-W2準備寫入數據??

Thread-W2寫入17??

Thread-W2準備寫入數據??

Thread-W2寫入11??

Thread-R2準備讀取數據??

Thread-R1準備讀取數據??

Thread-R0準備讀取數據??

Thread-R0讀取11??

Thread-R1讀取11??

Thread-R2讀取11??

Thread-W1準備寫入數據??

Thread-W1寫入18??

Thread-W1準備寫入數據??

Thread-W1寫入1??

Thread-R0準備讀取數據??

Thread-R2準備讀取數據??

Thread-R1準備讀取數據??

Thread-R2讀取1??

Thread-R2準備讀取數據??

Thread-R1讀取1??

Thread-R0讀取1??

Thread-R1準備讀取數據??

Thread-R0準備讀取數據??

Thread-R0讀取1??

Thread-R2讀取1??

Thread-R2準備讀取數據??

Thread-R1讀取1??

Thread-R0準備讀取數據??

Thread-R1準備讀取數據??

Thread-R0讀取1??

Thread-R2讀取1??

Thread-R1讀取1??

Thread-R0準備讀取數據??

Thread-R1準備讀取數據??

Thread-R2準備讀取數據??

Thread-R1讀取1??

Thread-R2讀取1??

Thread-R0讀取1??

與互斥鎖定相比,讀-寫鎖定允許對共享數據進行更高級別的并發訪問。雖然一次只有一個線程(writer?線程)可以修改共享數據,但在許多情況下,任何數量的線程可以同時讀取共享數據(reader?線程)

從理論上講,與互斥鎖定相比,使用讀-寫鎖定所允許的并發性增強將帶來更大的性能提高。

在實踐中,只有在多處理器上并且只在訪問模式適用于共享數據時,才能完全實現并發性增強。——例如,某個最初用數據填充并且之后不經常對其進行修改的 collection,因為經常對其進行搜索(比如搜索某種目錄),所以這樣的 collection 是使用讀-寫鎖定的理想候選者。

4、線程間通信Condition

Condition可以替代傳統的線程間通信,用await()替換wait(),用signal()替換notify(),用signalAll()替換notifyAll()。

——為什么方法名不直接叫wait()/notify()/nofityAll()?因為Object的這幾個方法是final的,不可重寫!

傳統線程的通信方式,Condition都可以實現。

注意,Condition是被綁定到Lock上的,要創建一個Lock的Condition必須用newCondition()方法。

Condition的強大之處在于它可以為多個線程間建立不同的Condition

看JDK文檔中的一個例子:假定有一個綁定的緩沖區,它支持?put?和?take?方法。如果試圖在空的緩沖區上執行?take操作,則在某一個項變得可用之前,線程將一直阻塞;如果試圖在滿的緩沖區上執行?put?操作,則在有空間變得可用之前,線程將一直阻塞。我們喜歡在單獨的等待 set 中保存put?線程和take?線程,這樣就可以在緩沖區中的項或空間變得可用時利用最佳規劃,一次只通知一個線程。可以使用兩個Condition?實例來做到這一點。

——其實就是java.util.concurrent.ArrayBlockingQueue的功能

class?BoundedBuffer?{??

final?Lock?lock?=?new?ReentrantLock();??????????//鎖對象??

final?Condition?notFull??=?lock.newCondition();?//寫線程鎖??

final?Condition?notEmpty?=?lock.newCondition();?//讀線程鎖??


final?Object[]?items?=?new?Object[100];//緩存隊列??

int?putptr;??//寫索引??

int?takeptr;?//讀索引??

int?count;???//隊列中數據數目??


//寫??

public?void?put(Object?x)?throws?InterruptedException?{??

lock.lock();//鎖定??

try?{??

//?如果隊列滿,則阻塞<寫線程>??

while?(count?==?items.length)?{??

????????notFull.await();???

??????}??

//?寫入隊列,并更新寫索引??

??????items[putptr]?=?x;???

if?(++putptr?==?items.length)?putptr?=?0;???

??????++count;??


//?喚醒<讀線程>??

??????notEmpty.signal();???

}finally?{???

lock.unlock();//解除鎖定???

????}???

??}??


//讀???

public?Object?take()?throws?InterruptedException?{???

lock.lock();//鎖定???

try?{??

//?如果隊列空,則阻塞<讀線程>??

while?(count?==?0)?{??

?????????notEmpty.await();??

??????}??


//讀取隊列,并更新讀索引??

??????Object?x?=?items[takeptr];???

if?(++takeptr?==?items.length)?takeptr?=?0;??

??????--count;??


//?喚醒<寫線程>??

??????notFull.signal();???

return?x;???

}finally?{???

lock.unlock();//解除鎖定???

????}???

??}???

優點:

假設緩存隊列中已經存滿,那么阻塞的肯定是寫線程,喚醒的肯定是讀線程,相反,阻塞的肯定是讀線程,喚醒的肯定是寫線程。

那么假設只有一個Condition會有什么效果呢?緩存隊列中已經存滿,這個Lock不知道喚醒的是讀線程還是寫線程了,如果喚醒的是讀線程,皆大歡喜,如果喚醒的是寫線程,那么線程剛被喚醒,又被阻塞了,這時又去喚醒,這樣就浪費了很多時間。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,283評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,947評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,094評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,485評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,268評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,817評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,906評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,039評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,551評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,502評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,662評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,188評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,907評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,304評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,563評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,255評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,637評論 2 370

推薦閱讀更多精彩內容