Java Memory Model(JMM)java內存模型,區(qū)別與java內存結構。JMM定義了一套在多線程讀寫共享數(shù)據(jù)(變量、數(shù)組)時,對數(shù)據(jù)的可見性、有序性和原子性的規(guī)則和保障。
原子性
原子性是指一個操作是不可中斷的,要么全部執(zhí)行成功要么全部執(zhí)行失敗。即使在多個線程一起執(zhí)行的時候,一個操作一旦開始,就不會被其他線程所干擾。
多線程情況下,對同一個對象進行操作時,會導致字節(jié)碼指令交錯執(zhí)行,從而產生原子性問題,可以通過synchronize關鍵字解決
可見性
可見性是指當一個線程修改了共享變量后,其他線程能夠立即得知這個修改
有序性
如果在本線程內觀察,所有的操作都是有序的;(線程內表現(xiàn)為串行的語義)如果在一個線程中觀察另外一個線程,所有的操作都是無序的。
多線程情況下,jvm會進行指令重排,會影響有序性。
happens-before
happens-before規(guī)定了哪些寫操作對其他讀操作可見,即前一個操作的結果可以被后續(xù)的操作獲取。它是可見性與有序性的一套規(guī)則總結。
程序次序規(guī)則:在一個線程內一段代碼的執(zhí)行結果是有序的。就是還會指令重排,但是隨便它怎么排,結果是按照我們代碼的順序生成的不會變!
管程鎖定規(guī)則:就是無論是在單線程環(huán)境還是多線程環(huán)境,對于同一個鎖來說,一個線程對這個鎖解鎖之后,另一個線程獲取了這個鎖都能看到前一個線程的操作結果!(管程是一種通用的同步原語,synchronized就是管程的實現(xiàn))
volatile變量規(guī)則:就是如果一個線程先去寫一個volatile變量,然后一個線程去讀這個變量,那么這個寫操作的結果一定對讀的這個線程可見。
線程啟動規(guī)則:在主線程A執(zhí)行過程中,啟動子線程B,那么線程A在啟動子線程B之前對共享變量的修改結果對線程B可見。
線程終止規(guī)則:在主線程A執(zhí)行過程中,子線程B終止,那么線程B在終止之前對共享變量的修改結果在線程A中可見。
線程中斷規(guī)則:對線程interrupt()方法的調用先行發(fā)生于被中斷線程代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()檢測到是否發(fā)生中斷。
傳遞規(guī)則:這個簡單的,就是happens-before原則具有傳遞性,即A happens-before B , B happens-before C,那么A happens-before C。
對象終結規(guī)則:這個也簡單的,就是一個對象的初始化的完成,也就是構造函數(shù)執(zhí)行的結束一定 happens-before它的finalize()方法。
synchronize和volatile對比
1、volatile是線程同步的輕量級實現(xiàn),性能比synchronize好
2、volatile只能修飾變量,而synchronize可以修飾方法、代碼塊和變量
3、volatile多線程時不會發(fā)生阻塞,而synchronize會阻塞線程
4、volatile可以保證可見性和有序性(禁止指令重排),無法保證原子性,而synchronize都可以保證
總結:volatile就是保證變量對其他線程的可見性和防止指令重排序
而synchronize解決多個線程訪問資源的同步性
鎖狀態(tài)
鎖的狀態(tài)總共有四種:
無鎖狀態(tài)(01)、偏向鎖(01)、輕量級鎖(00)和重量級鎖(10)。
隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現(xiàn)鎖的降級)
CAS和原子類
CAS即Compare and Swap,是一種樂觀鎖的思想。為了保證該變量的可見性,需要使用volatile修飾,結合CAS和volatile可以實現(xiàn)無鎖并發(fā),適用于競爭不激烈、多核CPU的場景。
原子操作類:AtomicInteger、AtomicBoolean,底層采用CAS+volatile實現(xiàn)。
Monitor
每一個java對象都會關聯(lián)一個monitor對象,monitor對象的主要組成部分:waitSet(之前獲得過鎖的線程,條件不滿足后會進入),EntryList(線程等待隊列),Owner(鎖持有者)。
synchronized重量級鎖
基于悲觀鎖思想,加鎖后關聯(lián)monitor對象,線程進入后成為monitor的owner(即owner指向該線程),當其他線程訪問時會先檢查monitor對象是否空閑,若monitor對象已經被持有,則進入entryList等待,從而實現(xiàn)阻塞。
synchronized輕量級鎖
如果多線程訪問一個對象的時間是錯開的,則可以使用輕量級鎖來優(yōu)化。
加鎖過程:如果加鎖對象為無鎖狀態(tài)(01)時,線程首先會在棧幀中創(chuàng)建一個鎖記錄空間(Lock Record),用于存儲鎖對象的Mark Work,并將使用CAS操作鎖對象的Mark Work更新為指向鎖記錄的指針且將鎖記錄里的owner指針指向鎖對象的Mark Work,此時鎖對象的鎖標志位是00(輕量級鎖),如果更新失敗,jvm首先會檢查鎖對象的Mark work是否指向當前線程的棧幀,是的話就進行鎖重入,繼續(xù)執(zhí)行同步代碼,否則說明多線程競爭鎖,輕量級鎖就要升級為重量級鎖。
解鎖過程:通過CAS操作嘗試把線程中的Mark word 替換當前對象的Mark word,若操作成功則解鎖完成,否則說明鎖已經膨脹為重量級鎖,那就要在釋放鎖的同時喚醒被掛起的線程。
鎖膨脹
鎖膨脹是指輕量級鎖在出現(xiàn)競爭的情況下,當線程1持有輕量鎖時,線程2過來競爭的時候,輕量鎖會膨脹為重量鎖,線程2會進入等待隊列(EntryList)。
鎖自旋
重量級鎖競爭的時候,不一定馬上進入阻塞,還可以使用自旋來進行優(yōu)化,如果當前線程自旋成功,就避免了阻塞。
- java6之后自旋鎖是自適應的,若自旋成功過,下次就會多自旋幾次;反之,就少自旋或不自旋
- 自旋會占用CPU時間,單核CPU自旋就是浪費,多核CPU才能發(fā)揮優(yōu)勢
- java7之后不能控制是否開啟自旋功能
偏向鎖
引入偏向鎖是為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執(zhí)行路徑(即減少鎖重入),因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(由于一旦出現(xiàn)多線程競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的性能損耗必須小于節(jié)省下來的CAS原子指令的性能消耗)。
偏向鎖:只有第一次使用CAS時將線程ID設置到對象的Mark Work頭,之后發(fā)現(xiàn)這個線程ID是自己的就表示沒有競爭,不用重新CAS。
一個對象創(chuàng)建時默認開啟偏向鎖,但該默認是延遲的(可通過VM參數(shù)BiasedLockingStartupDelay=0來禁用延遲),不會在程序啟動時立即生效。
- 輕量級鎖是為了在線程交替執(zhí)行同步塊時提高性能,而偏向鎖則是在只有一個線程執(zhí)行同步塊時進一步提高性能。
偏向鎖撤銷
- 對象調用hashCode()方法會禁用該對象的偏向鎖,原因就是調用了hashCode()方法,對象頭就沒有地方存放線程id了,只能禁用該對象的偏向鎖。重量級鎖在monitor對象中存儲hashCode。
- 當兩個及以上線程使用同一個對象時,偏向鎖將會升級為輕量級鎖,如果這些線程會產生資源競爭,則進一步升級為重量級鎖。
- 對象調用wait/notify,也會撤銷對象的偏向狀態(tài),原因是只有重量級鎖才會有wait/notify機制
- 連續(xù)撤銷偏向超過40 次(超過閾值),jvm會認為確實偏向錯了,于是所有類都不可偏向,新建的對象也不可以偏向
批量重偏向
如果對象雖然被多個線程訪問,但沒有競爭,這時偏向了線程 T1 的對象仍有機會重新偏向 T2,重偏向會重置對象的 Thread ID;當撤銷偏向鎖達到閾值 20 次后,jvm 會這樣覺得,我是不是偏向錯了呢,于是會在給這些對象加鎖時重新偏向至t2。因為前19次是輕量,釋放之后為無鎖不可偏向,但是20次后面的是偏向t2,釋放之后依然是偏向t2。
鎖消除
JIT即時編譯器對字節(jié)碼做的優(yōu)化,當判斷加鎖對象線程安全時,會進行鎖消除。
鎖粗化
JIT即時編譯器對字節(jié)碼做的優(yōu)化,當發(fā)現(xiàn)相鄰的synchronize塊使用的是同一個鎖對象,那么就會把這幾個synchronize塊合并為一個加大的同步塊,避免頻繁申請和釋放鎖。