? ? ? ? ?最近自己在整理關(guān)于并發(fā)編程相關(guān)的知識(shí)點(diǎn)翅溺,要細(xì)致的了解每個(gè)知識(shí)背后產(chǎn)生的原因和相關(guān)處理并發(fā)的底層原理智什,確實(shí)還是比較需要時(shí)間好好消化的,現(xiàn)在對(duì)于一般的知識(shí)點(diǎn)一般我會(huì)幾個(gè)方面著手去了解冰悠,從幾個(gè)方面看操灿,分別是:原理搀庶、使用場景、優(yōu)缺點(diǎn)铜异、產(chǎn)生的原因哥倔、代碼接入實(shí)例,廢話少說想在就帶大家去了解java并發(fā)相關(guān)的知識(shí)揍庄,本人計(jì)劃使用三篇文章把相關(guān)目錄的知識(shí)點(diǎn)匯總出來咆蒿。
并發(fā)編程相關(guān)知識(shí)點(diǎn)匯總:
一、為什么出現(xiàn)多線程并發(fā)問題
1.1蚂子、Java內(nèi)存模型介紹
1.2沃测、java對(duì)象的組成(對(duì)象頭,實(shí)例數(shù)據(jù)食茎,填充區(qū)域)
二蒂破、線程安全常見的關(guān)鍵字使用詳解
2.1、synchronized
2.2别渔、lock
2.3附迷、Atomic
2.4、volatile
2.5钠糊、Threadlocal詳解
三挟秤、Java并發(fā)編程中常用的類和集合
四、線程間的協(xié)作(wait/notify/sleep/yield/join)
五抄伍、線程安全的級(jí)別
六、并發(fā)編程常見問題匯總
七:擴(kuò)展閱讀
一管宵、為什么出現(xiàn)多線程并發(fā)問題
???并發(fā)編程是Java程序員最重要的技能之一截珍,也是最難掌握的一種技能。它要求編程者對(duì)計(jì)算機(jī)最底層的運(yùn)作原理有深刻的理解箩朴,同時(shí)要求編程者邏輯清晰岗喉、思維縝密,這樣才能寫出高效炸庞、安全钱床、可靠的多線程并發(fā)程序。
圖解:
一、共享性
數(shù)據(jù)共享性是線程安全的主要原因之一。如果所有的數(shù)據(jù)只是在線程內(nèi)有效述暂,那就不存在線程安全性問題狱窘,這也是我們?cè)诰幊痰臅r(shí)候經(jīng)常不需要考慮線程安全的主要原因之一。但是胰坟,在多線程編程中,數(shù)據(jù)共享是不可避免的。最典型的場景是數(shù)據(jù)庫中的數(shù)據(jù)胁孙,為了保證數(shù)據(jù)的一致性,我們通常需要共享同一個(gè)數(shù)據(jù)庫中數(shù)據(jù),即使是在主從的情況下涮较,訪問的也同一份數(shù)據(jù)稠鼻,主從只是為了訪問的效率和數(shù)據(jù)安全,而對(duì)同一份數(shù)據(jù)做的副本狂票。
二:互斥性
資源互斥是指同時(shí)只允許一個(gè)訪問者對(duì)其進(jìn)行訪問候齿,具有唯一性和排它性。我們通常允許多個(gè)線程同時(shí)對(duì)數(shù)據(jù)進(jìn)行讀操作苫亦,但同一時(shí)間內(nèi)只允許一個(gè)線程對(duì)數(shù)據(jù)進(jìn)行寫操作毛肋。所以我們通常將鎖分為共享鎖和排它鎖,也叫做讀鎖和寫鎖屋剑。如果資源不具有互斥性润匙,即使是共享資源,我們也不需要擔(dān)心線程安全唉匾。例如孕讳,對(duì)于不可變的數(shù)據(jù)共享,所有線程都只能對(duì)其進(jìn)行讀操作巍膘,所以不用考慮線程安全問題厂财。但是對(duì)共享數(shù)據(jù)的寫操作,一般就需要保證互斥性峡懈,上述例子中就是因?yàn)闆]有保證互斥性才導(dǎo)致數(shù)據(jù)的修改產(chǎn)生問題璃饱。Java中提供多種機(jī)制來保證互斥性,最簡單的方式是使用Synchronized肪康。
三荚恶、原子性
? ? ? ?原子性就是指對(duì)數(shù)據(jù)的操作是一個(gè)獨(dú)立的、不可分割的整體磷支。換句話說谒撼,就是一次操作,是一個(gè)連續(xù)不可中斷的過程雾狈,數(shù)據(jù)不會(huì)執(zhí)行的一半的時(shí)候被其他線程所修改廓潜。保證原子性的最簡單方式是操作系統(tǒng)指令,就是說如果一次操作對(duì)應(yīng)一條操作系統(tǒng)指令善榛,這樣肯定可以能保證原子性辩蛋。但是很多操作不能通過一條指令就完成。例如锭弊,對(duì)long類型的運(yùn)算堪澎,很多系統(tǒng)就需要分成多條指令分別對(duì)高位和低位進(jìn)行操作才能完成。還比如味滞,我們經(jīng)常使用的整數(shù)i++的操作樱蛤,其實(shí)需要分成三個(gè)步驟:(1)讀取整數(shù)i的值钮呀;(2)對(duì)i進(jìn)行加一操作;(3)將結(jié)果寫回內(nèi)存昨凡。這個(gè)過程在多線程下就可能出現(xiàn)如下現(xiàn)象:
? ? ? ? ?這也是代碼段一執(zhí)行的結(jié)果為什么不正確的原因爽醋。對(duì)于這種組合操作,要保證原子性便脊,最常見的方式是加鎖蚂四,如Java中的Synchronized或Lock都可以實(shí)現(xiàn),代碼段二就是通過Synchronized實(shí)現(xiàn)的哪痰。除了鎖以外遂赠,還有一種方式就是CAS(Compare And Swap),即修改數(shù)據(jù)之前先比較與之前讀取到的值是否一致晌杰,如果一致跷睦,則進(jìn)行修改,如果不一致則重新執(zhí)行肋演,這也是樂觀鎖的實(shí)現(xiàn)原理抑诸。不過CAS在某些場景下不一定有效,比如另一線程先修改了某個(gè)值爹殊,然后再改回原來值蜕乡,這種情況下,CAS是無法判斷的梗夸。
四层玲、可見性
???要理解可見性,需要先對(duì)JVM的內(nèi)存模型有一定的了解反症,JVM的內(nèi)存模型與操作系統(tǒng)類似称簿,如圖所示:
???從這個(gè)圖中我們可以看出,每個(gè)線程都有一個(gè)自己的工作內(nèi)存(相當(dāng)于CPU高級(jí)緩沖區(qū)惰帽,這么做的目的還是在于進(jìn)一步縮小存儲(chǔ)系統(tǒng)與CPU之間速度的差異,提高性能)父虑,對(duì)于共享變量该酗,線程每次讀取的是工作內(nèi)存中共享變量的副本,寫入的時(shí)候也直接修改工作內(nèi)存中副本的值士嚎,然后在某個(gè)時(shí)間點(diǎn)上再將工作內(nèi)存與主內(nèi)存中的值進(jìn)行同步呜魄。這樣導(dǎo)致的問題是,如果線程1對(duì)某個(gè)變量進(jìn)行了修改莱衩,線程2卻有可能看不到線程1對(duì)共享變量所做的修改爵嗅。
五、有序性
? ? ? ? ?為了提高性能笨蚁,編譯器和處理器可能會(huì)對(duì)指令做重排序睹晒。重排序可以分為三種:
1趟庄、編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下伪很,可以重新安排語句的執(zhí)行順序戚啥。
2、指令級(jí)并行的重排序★笔裕現(xiàn)代處理器采用了指令級(jí)并行技術(shù)(Instruction-Level Parallelism猫十,ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性呆盖,處理器可以改變語句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序拖云。
3、內(nèi)存系統(tǒng)的重排序应又。由于處理器使用緩存和讀/寫緩沖區(qū)宙项,這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。
???先看上圖中的(1)源碼部分丁频,從源碼來看杉允,要么指令1先執(zhí)行要么指令3先執(zhí)行。如果指令1先執(zhí)行席里,r2不應(yīng)該能看到指令4中寫入的值叔磷。如果指令3先執(zhí)行,r1不應(yīng)該能看到指令2寫的值奖磁。但是運(yùn)行結(jié)果卻可能出現(xiàn)r2==2改基,r1==1的情況,這就是“重排序”導(dǎo)致的結(jié)果咖为。上圖(2)即是一種可能出現(xiàn)的合法的編譯結(jié)果秕狰,編譯后,指令1和指令2的順序可能就互換了躁染。因此鸣哀,才會(huì)出現(xiàn)r2==2,r1==1的結(jié)果吞彤。Java中也可通過Synchronized或Volatile來保證順序性我衬。
備注:具體深入了解參考本人另外一篇博文(Java內(nèi)存模型詳解)
java對(duì)象的組成
Java對(duì)象保存在內(nèi)存中時(shí),由以下三部分組成:
1饰恕、對(duì)象頭
2挠羔、實(shí)例數(shù)據(jù)
3、對(duì)齊填充字節(jié)
1.1埋嵌、對(duì)象頭
對(duì)象頭也由以下三部分組成:
1破加、Mark Word
2、指向類的指針
3雹嗦、數(shù)組長度(只有數(shù)組對(duì)象才有)
1范舀、Mark Word
???Mark Word記錄了對(duì)象和鎖有關(guān)的信息合是,當(dāng)這個(gè)對(duì)象被synchronized關(guān)鍵字當(dāng)成同步鎖時(shí),圍繞這個(gè)鎖的一系列操作都和Mark Word有關(guān)尿背。
???Mark Word在32位JVM中的長度是32bit端仰,在64位JVM中長度是64bit。
2田藐、指向類的指針該指針在32位JVM中的長度是32bit荔烧,在64位JVM中長度是64bit。
????Java對(duì)象的類數(shù)據(jù)保存在方法區(qū)汽久。?
3鹤竭、數(shù)組長度只有數(shù)組對(duì)象保存了這部分?jǐn)?shù)據(jù)。該數(shù)據(jù)在32位和64位JVM中長度都是32bit景醇。?
Mark Word在不同的鎖狀態(tài)下存儲(chǔ)的內(nèi)容不同臀稚,在32位JVM中是這么存的:
? ? ? ? 其中無鎖和偏向鎖的鎖標(biāo)志位都是01,只是在前面的1bit區(qū)分了這是無鎖狀態(tài)還是偏向鎖狀態(tài)三痰。
? ? ? ? JDK1.6以后的版本在處理同步鎖時(shí)存在鎖升級(jí)的概念吧寺,JVM對(duì)于同步鎖的處理是從偏向鎖開始的,隨著競爭越來越激烈散劫,處理方式從偏向鎖升級(jí)到輕量級(jí)鎖稚机,最終升級(jí)到重量級(jí)鎖。?
JVM一般是這樣使用鎖和Mark Word的:
1获搏,當(dāng)沒有被當(dāng)成鎖時(shí)赖条,這就是一個(gè)普通的對(duì)象,Mark Word記錄對(duì)象的HashCode常熙,鎖標(biāo)志位是01纬乍,是否偏向鎖那一位是0。
2裸卫,當(dāng)對(duì)象被當(dāng)做同步鎖并有一個(gè)線程A搶到了鎖時(shí)仿贬,鎖標(biāo)志位還是01,但是否偏向鎖那一位改成1墓贿,前23bit記錄搶到鎖的線程id诅蝶,表示進(jìn)入偏向鎖狀態(tài)。
3募壕,當(dāng)線程A再次試圖來獲得鎖時(shí),JVM發(fā)現(xiàn)同步鎖對(duì)象的標(biāo)志位是01语盈,是否偏向鎖是1舱馅,也就是偏向狀態(tài),Mark Word中記錄的線程id就是線程A自己的id刀荒,表示線程A已經(jīng)獲得了這個(gè)偏向鎖代嗤,可以執(zhí)行同步鎖的代碼棘钞。
4,當(dāng)線程B試圖獲得這個(gè)鎖時(shí)干毅,JVM發(fā)現(xiàn)同步鎖處于偏向狀態(tài)宜猜,但是Mark Word中的線程id記錄的不是B,那么線程B會(huì)先用CAS操作試圖獲得鎖硝逢,這里的獲得鎖操作是有可能成功的姨拥,因?yàn)榫€程A一般不會(huì)自動(dòng)釋放偏向鎖。如果搶鎖成功渠鸽,就把Mark Word里的線程id改為線程B的id叫乌,代表線程B獲得了這個(gè)偏向鎖,可以執(zhí)行同步鎖代碼徽缚。如果搶鎖失敗憨奸,則繼續(xù)執(zhí)行步驟5。
5凿试,偏向鎖狀態(tài)搶鎖失敗排宰,代表當(dāng)前鎖有一定的競爭,偏向鎖將升級(jí)為輕量級(jí)鎖那婉。JVM會(huì)在當(dāng)前線程的線程棧中開辟一塊單獨(dú)的空間板甘,里面保存指向?qū)ο箧iMark Word的指針,同時(shí)在對(duì)象鎖Mark Word中保存指向這片空間的指針吧恃。上述兩個(gè)保存操作都是CAS操作虾啦,如果保存成功,代表線程搶到了同步鎖痕寓,就把Mark Word中的鎖標(biāo)志位改成00傲醉,可以執(zhí)行同步鎖代碼。如果保存失敗呻率,表示搶鎖失敗硬毕,競爭太激烈,繼續(xù)執(zhí)行步驟6礼仗。
6吐咳,輕量級(jí)鎖搶鎖失敗,JVM會(huì)使用自旋鎖元践,自旋鎖不是一個(gè)鎖狀態(tài)韭脊,只是代表不斷的重試,嘗試搶鎖单旁。從JDK1.7開始沪羔,自旋鎖默認(rèn)啟用,自旋次數(shù)由JVM決定象浑。如果搶鎖成功則執(zhí)行同步鎖代碼蔫饰,如果失敗則繼續(xù)執(zhí)行步驟7琅豆。
7,自旋鎖重試之后如果搶鎖依然失敗篓吁,同步鎖會(huì)升級(jí)至重量級(jí)鎖茫因,鎖標(biāo)志位改為10。在這個(gè)狀態(tài)下杖剪,未搶到鎖的線程都會(huì)被阻塞冻押。
Java對(duì)象剩余的兩個(gè)部分:
1、實(shí)例數(shù)據(jù)對(duì)象的實(shí)例數(shù)據(jù)就是在java代碼中能看到的屬性和他們的值摘盆。?
2翼雀、對(duì)齊填充字節(jié)因?yàn)镴VM要求java的對(duì)象占的內(nèi)存大小應(yīng)該是8bit的倍數(shù),所以后面有幾個(gè)字節(jié)用于把對(duì)象的大小補(bǔ)齊至8bit的倍數(shù)孩擂,沒有特別的功能狼渊。
二、線程安全常見的關(guān)鍵字使用
線程安全涉及的關(guān)鍵字:
1类垦、synchronized
2狈邑、lock
3、Atomic
4蚤认、volatile
5米苹、threadlocal
Synchronized詳解:
基本使用:
? ? ? ? Synchronized是Java中解決并發(fā)問題的一種最常用的方法,也是最簡單的一種方法砰琢。Synchronized的作用主要有三個(gè):
1蘸嘶、確保線程互斥的訪問同步代碼
2、保證共享變量的修改能夠及時(shí)可見
3陪汽、有效解決重排序問題训唱。
? ? ? ?從語法上講,Synchronized總共有三種用法:
1挚冤、修飾普通方法(方法鎖)
2况增、修飾靜態(tài)方法(類鎖)
3、修飾代碼塊(對(duì)象鎖)
修飾普通方法:
packagecom.paddx.test.concurrent;
public classSynchronizedTest{
???public synchronized void method1(){
???????System.out.println("Method 1 start");
???????try {
???????????System.out.println("Method 1 execute");
???????????Thread.sleep(3000);
???????} catch (InterruptedExceptione) {
???????????e.printStackTrace();
???????}
???????System.out.println("Method 1 end");
???}
???public synchronized void method2(){
???????System.out.println("Method 2 start");
???????try {
???????????System.out.println("Method 2 execute");
???????????Thread.sleep(1000);
???????} catch (InterruptedExceptione) {
???????????e.printStackTrace();
???????}
???????System.out.println("Method 2 end");
???}
public static void main(String[]args) {
???????finalSynchronizedTesttest = newSynchronizedTest();
???????new Thread(new Runnable() {
???????????@Override
???????????public void run() {
???????????????test.method1();
???????????}
???????}).start();
???????new Thread(new Runnable() {
???????????@Override
???????????public void run() {
???????????????test.method2();
???????????}
???????}).start();
???}
}
總結(jié):通過在方法聲明中加入synchronized關(guān)鍵字來聲明synchronized方法训挡。synchronized方法鎖控制對(duì)類成員變量的訪問:每個(gè)類實(shí)例對(duì)應(yīng)一把鎖每個(gè)synchronized方法都必須獲得調(diào)用該方法的類實(shí)例的”鎖“方能執(zhí)行澳骤,否則所屬線程阻塞。方法一旦執(zhí)行澜薄,就會(huì)獨(dú)占該鎖为肮,一直到從該方法返回時(shí)才將鎖釋放,此后被阻塞的線程方能獲得該鎖肤京,從而重新進(jìn)入可執(zhí)行狀態(tài)弥锄。這種機(jī)制確保了同一時(shí)刻對(duì)于每一個(gè)類的實(shí)例,其所有聲明為synchronized的成員函數(shù)中至多只有一個(gè)處于可執(zhí)行狀態(tài),從而有效避免了類成員變量的訪問沖突籽暇。
修飾靜態(tài)方法(類)同步:
packagecom.paddx.test.concurrent;
?public classSynchronizedTest{
????public static synchronized void method1(){
????????System.out.println("Method 1 start");
????????try {
????????????System.out.println("Method 1 execute");
????????????Thread.sleep(3000);
????????} catch (InterruptedExceptione) {
????????????e.printStackTrace();
????????}
????????System.out.println("Method 1 end");
????}
????public static synchronized void method2(){
????????System.out.println("Method 2 start");
????????try {
????????????System.out.println("Method 2 execute");
????????????Thread.sleep(1000);
????????} catch (InterruptedExceptione) {
????????????e.printStackTrace();
????????}
????????System.out.println("Method 2 end");
????}
?public static void main(String[]args) {
????????finalSynchronizedTesttest = newSynchronizedTest();
????????finalSynchronizedTesttest2 = newSynchronizedTest();
????????new Thread(new Runnable() {
????????????@Override
????????????public void run() {
????????????????test.method1();
????????????}
????????}).start();
????????new Thread(new Runnable() {
????????????@Override
????????????public void run() {
????????????????test2.method2();
????????????}
????????}).start();
????}
?}
總結(jié):對(duì)靜態(tài)方法的同步本質(zhì)上是對(duì)類的同步(靜態(tài)方法本質(zhì)上是屬于類的方法,而不是對(duì)象上的方法)饭庞,所以即使test和test2屬于不同的對(duì)象戒悠,但是它們都屬于SynchronizedTest類的實(shí)例,所以也只能順序的執(zhí)行method1和method2舟山,不能并發(fā)執(zhí)行绸狐。
? ? ? 對(duì)象鎖是用來控制實(shí)例方法之間的同步,而類鎖是用來控制靜態(tài)方法(或者靜態(tài)變量互斥體)之間的同步的累盗。
修飾代碼塊:
packagecom.paddx.test.concurrent;
public classSynchronizedTest{
???public void method1(){
???????System.out.println("Method 1 start");
???????try {
???????????synchronized (this) {
???????????????System.out.println("Method 1 execute");
???????????????Thread.sleep(3000);
???????????}
???????} catch (InterruptedExceptione) {
???????????e.printStackTrace();
???????}
???????System.out.println("Method 1 end");
???}
???public void method2(){
???????System.out.println("Method 2 start");
???????try {
???????????synchronized (this) {
???????????????System.out.println("Method 2 execute");
???????????????Thread.sleep(1000);
???????????}
???????} catch (InterruptedExceptione) {
???????????e.printStackTrace();
???????}
???????System.out.println("Method 2 end");
???}
public static void main(String[]args) {
???????finalSynchronizedTesttest = newSynchronizedTest();
???????new Thread(new Runnable() {
???????????@Override
???????????public void run() {
???????????????test.method1();
???????????}
???????}).start();
???????new Thread(new Runnable() {
???????????@Override
???????????public void run() {
???????????????test.method2();
???????????}
???????}).start();
???}
}
總結(jié):當(dāng)一個(gè)對(duì)象中有synchronized method或synchronized block的時(shí)候寒矿,調(diào)用此對(duì)象的同步方法或進(jìn)入其同步區(qū)域時(shí),就必須先獲得對(duì)象鎖若债。如果此對(duì)象的對(duì)象鎖已被其他調(diào)用者占用符相,則需要等待此鎖被釋放。(方法鎖也是對(duì)象鎖)java的所有對(duì)象都含有一個(gè)互斥鎖蠢琳,這個(gè)鎖由jvm自動(dòng)獲取和釋放啊终。線程進(jìn)入synchronized方法的時(shí)候獲取該對(duì)象的鎖,當(dāng)然如果已經(jīng)有線程獲取了這個(gè)對(duì)象的鎖傲须,那么當(dāng)前線程會(huì)等待蓝牲;synchronized方法正常返回或者拋異常而終止,jvm會(huì)自動(dòng)釋放對(duì)象鎖泰讽。這里也體現(xiàn)了用synchronized來加鎖的一個(gè)好處例衍,即 :方法拋異常的時(shí)候,鎖仍然可以由jvm來自動(dòng)釋放已卸。
Synchronized原理:
? ? ? ?Synchronized是如何實(shí)現(xiàn)對(duì)代碼塊進(jìn)行同步的佛玄。
monitorenter:
? ? ? ?每個(gè)對(duì)象有一個(gè)監(jiān)視器鎖(monitor)。當(dāng)monitor被占用時(shí)就會(huì)處于鎖定狀態(tài)咬最,線程執(zhí)行monitorenter指令時(shí)嘗試獲取monitor的所有權(quán)翎嫡,過程如下:
1、如果monitor的進(jìn)入數(shù)為0永乌,則該線程進(jìn)入monitor惑申,然后將進(jìn)入數(shù)設(shè)置為1,該線程即為monitor的所有者翅雏。
2圈驼、如果線程已經(jīng)占有該monitor,只是重新進(jìn)入望几,則進(jìn)入monitor的進(jìn)入數(shù)加1.
3.如果其他線程已經(jīng)占用了monitor绩脆,則該線程進(jìn)入阻塞狀態(tài),直到monitor的進(jìn)入數(shù)為0,再重新嘗試獲取monitor的所有權(quán)靴迫。
monitorexit:
1惕味、執(zhí)行monitorexit的線程必須是objectref所對(duì)應(yīng)的monitor的所有者。
2玉锌、指令執(zhí)行時(shí)名挥,monitor的進(jìn)入數(shù)減1,如果減1后進(jìn)入數(shù)為0主守,那線程退出monitor禀倔,不再是這個(gè)monitor的所有者。其他被這個(gè)monitor阻塞的線程可以嘗試去獲取這個(gè)monitor的所有權(quán)参淫。
??通過這兩段描述救湖,我們應(yīng)該能很清楚的看出Synchronized的實(shí)現(xiàn)原理,Synchronized的語義底層是通過一個(gè)monitor的對(duì)象來完成涎才,其實(shí)wait/notify等方法也依賴于monitor對(duì)象鞋既,這就是為什么只有在同步的塊或者方法中才能調(diào)用wait/notify等方法,否則會(huì)拋出java.lang.IllegalMonitorStateException的異常的原因憔维。
Synchronized是如何實(shí)現(xiàn)同步方法的:
???方法的同步并沒有通過指令monitorenter和monitorexit來完成(理論上其實(shí)也可以通過這兩條指令來實(shí)現(xiàn))涛救,不過相對(duì)于普通方法,其常量池中多了ACC_SYNCHRONIZED標(biāo)示符业扒。
???JVM就是根據(jù)該標(biāo)示符來實(shí)現(xiàn)方法的同步的:當(dāng)方法調(diào)用時(shí)检吆,調(diào)用指令將會(huì)檢查方法的ACC_SYNCHRONIZED訪問標(biāo)志是否被設(shè)置,如果設(shè)置了程储,執(zhí)行線程將先獲取monitor蹭沛,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放monitor章鲤。在方法執(zhí)行期間摊灭,其他任何線程都無法再獲得同一個(gè)monitor對(duì)象。 其實(shí)本質(zhì)上沒有區(qū)別败徊,只是方法的同步是一種隱式的方式來實(shí)現(xiàn)帚呼,無需通過字節(jié)碼來完成。
總結(jié):Synchronized是Java并發(fā)編程中最常用的用于保證線程安全的方式皱蹦,其使用相對(duì)也比較簡單煤杀。但是如果能夠深入了解其原理,對(duì)監(jiān)視器鎖等底層知識(shí)有所了解沪哺,一方面可以幫助我們正確的使用Synchronized關(guān)鍵字沈自,另一方面也能夠幫助我們更好的理解并發(fā)編程機(jī)制,有助我們?cè)诓煌那闆r下選擇更優(yōu)的并發(fā)策略來完成任務(wù)辜妓。對(duì)平時(shí)遇到的各種并發(fā)問題枯途,也能夠從容的應(yīng)對(duì)忌怎。
Synchronized底層優(yōu)化:
簡介:Synchronized是通過對(duì)象內(nèi)部的一個(gè)叫做監(jiān)視器鎖(monitor)來實(shí)現(xiàn)的。但是監(jiān)視器鎖本質(zhì)又是依賴于底層的操作系統(tǒng)的MutexLock來實(shí)現(xiàn)的酪夷。而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換這就需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)榴啸,這個(gè)成本非常高,狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長的時(shí)間晚岭,這就是為什么Synchronized效率低的原因插掂。因此,這種依賴于操作系統(tǒng)MutexLock所實(shí)現(xiàn)的鎖我們稱之為“重量級(jí)鎖”腥例。JDK中對(duì)Synchronized做的種種優(yōu)化,其核心都是為了減少這種重量級(jí)鎖的使用酝润。JDK1.6以后燎竖,為了減少獲得鎖和釋放鎖所帶來的性能消耗,提高性能要销,引入了“輕量級(jí)鎖”和“偏向鎖”构回。
二、輕量級(jí)鎖?
鎖的狀態(tài)總共有四種:無鎖狀態(tài)疏咐、偏向鎖纤掸、輕量級(jí)鎖和重量級(jí)鎖。隨著鎖的競爭浑塞,鎖可以從偏向鎖升級(jí)到輕量級(jí)鎖借跪,再升級(jí)的重量級(jí)鎖(但是鎖的升級(jí)是單向的,也就是說只能從低到高升級(jí)酌壕,不會(huì)出現(xiàn)鎖的降級(jí))。JDK 1.6中默認(rèn)是開啟偏向鎖和輕量級(jí)鎖的,我們也可以通過-XX:-UseBiasedLocking來禁用偏向鎖萍肆。鎖的狀態(tài)保存在對(duì)象的頭文件中削罩,以32位的JDK為例:
? ?“輕量級(jí)”是相對(duì)于使用操作系統(tǒng)互斥量來實(shí)現(xiàn)的傳統(tǒng)鎖而言的。但是糊昙,首先需要強(qiáng)調(diào)一點(diǎn)的是辛掠,輕量級(jí)鎖并不是用來代替重量級(jí)鎖的,它的本意是在沒有多線程競爭的前提下释牺,減少傳統(tǒng)的重量級(jí)鎖使用產(chǎn)生的性能消耗萝衩。在解釋輕量級(jí)鎖的執(zhí)行過程之前,先明白一點(diǎn)船侧,輕量級(jí)鎖所適應(yīng)的場景是線程交替執(zhí)行同步塊的情況欠气,如果存在同一時(shí)間訪問同一鎖的情況,就會(huì)導(dǎo)致輕量級(jí)鎖膨脹為重量級(jí)鎖镜撩。
1预柒、輕量級(jí)鎖的加鎖過程
1队塘、在代碼進(jìn)入同步塊的時(shí)候,如果同步對(duì)象鎖狀態(tài)為無鎖狀態(tài)(鎖標(biāo)志位為“01”狀態(tài)宜鸯,是否為偏向鎖為“0”)憔古,虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的Mark Word的拷貝淋袖,官方稱之為Displaced Mark Word鸿市。這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如圖2.1所示。
2即碗、拷貝對(duì)象頭中的Mark Word復(fù)制到鎖記錄中焰情。
3、拷貝成功后剥懒,虛擬機(jī)將使用CAS操作嘗試將對(duì)象的Mark Word更新為指向Lock Record的指針内舟,并將Lock record里的owner指針指向object mark word。如果更新成功初橘,則執(zhí)行步驟(3)验游,否則執(zhí)行步驟(4)。
4保檐、如果這個(gè)更新動(dòng)作成功了耕蝉,那么這個(gè)線程就擁有了該對(duì)象的鎖,并且對(duì)象Mark Word的鎖標(biāo)志位設(shè)置為“00”夜只,即表示此對(duì)象處于輕量級(jí)鎖定狀態(tài)垒在,這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如圖2.2所示。
5盐肃、如果這個(gè)更新操作失敗了爪膊,虛擬機(jī)首先會(huì)檢查對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀,如果是就說明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖砸王,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行推盛。否則說明多個(gè)線程競爭鎖,輕量級(jí)鎖就要膨脹為重量級(jí)鎖谦铃,鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”耘成,Mark Word中存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)驹闰。 而當(dāng)前線程便嘗試使用自旋來獲取鎖瘪菌,自旋就是為了不讓線程阻塞,而采用循環(huán)去獲取鎖的過程嘹朗。
輕量級(jí)鎖CAS操作之前堆棧與對(duì)象的狀態(tài):
輕量級(jí)鎖CAS操作之后堆棧與對(duì)象的狀態(tài):
輕量級(jí)鎖的解鎖過程:
1师妙、通過CAS操作嘗試把線程中復(fù)制的Displaced Mark Word對(duì)象替換當(dāng)前的Mark Word。
2屹培、如果替換成功默穴,整個(gè)同步過程就完成了怔檩。
3、如果替換失敗蓄诽,說明有其他線程嘗試過獲取該鎖(此時(shí)鎖已膨脹)薛训,那就要在釋放鎖的同時(shí),喚醒被掛起的線程仑氛。
三乙埃、偏向鎖
引入偏向鎖是為了在無多線程競爭的情況下盡量減少不必要的輕量級(jí)鎖執(zhí)行路徑,因?yàn)檩p量級(jí)鎖的獲取及釋放依賴多次CAS原子指令锯岖,而偏向鎖只需要在置換ThreadID的時(shí)候依賴一次CAS原子指令(由于一旦出現(xiàn)多線程競爭的情況就必須撤銷偏向鎖介袜,所以偏向鎖的撤銷操作的性能損耗必須小于節(jié)省下來的CAS原子指令的性能消耗)。上面說過出吹,輕量級(jí)鎖是為了在線程交替執(zhí)行同步塊時(shí)提高性能米酬,而偏向鎖則是在只有一個(gè)線程執(zhí)行同步塊時(shí)進(jìn)一步提高性能。
偏向鎖獲取過程:
1趋箩、訪問Mark Word中偏向鎖的標(biāo)識(shí)是否設(shè)置成1,鎖標(biāo)志位是否為01——確認(rèn)為可偏向狀態(tài)加派。
2叫确、如果為可偏向狀態(tài),則測(cè)試線程ID是否指向當(dāng)前線程芍锦,如果是竹勉,進(jìn)入步驟(5),否則進(jìn)入步驟(3)娄琉。
3次乓、如果線程ID并未指向當(dāng)前線程,則通過CAS操作競爭鎖孽水。如果競爭成功票腰,則將Mark Word中線程ID設(shè)置為當(dāng)前線程ID,然后執(zhí)行(5)女气;如果競爭失敗杏慰,執(zhí)行(4)。
4炼鞠、如果CAS獲取偏向鎖失敗缘滥,則表示有競爭。當(dāng)?shù)竭_(dá)全局安全點(diǎn)(safepoint)時(shí)獲得偏向鎖的線程被掛起谒主,偏向鎖升級(jí)為輕量級(jí)鎖朝扼,然后被阻塞在安全點(diǎn)的線程繼續(xù)往下執(zhí)行同步代碼。
5霎肯、執(zhí)行同步代碼擎颖。
偏向鎖的釋放:
偏向鎖的撤銷在上述第四步驟中有提到榛斯。偏向鎖只有遇到其他線程嘗試競爭偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖肠仪,線程不會(huì)主動(dòng)去釋放偏向鎖肖抱。偏向鎖的撤銷,需要等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒有字節(jié)碼正在執(zhí)行)异旧,它會(huì)首先暫停擁有偏向鎖的線程意述,判斷鎖對(duì)象是否處于被鎖定狀態(tài),撤銷偏向鎖后恢復(fù)到未鎖定(標(biāo)志位為“01”)或輕量級(jí)鎖(標(biāo)志位為“00”)的狀態(tài)吮蛹。
重量級(jí)鎖荤崇、輕量級(jí)鎖和偏向鎖之間轉(zhuǎn)換:
四、其他優(yōu)化
一潮针、適應(yīng)性自旋(Adaptive Spinning):從輕量級(jí)鎖獲取的流程中我們知道术荤,當(dāng)線程在獲取輕量級(jí)鎖的過程中執(zhí)行CAS操作失敗時(shí),是要通過自旋來獲取重量級(jí)鎖的每篷。
???問題在于瓣戚,自旋是需要消耗CPU的,如果一直獲取不到鎖的話焦读,那該線程就一直處在自旋狀態(tài)子库,白白浪費(fèi)CPU資源。
???解決這個(gè)問題最簡單的辦法就是指定自旋的次數(shù)矗晃,例如讓其循環(huán)10次仑嗅,如果還沒獲取到鎖就進(jìn)入阻塞狀態(tài)。但是JDK采用了更聰明的方式——適應(yīng)性自旋张症,簡單來說就是線程如果自旋成功了仓技,則下次自旋的次數(shù)會(huì)更多,如果自旋失敗了俗他,則自旋的次數(shù)就會(huì)減少脖捻。
二、鎖粗化(Lock Coarsening):鎖粗化的概念應(yīng)該比較好理解兆衅,就是將多次連接在一起的加鎖郭变、解鎖操作合并為一次,將多個(gè)連續(xù)的鎖擴(kuò)展成一個(gè)范圍更大的鎖涯保。???????
???這里每次調(diào)用stringBuffer.append方法都需要加鎖和解鎖诉濒,如果虛擬機(jī)檢測(cè)到有一系列連串的對(duì)同一個(gè)對(duì)象加鎖和解鎖操作,就會(huì)將其合并成一次范圍更大的加鎖和解鎖操作夕春,即在第一次append方法時(shí)進(jìn)行加鎖未荒,最后一次append方法結(jié)束后進(jìn)行解鎖。
三及志、鎖消除(Lock Elimination):鎖消除即刪除不必要的加鎖操作片排。根據(jù)代碼逃逸技術(shù)寨腔,如果判斷到一段代碼中,堆上的數(shù)據(jù)不會(huì)逃逸出當(dāng)前線程率寡,那么可以認(rèn)為這段代碼是線程安全的迫卢,不必要加鎖。 ?
總結(jié):JDk中采用輕量級(jí)鎖和偏向鎖等對(duì)Synchronized的優(yōu)化冶共,但是這兩種鎖也不是完全沒缺點(diǎn)的乾蛤,比如競爭比較激烈的時(shí)候,不但無法提升效率捅僵,反而會(huì)降低效率家卖,因?yàn)槎嗔艘粋€(gè)鎖升級(jí)的過程,這個(gè)時(shí)候就需要通過-XX:-UseBiasedLocking來禁用偏向鎖庙楚。
對(duì)比圖:
Lock詳解:
Lock是一個(gè)接口上荡,它主要由下面這幾個(gè)方法:
public interface Lock {
???void lock();
???voidlockInterruptibly() throwsInterruptedException;
???booleantryLock();
???booleantryLock(long time,TimeUnitunit) throwsInterruptedException;
???void unlock();
???ConditionnewCondition();
}
lock():lock方法可能是平常使用最多的一個(gè)方法,就是用來獲取鎖馒闷。如果鎖被其他線程獲取酪捡,則進(jìn)行等待。
???????如果采用Lock纳账,必須主動(dòng)去釋放鎖沛善,并且在發(fā)生異常時(shí),不會(huì)自動(dòng)釋放鎖塞祈。
???????Locklock= ...;
???????lock.lock();
???????try{
???????//處理任務(wù)
???????}catch(Exception ex){
???????}finally{
???????lock.unlock();??//釋放鎖
???????}
tryLock():方法是有返回值的,它表示用來嘗試獲取鎖帅涂,如果獲取成功议薪,則返回true,如果獲取失斚庇选(即鎖已被其他線程獲人挂椤),則返回false醇锚,也就說這個(gè)方法無論如何都會(huì)立即返回哼御。在拿不到鎖時(shí)不會(huì)一直在那等待。
???????tryLock(long time,TimeUnitunit)方法和tryLock()方法是類似的焊唬,只不過區(qū)別在于這個(gè)方法在拿不到鎖時(shí)會(huì)等待一定的時(shí)間恋昼,在時(shí)間期限之內(nèi)如果還拿不到鎖,就返回false赶促。如果一開始拿到鎖或者在等待期間內(nèi)拿到了鎖液肌,則返回true。
???????Locklock= ...;
???????if(lock.tryLock()) {
???????try{
???????//處理任務(wù)
???????}catch(Exception ex){
???????}finally{
???????lock.unlock();??//釋放鎖
???????}
???????}else {
???????//如果不能獲取鎖鸥滨,則直接做其他事情
???????}
lockInterruptibly() :此方法比較特殊嗦哆,當(dāng)通過這個(gè)方法去獲取鎖時(shí)谤祖,如果線程正在等待獲取鎖,則這個(gè)線程能夠響應(yīng)中斷老速,即中斷線程的等待狀態(tài)粥喜。也就是說,當(dāng)兩個(gè)線程同時(shí)通過lock.lockInterruptibly()想獲取某個(gè)鎖時(shí)橘券,假若此時(shí)線程A獲取到了鎖额湘,而線程B只有在等待,那么對(duì)線程B調(diào)用threadB.interrupt()方法能夠中斷線程B的等待過程约郁。
???由于lockInterruptibly()的聲明中拋出了異常缩挑,所以lock.lockInterruptibly()必須放在try塊中或者在調(diào)用lockInterruptibly()的方法外聲明拋出InterruptedException。
一般形式如下:
public void method() throwsInterruptedException{
???????lock.lockInterruptibly();
???????try {
???????//.....
???????}
???????finally {
???????lock.unlock();
???????}
???????}
???一般來說鬓梅,使用Lock必須在try{}catch{}塊中進(jìn)行供置,并且將釋放鎖的操作放在finally塊中進(jìn)行,以保證鎖一定被釋放绽快,防止死鎖的發(fā)生芥丧。
注意:當(dāng)一個(gè)線程獲取了鎖之后,是不會(huì)被interrupt()方法中斷的坊罢。因?yàn)檎{(diào)用interrupt()方法不能中斷正在運(yùn)行過程中的線程续担,只能中斷阻塞過程中的線程。因此當(dāng)通過lockInterruptibly()方法獲取某個(gè)鎖時(shí)活孩,如果不能獲取到物遇,只有進(jìn)行等待的情況下,是可以響應(yīng)中斷的憾儒。而用synchronized修飾的話询兴,當(dāng)一個(gè)線程處于等待某個(gè)鎖的狀態(tài),是無法被中斷的起趾,只有一直等待下去诗舰。
Java中15種鎖的介紹:
1、公平鎖/非公平鎖
2训裆、可重入鎖/不可重入鎖
3眶根、獨(dú)享鎖/共享鎖
4、互斥鎖/讀寫鎖
5边琉、樂觀鎖/悲觀鎖
6属百、分段鎖
7、偏向鎖/輕量級(jí)鎖/重量級(jí)鎖
8变姨、自旋鎖
???上面是很多鎖的名詞诸老,這些分類并不是全是指鎖的狀態(tài),有的指鎖的特性,有的指鎖的設(shè)計(jì)别伏,下面總結(jié)的內(nèi)容是對(duì)每個(gè)鎖的名詞進(jìn)行一定的解釋蹄衷。
一:公平鎖和非公平鎖
公平鎖:公平鎖是指多個(gè)線程按照申請(qǐng)鎖的順序來獲取鎖。
非公平鎖:非公平鎖是指多個(gè)線程獲取鎖的順序并不是按照申請(qǐng)鎖的順序厘肮,有可能后申請(qǐng)的線程比先申請(qǐng)的線程優(yōu)先獲取鎖愧口。有可能,會(huì)造成優(yōu)先級(jí)反轉(zhuǎn)或者饑餓現(xiàn)象类茂。
解析:
??????對(duì)于JavaReentrantLock而言耍属,通過構(gòu)造函數(shù)指定該鎖是否是公平鎖,默認(rèn)是非公平鎖巩检。非公平鎖的優(yōu)點(diǎn)在于吞吐量比公平鎖大厚骗。
??????對(duì)于Synchronized而言,也是一種非公平鎖兢哭。由于其并不像ReentrantLock是通過AQS的來實(shí)現(xiàn)線程調(diào)度领舰,所以并沒有任何辦法使其變成公平鎖。
備注:在AQS中維護(hù)了一個(gè)private volatileintstate來計(jì)數(shù)重入次數(shù)迟螺,避免了頻繁的持有釋放操作冲秽,這樣既提升了效率,又避免了死鎖矩父。
二:可重入鎖和不可重入鎖
可重入鎖:廣義上的可重入鎖指的是可重復(fù)可遞歸調(diào)用的鎖锉桑,在外層使用鎖之后,在內(nèi)層仍然可以使用窍株,并且不發(fā)生死鎖(前提得是同一個(gè)對(duì)象或者class)民轴,這樣的鎖就叫做可重入鎖。ReentrantLock和synchronized都是可重入鎖球订。
例子:
synchronized voidsetA() throws Exception{
???Thread.sleep(1000);
???setB();
}
synchronized voidsetB() throws Exception{
???Thread.sleep(1000);
}
???上面的代碼就是一個(gè)可重入鎖的一個(gè)特點(diǎn)后裸,如果不是可重入鎖的話,setB可能不會(huì)被當(dāng)前線程執(zhí)行辙售,可能造成死鎖。
不可重入鎖:不可重入鎖飞涂,與可重入鎖相反旦部,不可遞歸調(diào)用,遞歸調(diào)用就發(fā)生死鎖较店。
三:獨(dú)享鎖和共享鎖
??獨(dú)享鎖和共享鎖在你去讀C.U.T包下的ReeReentrantLock和ReentrantReadWriteLock你就會(huì)發(fā)現(xiàn)士八,它倆一個(gè)是獨(dú)享一個(gè)是共享鎖。
???獨(dú)享鎖:該鎖每一次只能被一個(gè)線程所持有梁呈。
???共享鎖:該鎖可被多個(gè)線程共有婚度,典型的就是ReentrantReadWriteLock里的讀鎖,它的讀鎖是可以被共享的官卡,但是它的寫鎖確每次只能被獨(dú)占蝗茁。
???另外讀鎖的共享可保證并發(fā)讀是非常高效的醋虏,但是讀寫和寫寫,寫讀都是互斥的哮翘。
???獨(dú)享鎖與共享鎖也是通過AQS來實(shí)現(xiàn)的颈嚼,通過實(shí)現(xiàn)不同的方法,來實(shí)現(xiàn)獨(dú)享或者共享饭寺,對(duì)于Synchronized而言阻课,當(dāng)然是獨(dú)享鎖。
四:互斥鎖和讀寫鎖
互斥鎖:
1艰匙、在訪問共享資源之前對(duì)進(jìn)行加鎖操作限煞,在訪問完成之后進(jìn)行解鎖操作。 加鎖后员凝,任何其他試圖再次加鎖的線程會(huì)被阻塞署驻,直到當(dāng)前進(jìn)程解鎖。
2绊序、如果解鎖時(shí)有一個(gè)以上的線程阻塞硕舆,那么所有該鎖上的線程都被變成就緒狀態(tài), 第一個(gè)變?yōu)榫途w狀態(tài)的線程又執(zhí)行加鎖操作骤公,那么其他的線程又會(huì)進(jìn)入等待抚官。 在這種方式下,只有一個(gè)線程能夠訪問被互斥鎖保護(hù)的資源阶捆。
讀寫鎖:
1凌节、讀寫鎖既是互斥鎖,又是共享鎖洒试,read模式是共享倍奢,write是互斥(排它鎖)的。?
2垒棋、讀寫鎖有三種狀態(tài):讀加鎖狀態(tài)卒煞、寫加鎖狀態(tài)和不加鎖狀態(tài)
3、讀寫鎖在Java中的具體實(shí)現(xiàn)就是ReadWriteLock
???一次只有一個(gè)線程可以占有寫模式的讀寫鎖叼架,但是多個(gè)線程可以同時(shí)占有讀模式的讀寫鎖畔裕。
???只有一個(gè)線程可以占有寫狀態(tài)的鎖,但可以有多個(gè)線程同時(shí)占有讀狀態(tài)鎖乖订,這也是它可以實(shí)現(xiàn)高并發(fā)的原因扮饶。當(dāng)其處于寫狀態(tài)鎖下,任何想要嘗試獲得鎖的線程都會(huì)被阻塞乍构,直到寫狀態(tài)鎖被釋放甜无;如果是處于讀狀態(tài)鎖下,允許其它線程獲得它的讀狀態(tài)鎖,但是不允許獲得它的寫狀態(tài)鎖岂丘,直到所有線程的讀狀態(tài)鎖被釋放陵究;為了避免想要嘗試寫操作的線程一直得不到寫狀態(tài)鎖,當(dāng)讀寫鎖感知到有線程想要獲得寫狀態(tài)鎖時(shí)元潘,便會(huì)阻塞其后所有想要獲得讀狀態(tài)鎖的線程畔乙。所以讀寫鎖非常適合資源的讀操作遠(yuǎn)多于寫操作的情況。
五:樂觀鎖和悲觀鎖
悲觀鎖:總是假設(shè)最壞的情況翩概,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改牲距,所以每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會(huì)阻塞直到它拿到鎖(共享資源每次只給一個(gè)線程使用钥庇,其它線程阻塞牍鞠,用完后再把資源轉(zhuǎn)讓給其它線程)。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫里邊就用到了很多這種鎖機(jī)制评姨,比如行鎖难述,表鎖等,讀鎖吐句,寫鎖等胁后,都是在做操作之前先上鎖。Java中synchronized和ReentrantLock等獨(dú)占鎖就是悲觀鎖思想的實(shí)現(xiàn)嗦枢。
樂觀鎖:總是假設(shè)最好的情況攀芯,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人不會(huì)修改,所以不會(huì)上鎖文虏,但是在更新的時(shí)候會(huì)判斷一下在此期間別人有沒有去更新這個(gè)數(shù)據(jù)侣诺,可以使用版本號(hào)機(jī)制和CAS算法實(shí)現(xiàn)。樂觀鎖適用于多讀的應(yīng)用類型氧秘,這樣可以提高吞吐量年鸳,像數(shù)據(jù)庫提供的類似于write_condition機(jī)制,其實(shí)都是提供的樂觀鎖丸相。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實(shí)現(xiàn)方式CAS實(shí)現(xiàn)的搔确。
六:分段鎖
???分段鎖其實(shí)是一種鎖的設(shè)計(jì),并不是具體的一種鎖灭忠,對(duì)于ConcurrentHashMap而言膳算,其并發(fā)的實(shí)現(xiàn)就是通過分段鎖的形式來實(shí)現(xiàn)高效的并發(fā)操作。
???并發(fā)容器類的加鎖機(jī)制是基于粒度更小的分段鎖更舞,分段鎖也是提升多并發(fā)程序性能的重要手段之一畦幢。
???在并發(fā)程序中坎吻,串行操作是會(huì)降低可伸縮性缆蝉,并且上下文切換也會(huì)減低性能。在鎖上發(fā)生競爭時(shí)將導(dǎo)致這兩種問題,使用獨(dú)占鎖時(shí)保護(hù)受限資源的時(shí)候刊头,基本上是采用串行方式—-每次只能有一個(gè)線程能訪問它黍瞧。所以對(duì)于可伸縮性來說最大的威脅就是獨(dú)占鎖。
我們一般有三種方式降低鎖的競爭程度:
1原杂、減少鎖的持有時(shí)間
2印颤、降低鎖的請(qǐng)求頻率
3、使用帶有協(xié)調(diào)機(jī)制的獨(dú)占鎖穿肄,這些機(jī)制允許更高的并發(fā)性年局。
???在某些情況下我們可以將鎖分解技術(shù)進(jìn)一步擴(kuò)展為一組獨(dú)立對(duì)象上的鎖進(jìn)行分解,這成為分段鎖咸产。
其實(shí)說的簡單一點(diǎn)就是:
???容器里有多把鎖矢否,每一把鎖用于鎖容器其中一部分?jǐn)?shù)據(jù),那么當(dāng)多線程訪問容器里不同數(shù)據(jù)段的數(shù)據(jù)時(shí)脑溢,線程間就不會(huì)存在鎖競爭僵朗,從而可以有效的提高并發(fā)訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術(shù)屑彻,首先將數(shù)據(jù)分成一段一段的存儲(chǔ)验庙,然后給每一段數(shù)據(jù)配一把鎖,當(dāng)一個(gè)線程占用鎖訪問其中一個(gè)段數(shù)據(jù)的時(shí)候社牲,其他段的數(shù)據(jù)也能被其他線程訪問粪薛。
???比如:在ConcurrentHashMap中使用了一個(gè)包含16個(gè)鎖的數(shù)組,每個(gè)鎖保護(hù)所有散列桶的1/16膳沽,其中第N個(gè)散列桶由第(N mod 16)個(gè)鎖來保護(hù)汗菜。假設(shè)使用合理的散列算法使關(guān)鍵字能夠均勻的分部,那么這大約能使對(duì)鎖的請(qǐng)求減少到原來的1/16挑社。也正是這項(xiàng)技術(shù)使得ConcurrentHashMap支持多達(dá)16個(gè)并發(fā)的寫入線程陨界。
七:偏向鎖、輕量級(jí)鎖痛阻、重量級(jí)鎖
鎖的狀態(tài):
1菌瘪、無鎖狀態(tài)
2、偏向鎖狀態(tài)
3阱当、輕量級(jí)鎖狀態(tài)
4俏扩、重量級(jí)鎖狀態(tài)
???鎖的狀態(tài)是通過對(duì)象監(jiān)視器在對(duì)象頭中的字段來表明的。
???四種狀態(tài)會(huì)隨著競爭的情況逐漸升級(jí)弊添,而且是不可逆的過程录淡,即不可降級(jí)。
這四種狀態(tài)都不是Java語言中的鎖油坝,而是Jvm為了提高鎖的獲取與釋放效率而做的優(yōu)化(使用synchronized時(shí))嫉戚。
偏向鎖:偏向鎖是指一段同步代碼一直被一個(gè)線程所訪問刨裆,那么該線程會(huì)自動(dòng)獲取鎖。降低獲取鎖的代價(jià)彬檀。
輕量級(jí):輕量級(jí)鎖是指當(dāng)鎖是偏向鎖的時(shí)候帆啃,被另一個(gè)線程所訪問,偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖窍帝,其他線程會(huì)通過自旋的形式嘗試獲取鎖努潘,不會(huì)阻塞,提高性能坤学。
重量級(jí)鎖:重量級(jí)鎖是指當(dāng)鎖為輕量級(jí)鎖的時(shí)候疯坤,另一個(gè)線程雖然是自旋,但自旋不會(huì)一直持續(xù)下去深浮,當(dāng)自旋一定次數(shù)的時(shí)候贴膘,還沒有獲取到鎖,就會(huì)進(jìn)入阻塞略号,該鎖膨脹為重量級(jí)鎖刑峡。重量級(jí)鎖會(huì)讓其他申請(qǐng)的線程進(jìn)入阻塞,性能降低玄柠。
八:自旋鎖
???我們知道CAS算法是樂觀鎖的一種實(shí)現(xiàn)方式突梦,CAS算法中又涉及到自旋鎖,所以這里給大家講一下什么是自旋鎖羽利。
???簡單回顧一下CAS算法:CAS是英文單詞Compare and Swap(比較并交換)宫患,是一種有名的無鎖算法。無鎖編程这弧,即不使用鎖的情況下實(shí)現(xiàn)多線程之間的變量同步糖荒,也就是在沒有線程被阻塞的情況下實(shí)現(xiàn)變量的同步俯逾,所以也叫非阻塞同步(Non-blocking Synchronization)痛垛。
CAS算法涉及到三個(gè)操作數(shù)
1匈睁、需要讀寫的內(nèi)存值V
2、進(jìn)行比較的值A(chǔ)
3蛋辈、擬寫入的新值B
更新一個(gè)變量的時(shí)候属拾,只有當(dāng)變量的預(yù)期值A(chǔ)和內(nèi)存地址V當(dāng)中的實(shí)際值相同時(shí),才會(huì)將內(nèi)存地址V對(duì)應(yīng)的值修改為B冷溶,否則不會(huì)執(zhí)行任何操作渐白。一般情況下是一個(gè)自旋操作,即不斷的重試逞频。
自旋鎖(spinlock):是指當(dāng)一個(gè)線程在獲取鎖的時(shí)候纯衍,如果鎖已經(jīng)被其它線程獲取,那么該線程將循環(huán)等待苗胀,然后不斷的判斷鎖是否能夠被成功獲取襟诸,直到獲取到鎖才會(huì)退出循環(huán)褒颈。
???它是為實(shí)現(xiàn)保護(hù)共享資源而提出一種鎖機(jī)制。其實(shí)励堡,自旋鎖與互斥鎖比較類似,它們都是為了解決對(duì)某項(xiàng)資源的互斥使用堡掏。無論是互斥鎖应结,還是自旋鎖,在任何時(shí)刻泉唁,最多只能有一個(gè)保持者鹅龄,也就說,在任何時(shí)刻最多只能有一個(gè)執(zhí)行單元獲得鎖亭畜。但是兩者在調(diào)度機(jī)制上略有不同扮休。對(duì)于互斥鎖,如果資源已經(jīng)被占用拴鸵,資源申請(qǐng)者只能進(jìn)入睡眠狀態(tài)玷坠。但是自旋鎖不會(huì)引起調(diào)用者睡眠,如果自旋鎖已經(jīng)被別的執(zhí)行單元保持劲藐,調(diào)用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經(jīng)釋放了鎖八堡,”自旋”一詞就是因此而得名。
Java如何實(shí)現(xiàn)自旋鎖:
例子:
public classSpinLock{
???privateAtomicReference<Thread>cas= newAtomicReference<Thread>();
???public void lock() {
???????Thread current =Thread.currentThread();
???????//利用CAS
???????while (!cas.compareAndSet(null, current)) {
???????????// DO nothing
???????}
???}
???public void unlock() {
???????Thread current =Thread.currentThread();
???????cas.compareAndSet(current, null);
???}
}
解析:Lock()方法利用的CAS聘芜,當(dāng)?shù)谝粋€(gè)線程A獲取鎖的時(shí)候兄渺,能夠成功獲取到,不會(huì)進(jìn)入while循環(huán)汰现,如果此時(shí)線程A沒有釋放鎖挂谍,另一個(gè)線程B又來獲取鎖,此時(shí)由于不滿足CAS瞎饲,所以就會(huì)進(jìn)入while循環(huán)口叙,不斷判斷是否滿足CAS,直到A線程調(diào)用unlock方法釋放了該鎖嗅战。
自旋鎖存在的問題:
1庐扫、如果某個(gè)線程持有鎖的時(shí)間過長,就會(huì)導(dǎo)致其它等待獲取鎖的線程進(jìn)入循環(huán)等待仗哨,消耗CPU形庭。使用不當(dāng)會(huì)造成CPU使用率極高。
2厌漂、上面Java實(shí)現(xiàn)的自旋鎖不是公平的萨醒,即無法滿足等待時(shí)間最長的線程優(yōu)先獲取鎖。不公平的鎖就會(huì)存在“線程饑餓”問題苇倡。
自旋鎖的優(yōu)點(diǎn):
1富纸、自旋鎖不會(huì)使線程狀態(tài)發(fā)生切換囤踩,一直處于用戶態(tài),即線程一直都是active的晓褪;不會(huì)使線程進(jìn)入阻塞狀態(tài)堵漱,減少了不必要的上下文切換,執(zhí)行速度快涣仿。
2勤庐、非自旋鎖在獲取不到鎖的時(shí)候會(huì)進(jìn)入阻塞狀態(tài),從而進(jìn)入內(nèi)核態(tài)好港,當(dāng)獲取到鎖的時(shí)候需要從內(nèi)核態(tài)恢復(fù)愉镰,需要線程上下文切換。(線程被阻塞后便進(jìn)入內(nèi)核(Linux)調(diào)度狀態(tài)钧汹,這個(gè)會(huì)導(dǎo)致系統(tǒng)在用戶態(tài)與內(nèi)核態(tài)之間來回切換丈探,嚴(yán)重影響鎖的性能)
可重入的自旋鎖和不可重入的自旋鎖:
解析:當(dāng)一個(gè)線程第一次已經(jīng)獲取到了該鎖,在鎖釋放之前又一次重新獲取該鎖拔莱,第二次就不能成功獲取碗降。由于不滿足CAS,所以第二次獲取會(huì)進(jìn)入while循環(huán)等待塘秦,而如果是可重入鎖遗锣,第二次也是應(yīng)該能夠成功獲取到的。
???而且嗤形,即使第二次能夠成功獲取精偿,那么當(dāng)?shù)谝淮吾尫沛i的時(shí)候,第二次獲取到的鎖也會(huì)被釋放赋兵,而這是不合理的笔咽。
???為了實(shí)現(xiàn)可重入鎖,我們需要引入一個(gè)計(jì)數(shù)器霹期,用來記錄獲取鎖的線程數(shù)叶组。
例子:
public classReentrantSpinLock{
???privateAtomicReference<Thread>cas= newAtomicReference<Thread>();
???privateintcount;
???public void lock() {
???????Thread current =Thread.currentThread();
???????if (current ==cas.get()) { //如果當(dāng)前線程已經(jīng)獲取到了鎖,線程數(shù)增加一历造,然后返回
???????????count++;
???????????return;
???????}
???????//如果沒獲取到鎖甩十,則通過CAS自旋
???????while (!cas.compareAndSet(null, current)) {
???????????// DO nothing
???????}
???}
???public void unlock() {
???????Thread cur =Thread.currentThread();
???????if (cur ==cas.get()) {
???????????if (count > 0) {//如果大于0,表示當(dāng)前線程多次獲取了該鎖吭产,釋放鎖通過count減一來模擬
???????????????count--;
???????????} else {//如果count==0侣监,可以將鎖釋放,這樣就能保證獲取鎖的次數(shù)與釋放鎖的次數(shù)是一致的了臣淤。
???????????????cas.compareAndSet(cur, null);
???????????}
???????}
???}
}
自旋鎖與互斥鎖
1橄霉、自旋鎖與互斥鎖都是為了實(shí)現(xiàn)保護(hù)資源共享的機(jī)制。
2邑蒋、無論是自旋鎖還是互斥鎖姓蜂,在任意時(shí)刻按厘,都最多只能有一個(gè)保持者。
3钱慢、獲取互斥鎖的線程逮京,如果鎖已經(jīng)被占用,則該線程將進(jìn)入睡眠狀態(tài)束莫;獲取自旋鎖的線程則不會(huì)睡眠懒棉,而是一直循環(huán)等待鎖釋放。
自旋鎖總結(jié)
1麦箍、自旋鎖:線程獲取鎖的時(shí)候,如果鎖被其他線程持有陶珠,則當(dāng)前線程將循環(huán)等待挟裂,直到獲取到鎖。
2揍诽、自旋鎖等待期間诀蓉,線程的狀態(tài)不會(huì)改變,線程一直是用戶態(tài)并且是活動(dòng)的(active)暑脆。
3渠啤、自旋鎖如果持有鎖的時(shí)間太長,則會(huì)導(dǎo)致其它等待獲取鎖的線程耗盡CPU添吗。
4沥曹、自旋鎖本身無法保證公平性,同時(shí)也無法保證可重入性碟联。
5妓美、基于自旋鎖,可以實(shí)現(xiàn)具備公平性和可重入性質(zhì)的鎖鲤孵。
Synchronzied和Lock的主要區(qū)別如下:
1壶栋、存在層面:Syncronized是Java中的一個(gè)關(guān)鍵字,存在于JVM層面普监,Lock是Java中的一個(gè)接口贵试。
2、鎖的釋放條件:
獲取鎖的線程執(zhí)行完同步代碼后凯正,自動(dòng)釋放毙玻;
2.線程發(fā)生異常時(shí),JVM會(huì)讓線程釋放鎖廊散;Lock必須在finally關(guān)鍵字中釋放鎖淆珊,不然容易造成線程死鎖。
3奸汇、鎖的獲取:在Syncronized中施符,假設(shè)線程A獲得鎖往声,B線程等待。如果A發(fā)生阻塞戳吝,那么B會(huì)一直等待浩销。在Lock中,會(huì)分情況而定听哭,Lock中有嘗試獲取鎖的方法慢洋,如果嘗試獲取到鎖,則不用一直等待陆盘。
4普筹、鎖的狀態(tài):Synchronized無法判斷鎖的狀態(tài),Lock則可以判斷隘马。
5太防、鎖的類型:
5.1、Synchronized是可重入酸员,不可中斷蜒车,非公平鎖。
5.2幔嗦、Lock鎖則是可重入酿愧,可判斷,可公平鎖邀泉。
6嬉挡、鎖的性能:Synchronized適用于少量同步的情況下,性能開銷比較大汇恤。
??Lock鎖適用于大量同步階段:
6.1棘伴、Lock鎖可以提高多個(gè)線程進(jìn)行讀的效率(使用readWriteLock)
6.2、在競爭不是很激烈的情況下屁置,Synchronized的性能要優(yōu)于ReetrantLock焊夸,但是在資源競爭很激烈的情況下,Synchronized的性能會(huì)下降幾十倍蓝角,但是ReetrantLock的性能能維持常態(tài)阱穗;
6.3、ReetrantLock提供了多樣化的同步使鹅,比如有時(shí)間限制的同步揪阶,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。
Atomic的使用:
簡介:J2SE 5.0提供了一組atomic class來幫助我們簡化同步處理患朱。
???????基本工作原理是使用了同步synchronized的方法實(shí)現(xiàn)了對(duì)一個(gè)long, integer,對(duì)象的增鲁僚、減、賦值(更新)操作.?比如對(duì)于++運(yùn)算符AtomicInteger可以將它持有的integer能夠atomic地遞增。在需要訪問兩個(gè)或兩個(gè)以上atomic變量的程序代碼(或者是對(duì)單一的atomic變量執(zhí)行兩個(gè)或兩個(gè)以上的操作)通常都需要被synchronize以便兩者的操作能夠被當(dāng)作是一個(gè)atomic的單元冰沙。
AtomicInteger使用:
1侨艾、多個(gè)線程訪問同一個(gè)整型數(shù)值;
2拓挥、自動(dòng)增加/減小值唠梨;
3、經(jīng)常作為流水值使用侥啤;
4当叭、線程安全,使用原子鎖盖灸;
5蚁鳖、包名java.util.concurrent.atomic, 該包名下包含其它同步數(shù)值類AtomicBoolean赁炎、AtomicLong等醉箕;
6、常用方法:get()甘邀、set()琅攘、getAndIncrement()垮庐、getAndDecrement()松邪;
下面通過簡單的兩個(gè)例子的對(duì)比來看一下AtomicInteger的強(qiáng)大的功能
例子一:
class Counter {
???private volatileintcount = 0;
???public synchronized void increment() {
??????????count++;?//若要線程安全執(zhí)行count++,需要加鎖
???}
???publicintgetCount() {
??????????return count;
???}
}
例子二:
class Counter {
???privateAtomicIntegercount = newAtomicInteger();
???public void increment() {
??????????count.incrementAndGet();
???}
??????//使用AtomicInteger之后哨查,不需要加鎖逗抑,也可以實(shí)現(xiàn)線程安全。
???publicintgetCount() {
?????????returncount.get();
???}
}
總結(jié):那么為什么不使用記數(shù)器自加呢寒亥,例如count++這樣的邮府,因?yàn)檫@種計(jì)數(shù)是線程不安全的,高并發(fā)訪問時(shí)統(tǒng)計(jì)會(huì)有誤溉奕,而AtomicInteger為什么能夠達(dá)到多而不亂褂傀,處理高并發(fā)應(yīng)付自如呢?
???這是由硬件提供原子操作指令實(shí)現(xiàn)的加勤。在非激烈競爭的情況下仙辟,開銷更小,速度更快鳄梅。Java.util.concurrent中實(shí)現(xiàn)的原子操作類包括:AtomicBoolean叠国、AtomicInteger、AtomicLong戴尸、AtomicReference粟焊。
Volatile關(guān)鍵字:
簡述:Java語言提供了一種稍弱的同步機(jī)制,即volatile變量,用來確保將變量的更新操作通知到其他線程项棠。當(dāng)把變量聲明為volatile類型后悲雳,編譯器與運(yùn)行時(shí)都會(huì)注意到這個(gè)變量是共享的,因此不會(huì)將該變量上的操作與其他內(nèi)存操作一起重排序沾乘。volatile變量不會(huì)被緩存在寄存器或者對(duì)其他處理器不可見的地方怜奖,因此在讀取volatile類型的變量時(shí)總會(huì)返回最新寫入的值。
在訪問volatile變量時(shí)不會(huì)執(zhí)行加鎖操作翅阵,因此也就不會(huì)使執(zhí)行線程阻塞歪玲,因此volatile變量是一種比sychronized關(guān)鍵字更輕量級(jí)的同步機(jī)制。???
???而聲明變量是volatile的掷匠,JVM保證了每次讀變量都從內(nèi)存中讀滥崩,跳過CPU cache這一步,底層基于C++的volatile實(shí)現(xiàn)讹语,因?yàn)関olatile自帶了編譯器屏障的功能钙皮,總能拿到內(nèi)存中的最新值。
當(dāng)一個(gè)變量定義為volatile之后顽决,將具備兩種特性:
1.保證此變量對(duì)所有的線程的可見性短条,這里的“可見性”,當(dāng)一個(gè)線程修改了這個(gè)變量的值才菠,volatile保證了新值能立即同步到主內(nèi)存茸时,以及每次使用前立即從主內(nèi)存刷新。但普通變量做不到這點(diǎn)赋访,普通變量的值在線程間傳遞均需要通過主內(nèi)存來完成可都。
2.禁止指令重排序優(yōu)化。有volatile修飾的變量蚓耽,賦值后多執(zhí)行了一個(gè)“l(fā)oadaddl$0x0, (%esp)”操作渠牲,這個(gè)操作相當(dāng)于一個(gè)內(nèi)存屏障(指令重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置),只有一個(gè)CPU訪問內(nèi)存時(shí)步悠,并不需要內(nèi)存屏障签杈;(什么是指令重排序:是指CPU采用了允許將多條指令不按程序規(guī)定的順序分開發(fā)送給各相應(yīng)電路單元處理)。
例子:
public volatileintcount = 0;
???public?intTestVolatile() {
???????finalCountDownLatchcountDownLatch= newCountDownLatch(1000);
???????for (inti= 0;i< 1000;i++) {
???????????new Thread(new Runnable() {
???????????????@Override
???????????????public void run() {
???????????????????try {
???????????????????????Thread.sleep(10);
???????????????????} catch (InterruptedExceptione) {
???????????????????}
???????????????????increase();
???????????????????countDownLatch.countDown();
???????????????}
???????????}).start();
???????}
???????try {
???????????countDownLatch.await();
???????} catch (InterruptedExceptione) {
???????????e.printStackTrace();
???????}
???????System.out.println("<<<<<" + count);
???????return count;
???}
???public void increase() {
???????count++;
???}
出現(xiàn)異常的原因分析:
???來看看count++操作的時(shí)候內(nèi)存都做了什么操作:
1鼎兽、從主內(nèi)存里面(棧)讀取到count的值答姥,到CPU的高速緩存當(dāng)中。
2接奈、寄存器對(duì)count的值進(jìn)行加一操作踢涌。
3、將CPU的高速緩存當(dāng)中的count值刷新到主內(nèi)存(棧)當(dāng)中序宦。
???我們可以看到++這個(gè)操作非原子睁壁,先讀count,然后+1, 最后再寫count潘明。
如果變量count被使用了volatile修飾行剂,那么在thread1中,當(dāng)count變?yōu)?的時(shí)候钳降,就會(huì)強(qiáng)制刷新到主存厚宰。如果這個(gè)時(shí)候,thread2已經(jīng)將count =2從從主存映射到緩存上并且已經(jīng)做完了自增操作遂填,此時(shí)count =3铲觉,那么最終主存中count值為3。
???所以吓坚,如果我們想讓count的最終值是4,僅僅保證可見性是不夠的撵幽,還得保證原子性。也就是對(duì)于變量count的自增操作加鎖礁击,保證任意一個(gè)時(shí)刻只有一個(gè)線程對(duì)count進(jìn)行自增操作盐杂。可以說volatile是一種“輕量級(jí)的鎖”哆窿,它能保證鎖的可見性链烈,但不能保證鎖的原子性。
例子二:
//線程1
volatilebooleanstop = false;
while(!stop){
???doSomething();
}
//線程2
stop = true;
沒有出現(xiàn)異常的原因:
1挚躯、使用volatile關(guān)鍵字會(huì)強(qiáng)制將修改的值立即寫入主存强衡;
2、使用volatile關(guān)鍵字的話秧均,當(dāng)線程2進(jìn)行修改時(shí)食侮,會(huì)導(dǎo)致線程1的工作內(nèi)存中緩存變量stop的緩存行無效号涯,也就是執(zhí)行線程1的CPU緩存中的stop無效目胡。
3、由于線程1的工作內(nèi)存中緩存變量stop的緩存行無效链快,所以線程1再次讀取變量stop的值時(shí)會(huì)去主存讀取誉己。
???那么在線程2修改stop值時(shí)(當(dāng)然這里包括2個(gè)操作,修改線程2工作內(nèi)存中的值域蜗,然后將修改后的值寫入內(nèi)存)巨双,會(huì)使得線程1的工作內(nèi)存中緩存變量stop的緩存行無效,然后線程1讀取時(shí)霉祸,發(fā)現(xiàn)自己的緩存行無效筑累,它會(huì)等待緩存行對(duì)應(yīng)的主存地址被更新之后,然后去對(duì)應(yīng)的主存讀取最新的值丝蹭,那么線程1讀取到的就是最新的正確的值慢宗。
volatile和synchronized區(qū)別:
1、volatile本質(zhì)是在告訴jvm當(dāng)前變量在寄存器中的值是不確定的,需要從主存中讀取,synchronized則是鎖定當(dāng)前變量镜沽,只有當(dāng)前線程可以訪問該變量敏晤,其他線程被阻塞住。
2缅茉、volatile僅能使用在變量級(jí)別嘴脾,synchronized則可以使用在變量,方法蔬墩。
3译打、volatile僅能實(shí)現(xiàn)變量的修改可見性,而synchronized則可以保證變量的修改可見性和原子性拇颅,《Java編程思想》上說扶平,定義long或double變量時(shí),如果使用volatile關(guān)鍵字蔬蕊,就會(huì)獲得(簡單的賦值與返回操作)原子性结澄。?
4、volatile不會(huì)造成線程的阻塞岸夯,而synchronized可能會(huì)造成線程的阻塞.
5麻献、當(dāng)一個(gè)域的值依賴于它之前的值時(shí),volatile就無法工作了猜扮,如n=n+1,n++等勉吻。如果某個(gè)域的值受到其他域的值的限制,那么volatile也無法工作旅赢,如Range類的lower和upper邊界齿桃,必須遵循lower<=upper的限制。
6煮盼、使用volatile而不是synchronized的唯一安全的情況是類中只有一個(gè)可變的域短纵。
ThreadLocal關(guān)鍵字:
???共享變量一直是并發(fā)中的老大難問題,每個(gè)線程都對(duì)它有操作權(quán)僵控,所以線程之間的同步很關(guān)鍵香到,鎖也就應(yīng)運(yùn)而生。這里換一個(gè)思路报破,是否可以把共享變量私有化悠就?即每個(gè)線程都擁有一份共享變量的本地副本,每個(gè)線程對(duì)應(yīng)一個(gè)副本充易,同時(shí)對(duì)共享變量的操作也改為對(duì)屬于自己的副本的操作梗脾,這樣每個(gè)線程處理自己的本地變量,形成數(shù)據(jù)隔離盹靴。事實(shí)上這就是ThreadLocal了炸茧。
使用場景:
1帆疟、ThreadLocal最適合的是變量在線程間隔離而在方法或類間共享的場景。
2宇立、每個(gè)線程需要自己獨(dú)立的實(shí)例且該實(shí)例需要在多個(gè)方法中使用踪宠。
輸出結(jié)果:
???Thread[Thread-1,5,main]====57
???Thread[Thread-0,5,main]====75
???????創(chuàng)建了兩個(gè)線程,它們都在threadlocal上面都set了一個(gè)隨機(jī)數(shù)妈嘹,我們看最后的輸出結(jié)果每個(gè)都是不同的值柳琢,那么我們?nèi)绻裻hreadlocal替換成一個(gè)集合會(huì)發(fā)生什么,由于兩個(gè)線程時(shí)上個(gè)線程生成的隨機(jī)數(shù)57會(huì)被第二個(gè)線程覆蓋掉润脸,而在Threadlocal中兩個(gè)線程都是操作的自己的本地副本柬脸,那么兩個(gè)線程互不影響都無法操控到對(duì)方的數(shù)據(jù),因此它們存取的都是不同的值毙驯。
實(shí)現(xiàn)原理:
ThreadLocal.ThreadLocalMapthreadLocals= null;
ThreadLocal.ThreadLocalMapinheritableThreadLocals= null;
???????我們調(diào)用ThreadLocal的set或者get才會(huì)真正創(chuàng)建他們倒堕,也就是你以為你把變量交給ThreadLocal了,其實(shí)這小子轉(zhuǎn)手就給ThreadLocalMap了爆价,ThreadLocal就是套在ThreadLocalMap外面的一層殼而已垦巴。
ThreadLocal的組成如下:
解析:可以看出,就跟map基本一樣铭段,key是ThreadLocal的引用骤宣,value則是由開發(fā)者設(shè)置,即本地變量序愚。
ThreadLocal函數(shù)原理:
Set函數(shù):
public void set(T value) {
???????Thread t =Thread.currentThread();
???????ThreadLocalMapmap =getMap(t);
???????if (map != null)
???????????//這里this是ThreadLocal的實(shí)例引用
???????????map.set(this, value);
???????else
???????????createMap(t, value);
}
voidcreateMap(Thread t, TfirstValue) {
???????t.threadLocals= newThreadLocalMap(this,firstValue);
}
解析:
1憔披、首先獲取當(dāng)前線程
2、以當(dāng)前線程為key去查找當(dāng)前線程的map即threadLocals變量
3爸吮、如果threadLocals不為空芬膝,就把ThreadLocal引用作為key,value傳給map
4形娇、否則創(chuàng)建map,也就是初始化當(dāng)前線程的threadLocals變量
備注:再次注意值存放的實(shí)際位置是Thread中的ThreadLocalMap變量锰霜,ThreadLocalMap是一個(gè)map,key是ThreadLocal的實(shí)例引用埂软,值則是我們要存的變量.
Get函數(shù):
public T get() {
???????Thread t =Thread.currentThread();
???????ThreadLocalMapmap =getMap(t);
???????if (map != null) {
???????????ThreadLocalMap.Entrye =map.getEntry(this);
???????????if (e != null) {
???????????????@SuppressWarnings("unchecked")
???????????????T result = (T)e.value;
???????????????return result;
???????????}
???????}
???????returnsetInitialValue();
}
解析:同樣道理锈遥,先得到當(dāng)前線程纫事,然后得到成員變量threadLocals勘畔,如果threadLocals不為空,返回本地變量對(duì)應(yīng)值丽惶,否則初始化threadLocals炫七。
初始化threadLocals函數(shù):
private TsetInitialValue() {
???????T value =initialValue();
???????Thread t =Thread.currentThread();
???????ThreadLocalMapmap =getMap(t);
???????if (map != null)
???????????map.set(this, value);
???????else
???????????createMap(t, value);
???????return value;
}
protected TinitialValue() {
???????return null;
}
解析:判斷當(dāng)前threadLocals是否為空,如果不為空钾唬,設(shè)置當(dāng)前ThreadLocal的實(shí)例引用對(duì)應(yīng)變量為null万哪,否則調(diào)用createMap創(chuàng)建threadLocals變量侠驯。
remove()函數(shù):
public void remove() {
????????ThreadLocalMapm =getMap(Thread.currentThread());
????????if (m != null)
????????????m.remove(this);
}
解析:如果threadLocals變量不為空,刪掉map中當(dāng)前ThreadLocal對(duì)應(yīng)實(shí)例引用的本地變量奕巍。
ThreadLocal的內(nèi)存溢出問題:
解析:
???從上邊一路走下來我們應(yīng)該了解了吟策,每一個(gè)線程中都有一個(gè)ThreadLocalMap
類型的threadLocals變量,這個(gè)map中key為ThreadLocal的實(shí)例引用的止,value為對(duì)應(yīng)的本地變量檩坚。如果這個(gè)線程不消亡,開發(fā)者也沒有采用remove操作及時(shí)清除掉不再使用的變量诅福,這些變量就會(huì)一直存在map中匾委,直到撐爆你的內(nèi)存,造成內(nèi)存溢出問題氓润。
總結(jié):
1赂乐、ThreadLocal并不解決線程間共享數(shù)據(jù)的問題。
2咖气、ThreadLocal通過隱式的在不同線程內(nèi)創(chuàng)建獨(dú)立實(shí)例副本避免了實(shí)例線程安全的問題挨措。
3、每個(gè)線程持有一個(gè)Map并維護(hù)了ThreadLocal對(duì)象與具體實(shí)例的映射崩溪,該Map由于只被持有它的線程訪問运嗜,故不存在線程安全以及鎖的問題。
4悯舟、每個(gè)線程對(duì)應(yīng)一個(gè)ThreadLocalMap担租,ThreadLocal其實(shí)就是套在ThreadLocalMap上的一層殼。
5抵怎、ThreadLocalMap的key是ThreadLocal的實(shí)例引用奋救,value是我們像設(shè)置的本地變量。
6反惕、若是線程一直不停尝艘,threadLocalMap中的本地變量就會(huì)越來越多,注意及時(shí)remove掉不再使用的變量姿染,防止內(nèi)存溢出背亥。
7、ThreadLocalMap的Entry對(duì)ThreadLocal的引用為弱引用悬赏,避免了ThreadLocal對(duì)象無法被回收的問題狡汉。
8、ThreadLocalMap的set方法通過調(diào)用replaceStaleEntry方法回收鍵為null的Entry對(duì)象的值(即為具體實(shí)例)以及Entry對(duì)象本身從而防止內(nèi)存泄漏闽颇。
9盾戴、ThreadLocal適用于變量在線程間隔離且在方法間共享的場景。
? ? ? 今天大概就說這么多兵多,后續(xù)的關(guān)于并發(fā)相關(guān)的知識(shí)點(diǎn)會(huì)再分兩篇文章總結(jié)尖啡,一共是三篇文章橄仆。
七:擴(kuò)展閱讀
1、https://www.xuebuyuan.com/3253276.html(synchronized的4種用法)
2衅斩、一文帶你徹底搞懂ThreadLocal(微信公眾號(hào))
3盆顾、https://blog.csdn.net/javazejian/article/details/72772461(全面理解Java內(nèi)存模型(JMM)及volatile關(guān)鍵字)
4、https://juejin.im/post/5d2c97bff265da1bc552954b(圖解Java線程安全)
5畏梆、https://www.cnblogs.com/paddix/p/5367116.html(Java并發(fā)編程:Synchronized及其實(shí)現(xiàn)原理椎扬,系列)
6、http://www.reibang.com/p/cfac5c131a9b(面試字節(jié)跳動(dòng)Android研發(fā)崗具温,已拿到offer蚕涤,這些知識(shí)點(diǎn)該放出來了)
7、http://www.infoq.com/cn/articles/java-se-16-synchronized(聊聊并發(fā)(二)——Java SE1.6中的Synchronized)
8铣猩、https://blog.csdn.net/niuwei22007/article/details/51433669(synchronized的JVM底層實(shí)現(xiàn)(很詳細(xì) 很底層))
9揖铜、https://blog.csdn.net/qq_22771739/article/details/82529874(Java線程的6種狀態(tài)及切換(透徹講解))
10、https://blog.csdn.net/lkforce/article/details/81128115(Java的對(duì)象頭和對(duì)象組成詳解)
11达皿、Synchronized和Lock的區(qū)別和使用場景(微信公眾號(hào))
12天吓、一文帶你理解Java中Lock的實(shí)現(xiàn)原理(微信公眾號(hào))
13、https://www.cnblogs.com/0616--ataozhijia/p/6869657.html(AtomicInteger的用法)
14峦椰、面試必問的volatile龄寞,你了解多少 (微信公眾號(hào))
15、Java中Volatile關(guān)鍵字詳解(微信公眾號(hào))
16汤功、https://segmentfault.com/a/1190000017766364(Java中15種鎖的介紹:公平鎖物邑,可重入鎖,獨(dú)享鎖滔金,互斥鎖色解,樂觀鎖,分段鎖餐茵,自旋鎖等等)
17科阎、ThreadLocal到底是什么?它解決了什么問題(微信公眾號(hào))
18忿族、Java并發(fā)集合的實(shí)現(xiàn)原理(微信公眾號(hào))
19锣笨、https://blog.csdn.net/jackyrongvip/article/details/89472397(筆記:Collections的synchronized XXX方法)
20、https://blog.csdn.net/qq_34039315/article/details/78549311(Java并發(fā)編程75道面試題及答案——穩(wěn)了)