java面試題之volatile和synchronized的使用方法和區別

我們先來看一下Java 內存模型中的可見性、原子性和有序性。

關注公衆號,一起學java

可見性:

可見性,是指線程之間的可見性,一個線程修改的狀態對另一個線程是可見的。

 

原子性:

原子是世界上的最小單位,具有不可分割性。synchronized塊之間的操作就具備原子性。volatile關鍵字定義的變量就可以做到這一點,Java還有兩個關鍵字能實現可見性,即synchronized和final。

 

有序性:

如果在本線程內觀察,所有的操作都是有序的:如果在一個線程中觀察另外一個線程,所有的線程操作都是無序的。前半句是指“線程內表現爲串行的語義”,後半句是指“指令重排序”現象和“工作內存與主內存同步延遲”現象。Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性

 

首先來看一下jvm的內存模型

 

JVM中存在一個主存區(Main Memory或Java Heap Memory),Java中所有變量都是存在主存中的,對於所有線程進行共享,而每個線程又存在自己的工作內存(Working Memory),工作內存中保存的是主存中某些變量的拷貝,線程對所有變量的操作並非發生在主存區,而是發生在工作內存中,而線程之間是不能直接相互訪問,變量在程序中的傳遞,是依賴主存來完成的。

 

我們來模擬一下2個線程在內存中去修改一個變量的過程,線程1對共享變量的修改要想被線程2及時看到,必須要經過如下兩個步驟:

1)把工作內存1中更新過的共享變量刷新到主內存中。

2)將主內存中最新的共享變量的值更新到工作內存2中。

1.首先線程1和線程2都會從主存中拷貝一份變量X的副本到自己的工作內存中。

2.線程1在自己的工作內存中修改變量X的副本。

3.線程1修改自己工作內存中的副本X後,把修改後的X同步到主存中。

4.主存中X的值更新成功後,通知其他線程同步X的值,當然這裏我們只有2個線程,那麼主存的X的值被線程1修改後會同步給線程2。這樣就保證了變量在多個線程中的可見性。

在java中可以實現可見性的兩個關鍵字 

 1)使用關鍵字synchronized

 2)使用關鍵字volatile

 

Synchronized能夠實現多線程的原子性(同步)和可見性。

JVM關於Synchronized的兩條規定:

1)線程解鎖前,必須把共享變量的最新值刷新到主內存中。

2)線程加鎖時,將清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取最新的值(注意:加鎖和解鎖需要同一把鎖)。

 

Synchronized執行互斥代碼的過程

1)獲得互斥鎖

2)清空工作內存

3)從主內存拷貝變量的最新副本到工作內存

4)執行代碼

5)將更改後的共享變量的值刷新到主內存

6)釋放互斥鎖

 

volatile可以保證變量的可見性,但是不能保證複合操作的原子性

關注公衆號,一起學java

volatile如何實現內存可見性?

深入來說:通過加入內存屏障和禁止重排序優化來實現的。

1)對volatile變量執行寫操作時,會在寫操作後加入一條store屏障指令。

2)對volatile變量執行讀操作時,會在讀操作後加入一條load屏障指令。

 

通俗地講:volatile變量在每次被線程訪問時,都強迫從主內存中重讀該變量的值,而當該變量發生變化時,又會強迫線程將最新的值刷新到主內存,這樣任何時刻,不同的線程總能看到該變量的最新值。

 

線程寫volatile變量的過程:

1)改變線程工作內存中volatile變量副本的值。

2)將改變後的副本的值從工作內存刷新到主內存。

 

線程讀volatile變量的過程:

1)從主內存中讀取volatile變量的最新值到線程的工作內存中。

2)從工作內存中讀取volatile變量的副本。

 

volatile不能保證volatile變量複合操作的原子性

對於下面的一段程序的使用volatile和synchronized

private int number = 0;              

number++;//不是原子操作                   

1讀取number的值                      

2將number的值加1                    

3寫入最新的number的值  

//加入synchronized,變爲原子操作   

synchronized(thhis){ 

        number++; 

}

//變爲volatile變量,無法保證原子性

private volatile int number = 0;

 

volatile變量可用於提供線程安全,但是隻能應用於非常有限的一組用例:多個變量之間或者某個變量的當前值與修改後值之間沒有約束。因此,單獨使用 volatile 還不足以實現計數器、互斥鎖或任何具有與多個變量相關的不變式(Invariants)的類(例如 “start <=end”)。

出於簡易性或可伸縮性的考慮,您可能傾向於使用 volatile 變量而不是鎖。當使用 volatile 變量而非鎖時,某些習慣用法(idiom)更加易於編碼和閱讀。此外,volatile 變量不會像鎖那樣造成線程阻塞,因此也很少造成可伸縮性問題。在某些情況下,如果讀操作遠遠大於寫操作,volatile 變量還可以提供優於鎖的性能優勢。

 

volatile適合的使用場景

只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時滿足下面兩個條件:

(1)對變量的寫入操作不依賴其當前值

(2)該變量沒有包含在具有其他變量的不變式中。

第一個條件的限制使 volatile 變量不能用作線程安全計數器。雖然增量操作(x++)看上去類似一個單獨操作,實際上它是一個由(讀取-修改-寫入)操作序列組成的組合操作,必須以原子方式執行,而 volatile 不能提供必須的原子特性。實現正確的操作需要使x 的值在操作期間保持不變,而 volatile 變量無法實現這點。(然而,如果只從單個線程寫入,那麼可以忽略第一個條件。)

 

總結:

1)volatile比synchronized更輕量級。

2)volatile沒有synchronized使用的廣泛。

3)volatile不需要加鎖,比synchronized更輕量級,不會阻塞線程。

4)從內存可見性角度看,volatile讀相當於加鎖,volatile寫相當於解鎖。

5)synchronized既能保證可見性,又能保證原子性,而volatile只能保證可見性,無法保證原子性。

6)volatile本身不保證獲取和設置操作的原子性,僅僅保持修改的可見性。但是java的內存模型保證聲明爲volatile的long和double變量的get和set操作是原子的。

 

注意:

對64位(long、double)變量的讀寫可能不是原子操作

Java內存模型允許JVM將沒有被volatile修飾的64位數據類型的讀寫操作劃分爲兩次32位的讀寫操作來運行。

 

導致問題:有可能會出現讀取到半個變量的情況。

解決方法:加volatile關鍵字。

 

一個問題:即使沒有保證可見性的措施,很多時候共享變量依然能夠在主內存和工作內存間得到及時的更新?

 

        答:一般只有在短時間內高併發的情況下才會出現變量得不到及時更新的情況,因爲CPU在執行時會很快地刷新緩存,所以一般情況下很難看到這種問題。慢了不就不會刷新了。CPU運算快的話,在分配的時間片內就能完成所有工作:工作內從1->主內存->工作內存2,這樣一來就保證了數據的可見性。在這個過程中,假如線程沒有在規定時間內完成工作,然後這個線程就釋放CPU,分配給其它線程,該線程就需要等待CPU下次給該線程分配時間片,如果在這段時間內有別的線程訪問共享變量,可見性就沒法保證了。

關注公衆號,一起學java


 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章