并發編程的三大特性:可見性、原子性、有序性
volatile保證可見性與部分有序性,但是不保證原子性,保證原子性需要借助synchronized這樣的鎖機制
volatile(最底層:lock add)
●保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的,volatile關鍵字會強制將修改的值立即寫入主存,
●禁止進行指令重排序
volatile的可見性
Java記憶體模型 JMM
Java記憶體模型,是java虛擬機規范中所定義的?種記憶體模型,Java記憶體模型是標準化的,屏蔽掉了底層不同計算機的區別( 注意這個跟JVM完全不是?個東?))
描述了Java程式中各種變數(執行緒共享變數)的訪問規則,以及在JVM中將變數,存盤到記憶體和從記憶體中讀取變數這樣的底層細節,
JMM有以下規定:
所有的共享變數都存盤于主記憶體,這?所說的變數指的是實體變數和類變數,不包含區域變數,因為區域變數是執行緒私有的,因此不存在競爭問題,
每?個執行緒還存在??的?作記憶體,執行緒的?作記憶體,保留了被執行緒使?的變數的?作副本,
執行緒對變數的所有的操作(讀,取)都必須在?作記憶體中完成,?不能直接讀寫主記憶體中的變數 ,
不同執行緒之間也不能直接訪問對??作記憶體中的變數,執行緒間變數的值的傳遞需要通過主記憶體中轉來完成,
正是因為這樣的機制,才導致了可?性問題的存在
volatile可?性是有指令原?性保證的,在jmm中定義了8類原?性指令,?如write,store,read,load,?volatile就要求write-store,load-read成為?個原?性操作,這樣?可以確保在讀取的時候都是從主記憶體讀?,寫?的時候會同步到主記憶體中(準確來說也是記憶體屏障)
JMM快取不一致問題
●鎖總線
CPU從主記憶體讀取資料到高速快取,會在總線對這個資料加鎖,這樣其他CPU就沒辦法去讀寫這個資料,直到這個CPU使用完資料釋放鎖之后,其他CPU才可使用(效率過低)
●MESI快取一致性協議
多個CPU從主記憶體讀取同一個資料到各自的高速快取中,當其中某個CPU修改了快取里面的資料,該資料就會馬士同步回主記憶體,其他CPU通過總線嗅探機制可以感知到資料的變化從而將自己快取的資料設為失效并且重新讀取最新資料
嗅探
每個處理器通過嗅探在總線上傳播的資料來檢查??快取的值是不是過期了,當處理器發現??快取?對應的記憶體地址被修改,就會將當前處理器的快取?設定成?效狀態,當處理器對這個資料進?修改操作的時候,會重新從系統記憶體中把資料讀到處理器快取?,
總線?暴
由于Volatile的MESI快取?致性協議,需要不斷的從主記憶體嗅探和cas不斷回圈,?效互動會導致總線帶寬達到峰值,
所以不要?量使?Volatile,?于什么時候去使?Volatile什么時候使?鎖,根據場景區分,
volatile快取可見性實作原理(最底層lock add)
底層實作主要是通過匯編lock前綴指令(lock add),它會鎖定這塊記憶體區域的快取(快取行鎖定)并寫回到主記憶體,
?會將當前處理器快取行的資料立即寫回到系統記憶體
?這個寫回到記憶體的操作會引起其他CPU里快取了該記憶體地址的資料無效(MESI快取一致性協議)
禁?指令重排序
什么是重排序?
為了提?性能,編譯器和處理器常常會對既定的代碼執?順序進?指令重排序,
源代碼–>編譯器優化的重排–> 指令并行也可能會重排–> 記憶體系統也會重排—> 執行
?般重排序可以分為如下三種:
編譯器優化的重排序,編譯器在不改變單執行緒程式語意的前提下,可以重新安排陳述句的執?順序;
指令級并?的重排序,現代處理器采?了指令級并?技術來將多條指令重疊執?,如果不存在資料依賴性,處理器可以改變陳述句對應機器指令的執?順序;
記憶體系統的重排序,由于處理器使?快取和讀/寫緩沖區,這使得加載和存盤操作看上去可能是在亂序執?的,
as-if-serial有序性的保障
不管怎么重排序,單執行緒下的執?結果不能被改變,
編譯器、runtime和處理器都必須遵守as-if-serial語意,
volatile如何保證執行緒間可?和避免指令重排
volatile可?性是有指令原?性保證的,在jmm中定義了8類原?性指令,?如write,store,read,load,?volatile就要求write-store,load-read成為?個原?性操作,這樣?可以確保在讀取的時候都是從主記憶體讀?,寫?的時候會同步到主記憶體中(準確來說也是記憶體屏障),指令重排則是由記憶體屏障來保證的,由兩個記憶體屏障:
??個是編譯器屏障:阻?編譯器重排,保證編譯程式時在優化屏障之前的指令不會在優化屏障之后執?,
?第?個是cpu屏障:sfence保證寫?,lfence保證讀取,lock類似于鎖的?式,java多執?了?個“load addl $0x0, (%esp)”操作,這個操作相當于?個lock指令,就是增加?個完全的記憶體屏障指令,
需要注意的是:volatile寫是在前?和后?分別插?記憶體屏障,?volatile讀操作是在后?插?兩個記憶體屏障,

?位元組碼層面:ACC_VOLATILE
?jvm層面

happens-before
如果?個操作執?的結果需要對另?個操作可?,那么這兩個操作之間必須存在happens-before關系,
volatile域規則:對?個volatile域的寫操作,happens-before于任意執行緒后續對這個volatile域的讀,
如果現在我的變了falg變成了false,那么后?的那個操作,?定要知道我變了,
聊了這么多,我們要知道Volatile是沒辦法保證原?性的,?定要保證原?性,可以使?其他?法,
為啥要雙重檢查?如果不?Volatile會怎么樣?
物件實際上創建物件要進過如下?個步驟:
?1.分配記憶體空間,
?2.調?構造器,初始化實體,
?3.回傳地址給引?
但由于JVM指令重排序的特性,執行順序有可能變成 1->3->2,那有可能建構式在物件初始化完成前就賦值完成了,在記憶體??開辟了??存盤區域后直接回傳記憶體的引?,這個時候還沒真正的初始化完物件,會導致一個執行緒獲得還沒有初始化的實體,,

什么叫保證部分有序性?
當程式執行到volatile變數的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對后面的操作可見;在其后面的操作肯定還沒有進行;
x=2 //陳述句1
y=0 //陳述句2
flag=true //陳述句3
x=4 //陳述句4
y=-1 //陳述句5
由于flag變數為volatile變數,那么在進行指令重排序的程序的時候,不會將陳述句3放到陳述句1、陳述句2前面,也不會講陳述句3放到陳述句4、陳述句5后面,但是要注意陳述句1和陳述句2的順序、陳述句4和陳述句5的順序是不作任何保證的,
使用 Volatile 一般用于 狀態標記量 和 單例模式的雙檢鎖
volatile與synchronized的區別
volatile只能修飾實體變數和類變數,?synchronized可以修飾?法,以及代碼塊,
volatile保證資料的可?性,但是不保證原?性(多執行緒進?寫操作,不保證執行緒安全);?synchronized是?種排他(互斥)的機制, volatile?于禁?指令重排序:可以解決單例雙重檢查物件初始化代碼執?亂序問題,
volatile可以看做是輕量版的synchronized,volatile不保證原?性,但是如果是對?個共享變數進?多個執行緒的賦值,?沒有其他的操作,那么就可以?volatile來代替synchronized,因為賦值本身是有原?性的,?volatile?保證了可?性,所以就可以保證執行緒安全了,
Volatile總結
- volatile修飾符適?于以下場景:某個屬性被多個執行緒共享,其中有?個執行緒修改了此屬性,其他執行緒可以?即得到修改后的值,?如booleanflag;或者作為觸發器,實作輕量級同步,
- volatile屬性的讀寫操作都是?鎖的,它不能替代synchronized,因為它沒有提供原?性和互斥性,因為?鎖,不需要花費時間在獲取鎖和釋放鎖_上,所以說它是低成本的,
- volatile只能作?于屬性,我們?volatile修飾屬性,這樣compilers就不會對這個屬性做指令重排序,
- volatile提供了可?性,任何?個執行緒對其的修改將??對其他執行緒可?,volatile屬性不會被執行緒緩
存,始終從主 存中讀取, - volatile提供了happens-before保證,對volatile變數v的寫?happens-before所有其他執行緒后續對v的讀操作,
- volatile可以使得long和double的賦值是原?的,
- volatile可以在單例雙重檢查中實作可?性和禁?指令重排序,從?保證安全性,
volatile擴展
●原子操作類AtomicInteger
對于a++的操作,其實可以分解為3個步驟,
(1)從主存中讀取a的值
(2)對a進行加1操作
(3)把a重新重繪到主存
這三個步驟在單執行緒中一點問題都沒有,但是到了多執行緒就出現了問題了,比如說有的執行緒已經把a進行了加1操作,但是還沒來得及重新刷入到主存,其他的執行緒就重新讀取了舊值,因為才造成了錯誤,如何去解決呢?方法當然很多,但是為了和我們今天的主題對應上,很自然的聯想到使用AtomicInteger
●Unsafe.compareAndSwapInt()方法,即Unsafe類的CAS操作
CAS 即比較并替換,實作并發演算法時常用到的一種技術,CAS操作包含三個運算元——記憶體位置、預期原值及新值,執行CAS操作的時候,將記憶體位置的值與預期原值比較,如果相匹配,那么處理器會自動將該位置值更新為新值,否則,處理器不做任何操作,
●在高并發情況下,LongAdder(累加器)比AtomicLong原子操作效率更高
在高度并發競爭情形下,AtomicLong每次進行add都需要flush和refresh(這一塊涉及到java記憶體模型中的作業記憶體和主記憶體的,所有變數操作只能在作業記憶體中進行,然后寫回主記憶體,其它執行緒再次讀取新值),每次add()都需要同步,在高并發時會有比較多沖突,比較耗時導致效率低;而LongAdder中每個執行緒會維護自己的一個計數器,在最后執行LongAdder.sum()方法時候才需要同步,把所有計數器全部加起來,不需要flush和refresh操作,
synchronized原子性
synchronized關鍵字解決的是多個執行緒之間訪問資源的同步性,synchronized關鍵字可以保證被它修飾的方法或者代碼塊在任意時刻只能有一個執行緒執行,
另外,在 Java 早期版本中,synchronized屬于重量級鎖,效率低下,因為監視器鎖(monitor)是依賴于底層的作業系統的 Mutex Lock 來實作的,Java 的執行緒是映射到作業系統的原生執行緒之上的,如果要掛起或者喚醒一個執行緒,都需要作業系統幫忙完成,而作業系統實作執行緒之間的切換時需要從用戶態轉換到內核態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是為什么早期的synchronized 效率低的原因,慶幸的是在 Java 6 之后 Java 官方對從 JVM 層面對synchronized 較大優化,所以現在的 synchronized 鎖效率也優化得很不錯了,JDK1.6對鎖的實作引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷,
synchronized三種使用方式:
?修飾實體方法: 作用于當前物件實體加鎖,進入同步代碼前要獲得當前物件實體的鎖
?修飾靜態方法: 也就是給當前類加鎖,會作用于類的所有物件實體,因為靜態成員不屬于任何一個實體物件,是類成員( static 表明這是該類的一個靜態資源,不管new了多少個物件,只有一份),所以如果一個執行緒A呼叫一個實體物件的非靜態 synchronized 方法,而執行緒B需要呼叫這個實體物件所屬類的靜態 synchronized 方法,是允許的,不會發生互斥現象,因為訪問靜態 synchronized 方法占用的鎖是當前類的鎖,而訪問非靜態 synchronized 方法占用的鎖是當前實體物件鎖,
?修飾代碼塊: 指定加鎖物件,對給定物件加鎖,進入同步代碼庫前要獲得給定物件的鎖,
總結: synchronized 關鍵字加到 static 靜態方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖,synchronized 關鍵字加到實體方法上是給物件實體上鎖,盡量不要使用 synchronized(String a) 因 為JVM中,字串常量池具有快取功能!
synchronized 關鍵字的底層原理
synchronized 關鍵字底層原理屬于 JVM 層面
① synchronized 同步陳述句塊的情況–同步代碼
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代碼塊");
}
}
}

synchronized 同步陳述句塊的實作使用的是 monitorenter 和 monitorexit 指令,其中monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結束位置,當執行 monitorenter 指令時,執行緒試圖獲取鎖也就是獲取 monitor(monitor物件存在于每個Java物件的物件頭中,synchronized 鎖便是通過這種方式獲取鎖的,也是為什么Java中任意物件可以作為鎖的原因) 的持有權.當計數器為0則可以成功獲取,獲取后將鎖計數器設為1也就是加1,相應的在執行monitorexit 指令后,將鎖計數器設為0,表明鎖被釋放,如果獲取物件鎖失敗,那當前執行緒就要阻塞等待,直到鎖被另外一個執行緒釋放為止,
②synchronized修飾方法的的情況–同步方法
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實是ACC_SYNCHRONIZED 標識,該標識指明了該方法是一個同步方法,JVM 通過該 ACC_SYNCHRONIZED訪問標志來辨別一個方法是否宣告為同步方法,從而執行相應的同步呼叫,
ACC_SYNCHRONIZED會去隱式調?剛才的兩個指令:monitorenter和monitorexit,所以歸根究底,還是monitor物件的爭奪,
CAS(Compare And Swap ?較并且替換)
CAS是樂觀鎖的?種實作?式,是?種輕量級鎖,JUC 中很多?具類的實作就是基于 CAS 的,
程序:記憶體值V,舊的預期值A,要修改的新值B,當A=V時,將記憶體值修改為B,否則什么都不做;
執行緒在讀取資料時不進?加鎖,在準備寫回資料時,先去查詢原值,操作的時候?較原值是否修改,若未被其他執行緒修改則寫回,若已被修改,則重新執?讀取流程,

CAS問題
?ABA問題(貍貓換太子)–>版本號時間戳
在cas(1,2)還沒完成之前,cas(1,3)與cas(3,1)已經完成,但cas(1,2)看到A的值還是1,不知道它已經經歷過值為3,(如:分手復合中間她還有過一次傷心的經歷)
?回圈時間?開銷?的問題:
是因為CAS操作?時間不成功的話,會導致?直?旋,相當于死回圈了,CPU的壓?會很?,
?只能保證?個共享變數的原?操作:
CAS操作單個共享變數的時候可以保證原?的操作,多個變數就不?了,JDK 5之后 AtomicReference可以?來保證物件之間的原?性,就可以把多個物件放?CAS中操作,
CAS在java中的應?:
(1):Atomic系列
CAS保證原子性問題

鎖的四種狀態–鎖升級
無鎖(new)->偏向鎖->輕量級鎖->重量級鎖
●無鎖(new)
●偏向鎖 會偏向第?個訪問鎖的執行緒,當?個執行緒訪問同步代碼塊獲得鎖時,會在物件頭和堆疊幀記錄?存盤鎖偏向的執行緒ID,當這個執行緒再次進?同步代碼塊時,就不需要CAS操作來加鎖了,只要測驗?下物件頭?是否存盤著指向當前執行緒的偏向鎖 如果偏向鎖未啟動,new出的物件是普通物件(即?鎖,有稍微競爭會成輕量級鎖),如果啟動,new出的物件是匿名偏向(偏向鎖) 物件頭主要包括兩部分資料:Mark Word(標記欄位, 存盤物件?身的運?時資料)、class Pointer(型別指標, 是物件指向它的類元資料的指標)
●輕量級鎖(?旋鎖)–忙等待
lock cmpxchg,cmpxchg作用就是CAS這個函式的作用,比較并交換運算元,這就是CAS原子操作,輕量級鎖是通過CAS來避免進?開銷較?的互斥操作
(1):在把執行緒進?阻塞操作之前先讓執行緒?旋等待?段時間,可能在等待期間其他執行緒已經 解鎖,這時就?需再讓執行緒執?阻塞操作,避免了?戶態到內核態的切換,(?適應?旋時間為?個執行緒上下?切換的時間)
(2):在??旋鎖時有可能造成死鎖,當遞回調?時有可能造成死鎖
(3):?旋鎖底層是通過指向執行緒堆疊中Lock Record的指標來實作的
輕量級鎖與偏向鎖的區別
(1):輕量級鎖是通過CAS來避免進?開銷較?的互斥操作
(2):偏向鎖是在?競爭場景下完全消除同步,連CAS也不執?
?旋鎖升級到重量級鎖條件
(1):某執行緒?旋次數超過10次;
(2):等待的?旋執行緒超過了系統core數的?半;
●重量級鎖–等待佇列–由作業系統完成的
Atomic::cmpxchg_ptr,Atomic::inc_ptr等內核函式,對應
的執行緒就是park()和upark(),作業系統完成的這個操作涉及?戶態和內核態的轉換了,這種切換是很耗資源的(一次80中斷操作),

公平鎖與分公平鎖
(1):公平鎖指在分配鎖前檢查是否有執行緒在排隊等待獲取該鎖,優先分配排隊時間最?的執行緒,?公平直接嘗試獲取鎖
(2):公平鎖需多維護?個鎖執行緒佇列,效率低;默認?公平
獨占鎖與共享鎖
(1):ReentrantLock為獨占鎖(悲觀加鎖策略) (2):ReentrantReadWriteLock中讀鎖為共享鎖
(3): JDK1.8 郵戳鎖(StampedLock), 不可重?鎖 讀的程序中也允許獲取寫鎖后寫?!這樣?來,我們讀的資料就可能不?致,所以,需要?點額外的代碼來判斷讀的程序中是否有寫?,這種讀鎖是?種樂觀鎖, 樂觀鎖的并發效率更?,但?旦有?概率的寫?導致讀取的資料不?致,需要能檢測出來,再讀?遍就?
?戶態和內核態
Linux系統的體系結構,分為?戶空間(應?程式的活動空間)和內核,
我們所有的程式都在?戶空間運?,進??戶運?狀態也就是(?戶態),但是很多操作可能涉及內核運?,?我I/O,我們就會進?內核運?狀態(內核態),
?synchronized還是Lock呢?
我們先看看他們的區別:
synchronized是關鍵字,是JVM層?的底層啥都幫我們做了,?Lock是?個接?,是JDK層?的有豐富的API,
?synchronized會?動釋放鎖,?Lock必須?動釋放鎖,
?synchronized是不可中斷的,Lock可以中斷也可以不中斷,
?通過Lock可以知道執行緒有沒有拿到鎖,?synchronized不能,
?synchronized能鎖住?法和代碼塊,?Lock只能鎖住代碼塊,
?Lock可以使?讀鎖提?多執行緒讀效率,
?synchronized是?公平鎖,ReentrantLock可以控制是否是公平鎖,
?兩者?個是JDK層?的?個是JVM層?的,我覺得最?的區別其實在,我們是否需要豐富的api,還有?個我們的場景,

?如我現在是滴滴,我早上有打??峰,我代碼使?了?量的synchronized,有什么問題?鎖升級程序是不可逆的,過了?峰我們還是重量級的鎖,那效率是不是?打折扣了?這個時候你?Lock是不是很好?
Java中synchronized 和 ReentrantLock 有什么不同?
●相似點:
這兩種同步方式有很多相似之處,它們都是加鎖方式同步,而且都是阻塞式的同步,也就是說當如果一個執行緒獲得了物件鎖,進入了同步塊,其他訪問該同步塊的執行緒都必須阻塞在同步塊外面等待,而進行執行緒阻塞和喚醒的代價是比較高的.
●區別:
這兩種方式最大區別就是對于Synchronized來說,它是java語言的關鍵字,是原生語法層面的互斥,需要jvm實作,而ReentrantLock它是JDK 1.5之后提供的API層面的互斥鎖,需要lock()和unlock()方法配合try/finally陳述句塊來完成,
Synchronized進過編譯,會在同步塊的前后分別形成monitorenter和monitorexit這個兩個位元組碼指令,在執行monitorenter指令時,首先要嘗試獲取物件鎖,如果這個物件沒被鎖定,或者當前執行緒已經擁有了那個物件鎖,把鎖的計算器加1,相應的,在執行monitorexit指令時會將鎖計算器就減1,當計算器為0時,鎖就被釋放了,如果獲取物件鎖失敗,那當前執行緒就要阻塞,直到物件鎖被另一個執行緒釋放為止,
由于ReentrantLock是java.util.concurrent包下提供的一套互斥鎖,相比Synchronized,ReentrantLock類提供了一些高級功能,主要有以下3項:
1.等待可中斷,持有鎖的執行緒長期不釋放的時候,正在等待的執行緒可以選擇放棄等待,這相當于Synchronized來說可以避免出現死鎖的情況,
2.公平鎖,多個執行緒等待同一個鎖時,必須按照申請鎖的時間順序獲得鎖,Synchronized鎖非公平鎖,ReentrantLock默認的建構式是創建的非公平鎖,可以通過引數true設為公平鎖,但公平鎖表現的性能不是很好,
3.鎖系結多個條件,一個ReentrantLock物件可以同時系結對個物件
SynchronizedMap和ConcurrentHashMap有什么區別?
SynchronizedMap()和Hashtable一樣,實作上在呼叫map所有方法時,都對整個map進行同步,而ConcurrentHashMap的實作卻更加精細,它對map中的所有桶加了鎖,所以,只要有一個執行緒訪問map,其他執行緒就無法進入map,而如果一個執行緒在訪問ConcurrentHashMap某個桶時,其他執行緒,仍然可以對map執行某些操作,
所以,ConcurrentHashMap在性能以及安全性方面,明顯比Collections.synchronizedMap()更加有優勢,同時,同步操作精確控制到桶,這樣,即使在遍歷map時,如果其他執行緒試圖對map進行資料修改,也不會拋出ConcurrentModificationException,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/4675.html
標籤:其他
上一篇:帶你通俗理解HTTPS
