此次文章主要探討volatile與synchronized,通過一些基礎概念的介紹,讓讀者對於兩者有更深的瞭解。 一、幾個相關概念 1、原子性 其本意是“不能被進一步分隔的最小粒子”,而原子操作意為“不可被中斷的一個或一系列操作”。在多處理器重實現原子操作變得有點複雜。 1)操作系統如何實現原子性。 ...
此次文章主要探討volatile與synchronized,通過一些基礎概念的介紹,讓讀者對於兩者有更深的瞭解。
一、幾個相關概念
1、原子性
其本意是“不能被進一步分隔的最小粒子”,而原子操作意為“不可被中斷的一個或一系列操作”。在多處理器重實現原子操作變得有點複雜。
1)操作系統如何實現原子性。
單處理器可以對同一個緩存行里自動進行16/32/64位的原子操作。但是複雜的記憶體操作處理器是不能保證其原子性的,比如跨匯流排寬度、跨多個緩存行和跨頁表的訪問。例如,i++是一個讀改寫的操作,由於該代碼可能被不同的線程執行導致最終出現的結果可能不是我們想要的結果(具體原因不在此贅述)。但是,處理器提供匯流排鎖定和緩存鎖定兩個機制來保證複雜記憶體操作的原子性。
a。使用匯流排鎖保證原子性
處理器通過使用匯流排鎖來解決i++問題。所謂匯流排鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在匯流排上輸出此信號時,其他處理器的請求將被阻塞,此時該處理器可以獨占共用記憶體。
b。使用緩存鎖來保證原子性
匯流排鎖把CPU與記憶體間的通信鎖住了,這使得在鎖定期間,其它處理器不能操作其它記憶體地址的數據,所以匯流排鎖開銷比較大。我們只需要保證對某個記憶體地址的操作是原子性即可(減小鎖粒度)。目前處理器在某些場合下回使用緩存鎖定來代替匯流排鎖來進行優化。
2、可見性
可見性的意思是當一個線程修改一個共用變數時,另一個線程能讀到這個修改的值。
3、指令重排
重排序指編譯器和處理器為了優化程式性能而對指令序列進行重新排序的一種手段。在多線程的程式中,對某些指令的重排序可能會改變程式的執行結果(後續會有實例說明)。
二、volatile
1、volatile變數具有以下特性。
可見性:對一個volatile變數的讀,總是能看到任意線程對這個volatile變數最後的寫入、
禁止重排序:jdk1.5以後對volatile語義進行了加強,不允許volatile變數之間進行重排序。
2、底層實現原理
1)操作系統層面
操作系統可通過LOCK#首碼指令實現以上前兩個特性。為了提高處理速度,處理器不直接和記憶體進行通信,而是先將系統記憶體的數據讀到內部緩存(L1、L2或其它)後再進行操作,但操作完不知道何時寫回到記憶體(操作系統這裡其實使用了非同步操作來解決生產消費速度不均的問題)。如果申明瞭volatile的變數進行寫操作,JVM就會想處理器發送一條Lock首碼指令,將這個變數的緩存行的數據寫回到記憶體中。此時,其它處理器中的值還是舊值。在多處理器下,為了保證各個處理器緩存一致,實現了緩存一致性協議。每個處理器通過嗅探在匯流排上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行過期時,就會將當前處理器的緩存行置為無效狀態,當處理器對這個數據進行修改時,會重新從操作系統記憶體中把數據讀取到處理器緩存中。
a。Lock首碼指令會引起處理器緩存回寫到記憶體。
Lock首碼指令導致在執行指令期間,聲言處理器的Lock信號。在多處理器環境下,處理器可以獨占任何共用記憶體。操作系統通過匯流排鎖或者緩存鎖定,來確保同時只能有一個處理器可修改緩存數據。
b。一個處理器的緩存會寫到記憶體導致其它處理器的緩存無效。
2)JMM層面。
在Java中,所有實例域、靜態域和數組元素都存儲在堆記憶體中,堆記憶體線上程之間共用。局部變數,方法定義參數和異常處理參數不會線上程之間共用,它們不會有記憶體可見性問題。
從抽象的角度來看,JMM定義了線程和主記憶體之間的抽象關係:線程之間的共用變數存儲在主記憶體中,每個線程都有一個私有的本地記憶體(Local Memory),本地記憶體中存儲了改線程共用變數的副本。本地記憶體是JMM的一個抽象概念,並不真實存在。JMM的抽象示意圖如下所示。
當一個變數被申明為volatile時。
寫入操作:JMM會把該線程對應的本地記憶體中的變數刷新到主存中。
讀取操作:JMM會把本地記憶體置為無效,線程接下來會從主存中讀取共用變數。
3)禁止重排序應用
在單例模式中,人們使用了雙重校驗來降低鎖同步的開銷,查看以下無volatile時的代碼。
以上是一個錯誤的優化,當線程執行到第4行時,代碼讀取到instance不為null,但是註意此時instance引用的對象還未初始化。原因如下。
instance = new Singleton()可以分解為如下3行偽代碼。
memory = allocate();//1.分配對象的記憶體空間。
ctorInstance(memory);//2.初始化對象
instance = memory;// 3.設置instance指向剛剛分配的地址
步驟2和3由於指令重排,可能導致另一個線程訪問到未被初始化的對象。如果在instance變數前加上volatile即可解決此問題。
三、synchronized
1、簡單介紹
synchronized簡單的理解就是對象鎖,Java中的每一個對象都可以作為鎖。它主要可以確保代碼一系列操作在同一線程只能由一個線程訪問。
具體表現為一下3種形式。
對於普通同步方法,鎖是當前實例對象。
對於靜態同步方法,鎖是當前的Class對象。
對於同步方法塊,鎖是synchronized括弧里配置的對象。
2、原理介紹。
在Java中任意一個對象都擁有自己的監視器,當這個對象由同步塊或者這個對象的同步方法調用時,執行方法的線程必須先獲取到該對象的監視器才能進入到同步塊或者同步方法中,而沒有獲取到監視器的線程將會被阻塞在同步塊或者同步方法入口處,進入BLOCKED狀態。當訪問訪問Object的前驅(獲得了鎖的線程)釋放了鎖,則會喚醒阻塞在同步隊列中的線程,使其重新嘗試對監視器獲取。具體過程如下圖。
四、synchronized和volatile比較
1、原理分析
從原理上分析,volatile是JMM藉助操作系統底層指令實現的關鍵字。當變數被它修飾時,它可以保證對於該變數的訪問都需要從共用記憶體中獲取,而每次修改則必須同步刷新回共用記憶體,它能保證所有線程對變數訪問的可見性。synchronized是JVM層面的,它藉助Java對象的monitor實現的,同一時刻只能有一個線程進入監視器,它可以保證程式對於變數訪問的可見性和排它性。
為了實現線程間的同步,兩者都是通過匯流排鎖/記憶體鎖定、monitor這樣的方式,即線程在同一時刻只能訪問對應的記憶體區域,這樣就避免了多個線程同時寫入記憶體導致結果無法預知的情況。
2、特性對比
1)volatile
a.可見性:可保證變數在記憶體中的可以性。
b.多數情況下相對synchronized具有較高的性能
c.有序性:在某些情況下可以防止指令重排(經典案例為單例雙重校驗)
2)synchronized
a.性能相對較差,但是1.6以後性能有所提升。
b.有序性,被加鎖的代碼塊同一時刻只能有一個線程訪問程式,保證程式有序執行
五、總結思考
1)為瞭解決多線程間的同步問題核心思想:通過限定同一時刻僅有一個線程有寫入。
2)操作系統通過減小鎖的粒度提升性能。