Java中的volatile關鍵字

Java併發中的可見性與原子性

Java併發是一個十分重要的知識點,然而我並不會(…..),慢慢上手吧,今天來看一看這個volatile

可見性

可見性是指線程之間的可見性,也就是一個線程修改的結果對另一個線程是可見的。使用volatile修飾的變量就會具有可見性。但需要注意的是volatile只能保證被修飾的內容具有可見性,而不能保證具有原子性(單個volatile變量讀寫是原子性的),因而就會存在線程安全問題

原子性

原子是不可分割的,因此原子操作也是指某些操作是連續的不可分割的(操作系統中有詳細的解釋)。非原子操作會存在線程安全問題,而加上synchronized關鍵字後就會使操作變成原子操作

// ......
int a = 0;
a = a + 1;
// ......

這麼一個簡單的過程,CPU在運行的時候會先讀取a的值,然後相加計算的結果會再賦值給a;這時候如果是多個線程在工作,那麼在賦值操作前CPU讀取的值到底是0還是1呢?(多個線程同時工作,無法得知哪個線程在CPU執行的先後順序,此時使用的a值說不定就是彼時計算後的a值)

重排序-Java多線程

再看下面的內容之前,先要看一看這個重排序:指令重排序,是指編譯器或程序運行時環境爲了優化程序性能而採取的對指令重新排序執行的一種手段
簡單的說,兩條語句在執行時,處於優化的原因,誰先執行誰後不一定

synchronized和volatile

爲了解決線程併發的問題,Java引入了同步快synchronized和volatile關鍵字機制

  • synchronized關鍵字:被synchronized修飾的塊結構在多線程訪問時,同一時刻只能有一個線程能有訪問的到塊內容
  • volatile關鍵字:volatile修飾的變量,線程在每次訪問的時候,都會讀取變量最後一次修改的值

注意

  • synchronized保證了原子性,但仍不代表線程安全
  • 如果一定要保證線程安全,可以使用重入鎖ReentrantLock

volatile原理

再來仔細探討一下volatile深入的原理,這是一種相對較弱的同步機制,能夠確保使變量的更新對其他線程是可見的。被volatile聲明的變量,編譯器與運行時的環境都會注意到這是一個共享的變量,因此不會將該變量上的操作與其他內容操作一起重排序。這是因爲volatile變量不會被緩存在寄存器或者其它對處理器不可見的地方,因此每次訪問volatile變量都會返回最新更新的值。

處理器在訪問volatile變量時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變量是一種比synchronized關鍵字更爲輕量級(稍弱)的同步機制

先看一下普通狀態下的線程工作的內存變化

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
這裏寫圖片描述
JVM在運行時會對不同的線程分配自己的線程棧(線程內存),線程棧保存了線程運行時變量的信息。當線程想訪問一個對象的值的時候,會進行如下的操作:
- 首先,通過對象的引用找到對應在堆內存中變量的值
- 然後把該值load到本地線程內存中,建立一個變量的副本,之後線程就不在和對內存中變量的值有任何關係,而是直接修改副本中的值
- 在修改完副本變量值後,在線程完全退出前,會自動把線程副本變量的值寫回到對象在堆中的值,這樣堆中變量的值就發生了變化

如上圖所示,取副本中的值(use)和寫到副本中(asign)可以多次出現。重要的是,上圖中的操作並不是原子性的,就是說當線程read和load後,如果主內存中變量的值發生了改變,線程無從得知,進而導致最後計算出的結果並不是我們預想中的。

回過頭來看一下volatile的原理

還是上圖,當使用volatile修飾後,JVM**只會**保證從主存加載到線程棧中的變量的值是最新的,這已經可以解釋了volatile是如何使處理器總是使用到最新的變量值(依靠上圖中藍色的雙向箭頭)。

但是,注意但是,凡事都有個意外,volatile也會引發併發取值不一致的情況,原因在這裏:
- 假設有一個線程1和一個線程2,兩個線程都會取number變量的值,計算,並寫回主存
- 先是線程1,read和load並計算寫回後,number的值發生了變化
- 再是線程2,當線程2read和load時,可能會是線程1寫回並更新之後的number的新值,當線程2計算並寫回後,這個number的值還是我們想要的值嘛?

總結普通狀態與加了volatile關鍵字的對比

簡單的說,普通狀態下,每個線程先從內存拷貝變量值到CPU緩存中(線程工作內存)。當有多個CPU工作時,每個線程可能在不同的CPU上被處理,也就是說,不同的線程使用的變量值都是來自不同的CPU緩存的

volatile生命的變量就保證了JVM每次讀變量都從主存中讀取,跳過了CPU緩存這一步

加了volatile關鍵詞後帶來的特性

  • 一就是可見性了,因爲線程都是從主存讀取數據,相當於線程利用主存傳遞數據
  • 二就是禁止了指令重排序,查看網上的博客,發現了這麼一句指令代碼load addl $0x0,(%esp),這是彙編指令,該操作相當於是一個內存屏障,作用是指令重排序時不能把屏障之後的指令排到屏障之前的位置

日常O_O

寫這個Blog主要還是被筆試題虐了,關於JVM內存處理機制還是處於比較懵懂的狀態,後面買了書再慢慢填坑
PS:今天心情爆炸不爽,服。自己還是先狗後人吧。:-)

發佈了51 篇原創文章 · 獲贊 89 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章