synchronized
并發(fā)編程中的三個問題:可見性(Visibility)是指一個線程對共享變量進行修改,另一個先立即得到修改后的最新值。 代碼演示:
小結(jié):并發(fā)編程時,會出現(xiàn)可見性問題,當一個線程對共享變量進行了修改,另外的線程并沒有立即看到修改 后的最新值。 原子性(Atomicity)在一次或多次操作中,要么所有的操作都執(zhí)行并且不會受其他因素干擾而中斷,要么所有的操作都不執(zhí)行 代碼演示:
通過 小結(jié):并發(fā)編程時,會出現(xiàn)原子性問題,當一個線程對共享變量操作到一半時,另外的線程也有可能來操作共享變量,干擾了前一個線程的操作 有序性(Ordering)是指程序中代碼的執(zhí)行順序,Java在編譯時和運行時會對代碼進行優(yōu)化,會導致程序最終的執(zhí)行順序不一定就是我們編寫代碼時的順序。 代碼演示:
運行的結(jié)果有:0、1、4 小結(jié):程序代碼在執(zhí)行過程中的先后順序,由于Java在編譯期以及運行期的優(yōu)化,導致了代碼的執(zhí)行順序未必 就是開發(fā)者編寫代碼時的順序。 Java內(nèi)存模型(JMM)計算機結(jié)構(gòu)簡介根據(jù)馮諾依曼體系結(jié)構(gòu),計算機由五大組成部分,輸入設(shè)備,輸出設(shè)備,存儲器,控制器,運算器。 CPU: 中央處理器,是計算機的控制和運算的核心,我們的程序最終都會變成指令讓CPU去執(zhí)行,處理程序中的數(shù)據(jù)。 內(nèi)存: 我們的程序都是在內(nèi)存中運行的,內(nèi)存會保存程序運行時的數(shù)據(jù),供CPU處理。 緩存: CPU的運算速度和內(nèi)存的訪問速度相差比較大。這就導致CPU每次操作內(nèi)存都要耗費很多等待時間。于是就有了在 CPU和主內(nèi)存之間增加緩存的設(shè)計。CPU Cache分成了三個級別: L1, L2, L3。級別越小越接近CPU,速度也更快,同時也代表著容量越小。 Java內(nèi)存模型Java內(nèi)存模型是一套規(guī)范,描述了Java程序中各種變量(線程共享變量)的訪問規(guī)則,以及在JVM中將變量存儲到內(nèi)存和從內(nèi)存中讀取變量這樣的底層細節(jié),具體如下。 主內(nèi)存 主內(nèi)存是所有線程都共享的,都能訪問的。所有的共享變量都存儲于主內(nèi)存。 工作內(nèi)存 每一個線程有自己的工作內(nèi)存,工作內(nèi)存只存儲該線程對共享變量的副本。線程對變量的所有的操作(讀,取)都必須在工作內(nèi)存中完成,而不能直接讀寫主內(nèi)存中的變量,不同線程之間也不能直接訪問對方工作內(nèi)存中的變量。 小結(jié) Java內(nèi)存模型是一套規(guī)范,描述了Java程序中各種變量(線程共享變量)的訪問規(guī)則,以及在JVM中將變量存儲到內(nèi)存和從內(nèi)存中讀取變量這樣的底層細節(jié),Java內(nèi)存模型是對共享數(shù)據(jù)的可見性、有序性、和原子性的規(guī)則和保障。 主內(nèi)存與工作內(nèi)存之間的交互注意:1. 如果對一個變量執(zhí)行l(wèi)ock操作,將會清空工作內(nèi)存中此變量的值
synchronized保證三大特性synchronized保證可見性
小結(jié): synchronized保證可見性的原理,執(zhí)行synchronized時,lock原子操作會刷新工作內(nèi)存中共享變量的值。 synchronized保證原子性
小結(jié): synchronized保證原子性的原理,synchronized保證只有一個線程拿到鎖,能夠進入同步代碼塊。 synchronized保證有序性
小結(jié) synchronized保證有序性的原理,我們加synchronized后,依然會發(fā)生重排序,只不過,我們有同步代碼塊,可以保證只有一個線程執(zhí)行同步代碼中的代碼,保證有序性。 synchronized的特性可重入特性
可重入原理: synchronized的鎖對象中有一個計數(shù)器(recursions變量)會記錄線程獲得幾次鎖。 可重入的好處:
小結(jié): synchronized是可重入鎖,內(nèi)部鎖對象中會有一個計數(shù)器記錄線程獲取幾次鎖啦,獲取一次鎖加+1,在執(zhí)行完同步代碼塊時,計數(shù)器的數(shù)量會-1,直到計數(shù)器的數(shù)量為0,就釋放這個鎖。 不可中斷特性什么是不可中斷? 一個線程獲得鎖后,另一個線程想要獲得鎖,必須處于阻塞或等待狀態(tài),如果第一個線程不釋放鎖,第二個線程會一直阻塞或等待,不可被中斷。
synchronized是不可中斷,處于阻塞狀態(tài)的線程會一直等待鎖。 ReentrantLock可中斷演示
小結(jié): synchronized屬于不可被中斷 Lock的lock方法是不可中斷的 Lock的tryLock方法是可中斷的 synchronized 的原理monitorenter:每一個對象都會和一個監(jiān)視器monitor關(guān)聯(lián)。監(jiān)視器被占用時會被鎖住,其他線程無法來獲取該monitor。當JVM執(zhí)行某個線程的某個方法內(nèi)部的monitorenter時,它會嘗試去獲取當前對象對應(yīng)的monitor的所有權(quán)。其過程如下:
monitorenter小結(jié): synchronized的鎖對象會關(guān)聯(lián)一個monitor, 這個monitor不是我們主動創(chuàng)建的, 是JVM的線程執(zhí)行到這個同步代碼塊,發(fā)現(xiàn)鎖對象 有monitor就會創(chuàng)建monitor, monitor內(nèi)部有兩個重要的成員變量owner擁有這把鎖的線程,recursions會記錄線程擁有鎖的次數(shù), 當一個線程擁有monitor后其他線程只能等待。 monitorexit:
monitorexit釋放鎖。 monitorexit插入在方法結(jié)束處和異常處,JVM保證每個monitorenter必須有對應(yīng)的monitorexit。 面試題synchroznied出現(xiàn)異常會釋放鎖嗎? :會釋放鎖。 同步方法同步方法在反匯編后,會增加ACC_SYNCHRONIZED修飾。會隱式調(diào)用monitorenter 和monitorexit。在執(zhí)行同步方法前會調(diào)用 monitorenter,在執(zhí)行完同步方法后會調(diào)用monitorexit 。 小結(jié): 通過javap反匯編可以看到synchronized 使用了monitorentor和monitorexit兩個指令。每個鎖對象都會關(guān)聯(lián)一個monitor(監(jiān)視 器,它才是真正的鎖對象),它內(nèi)部有兩個重要的成員變量owner會保存獲得鎖的線程,recursions會保存線程獲得鎖的次數(shù), 當執(zhí)行到 monitorexit時, recursions會-1, 當計數(shù)器減到0時這個線程就會釋放鎖。 面試題:synchronized與Lock的區(qū)別1、synchronized 是關(guān)鍵字,lock 是一個接口 2、synchronized 會自動釋放鎖,lock 需要手動釋放鎖。 3、synchronized 是不可中斷的,lock 可以中斷也可以不中斷。 4、通過lock 可以知道線程有沒有拿到鎖,而synchronized 不能。 5、synchronized 能鎖住方法和代碼塊,而lock 只能鎖住代碼塊。 6、lock 可以使用讀鎖提高多線程讀效率。 7、synchronized 是非公平鎖,ReentrantLock 可以控制是否是公平鎖。 CAScas的概述和作用: compare and swap,可以將比較和交換轉(zhuǎn)為原子操作,這個原子操作直接由cpu保證,cas可以保證共享變量賦值時的原子操作,cas依賴3個值:內(nèi)存中的值v,舊的預估值x,要修改的新值b。根據(jù)atomicInteger的地址加上偏移量offset的值可以得到內(nèi)存中的值,將內(nèi)存中的值和舊的預估值進行比較,如果相同,就將新值保存到內(nèi)存中。不相同就進行重試。 Java對象的布局在JVM中,對象在內(nèi)存中的布局分為三塊區(qū)域:對象頭、實例數(shù)據(jù)和對齊填充。如下圖所示: HotSpot采用instanceOopDesc和arrayOopDesc來描述對象頭,arrayOopDesc對象用來描述數(shù)組類型。 從instanceOopDesc代碼中可以看到 instanceOopDesc繼承自oopDesc。 _mark表示對象標記、屬于markOop類型,也就是Mark World,它記錄了對象和鎖有關(guān)的信息 _metadata表示類元信息,類元信息存儲的是對象指向它的類元數(shù)據(jù)(Klass)的首地址,其中Klass表示普通指針、compressed_klass表示壓縮類指針。 Mark Word
klass pointer 用于存儲對象的類型指針,該指針指向它的類元數(shù)據(jù),JVM通過這個指針確定對象是哪個類的實例。通過-XX:+UseCompressedOops開啟指針壓縮, 在64位系統(tǒng)中,Mark Word = 8 bytes,類型指針 = 8bytes,對象頭 = 16 bytes = 128bits; 實例數(shù)據(jù) 就是類中定義的成員變量。 對齊填充 由于HotSpot VM的自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍,對象的大小必須是8字節(jié)的整數(shù)倍。因此,當對象實例數(shù)據(jù)部分沒有對齊時,就需要通過對齊填來補全。 查看Java對象布局
小結(jié) Java對象由3部分組成,對象頭,實例數(shù)據(jù),對齊數(shù)據(jù),對象頭分成兩部分:Mark World + Klass pointer 偏向鎖什么是偏向鎖? 鎖會偏向于第一個獲得它的線程,會在對象頭存儲鎖偏向的線程ID,以后該線程進入和退出同步塊時只需要檢查是否為偏向鎖、鎖標志位以及ThreadID即可。 不過一旦出現(xiàn)多個線程競爭時必須撤銷偏向鎖,所以撤銷偏向鎖消耗的性能必須小于之前節(jié)省下來的CAS原子操作的性能消耗,不然就得不償失了。 偏向鎖原理 當線程第一次訪問同步塊并獲取鎖時,偏向鎖處理流程如下:
偏向鎖的撤銷
偏向鎖是自適應(yīng)的 小結(jié): 偏向鎖的原理是什么?
偏向鎖的好處是什么?
輕量級鎖什么是輕量級鎖? 輕量級鎖是JDK 6之中加入的新型鎖機制,輕量級鎖并不是用來代替重量級鎖的。 引入輕量級鎖的目的:在多線程交替執(zhí)行同步塊的情況下,盡量避免重量級鎖引起的性能消耗,但是如果多個線程在同一時刻進入臨界區(qū),會導致輕量級鎖膨脹升級為重量級鎖,所以輕量級鎖的出現(xiàn)并非是要代替重量級鎖。 輕量級鎖原理: 當關(guān)閉偏向鎖或多個線程競爭偏向鎖導致偏向鎖升級為輕量級鎖,則會嘗試獲取輕量級鎖,其步驟如下:
輕量級鎖的釋放: 輕量級鎖的釋放也是通過CAS操作來進行的,主要步驟如下:
對于輕量級鎖,其性能提升的依據(jù)是“對于絕大部分的鎖,在整個生命周期內(nèi)都是不會存在競爭的”,如果打破這個依據(jù)則除了互斥的開銷外,還有額外的CAS 操作,因此在有多線程競爭的情況下,輕量級鎖比重量級鎖更慢。 輕量級鎖好處: 在多線程交替執(zhí)行同步塊的情況下,可以避免重量級鎖引起的性能消耗。 自旋鎖monitor 實現(xiàn)鎖的時候, monitor 會阻塞和喚醒線程,線程的阻塞和喚醒需要CPU 從用戶態(tài)轉(zhuǎn)為核心態(tài),頻繁的阻塞和喚醒對CPU 來說是一件負擔很重的工作,這些操作給系統(tǒng)的并發(fā)性能帶來了很大的壓力。同時,共享數(shù)據(jù)的鎖定狀態(tài)可能只會持續(xù)很短的一段時間,為了這段時間阻塞和喚醒線程并不值得。如果有一個以上的處理器,能讓兩個或以上的線程同時并行執(zhí)行,就可以讓后面請求鎖的那個線程“稍微等一下”,但不放棄處理器的執(zhí)行時間,看看持有鎖的線程是否釋放了鎖。為了讓線程等待,我們只需讓線程執(zhí)行一個循環(huán)(即自旋),這就是自旋鎖。 自旋鎖在JDK 1.4.2中就已經(jīng)引入,只不過默認是關(guān)閉的,可以使用-XX:+UseSpinning參數(shù)來開啟,在JDK 6中就已經(jīng)改為默認開啟了。自旋等待不能代替阻塞,且先不說對處理器數(shù)量的要求,自旋等待本身雖然避免了線程切換的開銷,但它是要占用處理器時間的,因此,如果鎖被占用的時間很短,自旋等待的效果就會非常好,反之,如果鎖被占用的時間很長。那么自旋的線程只會白白消耗處理器資源,而不會做任何有用的工作,反而會帶來性能上的浪費。因此,自旋等待的時間必須要有一定的限度,如果自旋超過了限定的次數(shù)仍然沒有成功獲得鎖,就應(yīng)當使用傳統(tǒng)的方式去掛起線程了。自旋次數(shù)的默認值是10次,用戶可以使用參數(shù)-XX : PreBlockSpin來更改。 適應(yīng)性自旋鎖 在JDK 6 中引入了自適應(yīng)的自旋鎖。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運行中,那么虛擬機就會認為這次自旋也很有可能再次成功,進而它將允許自旋等待持續(xù)相對更長的時間。如果對于某個鎖,自旋很少成功獲得過,那在以后要獲取這個鎖時將可能省略掉自旋過程,以避免浪費處理器資源。 平時寫代碼如何對synchronized優(yōu)化減少synchronized的范圍: 同步代碼塊中盡量短,減少同步代碼塊中代碼的執(zhí)行時間,減少鎖的競爭。
降低synchronized鎖的粒度: 將一個鎖拆分為多個鎖提高并發(fā)度,如HashTable:鎖定整個哈希表,一個操作正在進行時,其他操作也同時鎖定,效率低下。ConcurrentHashMap:局部鎖定,只鎖定桶。 讀寫分離: 讀取時不加鎖,寫入和刪除時加鎖 ConcurrentHashMap,CopyOnWriteArrayList和ConyOnWriteSet |
|