原子性
Java 的原子性就和數據庫事物的原子性差不多,一個操作中要麼全部執行成功或者失敗。
JMM 只是保證了基本的原子性,但類似於 i++ 之類的操作,看似是原子操作,其實裡面涉及到:
- 獲取 i 的值。
- 自增。
- 再賦值給 i。
這三步操作,所以想要實現 i++ 這樣的原子操作就需要用到 synchronize 或者是 lock 進行加鎖處理。
如果是基礎類的自增操作可以使用 AtomicInteger 這樣的原子類來實現(其本質是利用了 CPU 級別的 的 CAS 指令來完成的)。
其中用的最多的方法就是: incrementAndGet() 以原子的方式自增。
源碼如下:
首先是獲得當前的值,然後自增 +1。接著則是最核心的 compareAndSet() 來進行原子更新。
其邏輯就是判斷當前的值是否被更新過,是否等於 current,如果等於就說明沒有更新過然後將當前的值更新為 next,如果不等於則返回false 進入循環,直到更新成功為止。
還有其中的 get() 方法也很關鍵,返回的是當前的值,當前值用了 volatile 關鍵詞修飾,保證了內存可見性。
可見性
現代計算機中,由於 CPU 直接從主內存中讀取數據的效率不高,所以都會對應的 CPU 高速緩存,先將主內存中的數據讀取到緩存中,線程修改數據之後首先更新到緩存,之後才會更新到主內存。如果此時還沒有將數據更新到主內存其他的線程此時來讀取就是修改之前的數據。
如上圖所示。
volatile 關鍵字就是用於保證內存可見性,當線程A更新了 volatile 修飾的變量時,它會立即刷新到主線程,並且將其餘緩存中該變量的值清空,導致其餘線程只能去主內存讀取最新值。
使用 volatile 關鍵詞修飾的變量每次讀取都會得到最新的數據,不管哪個線程對這個變量的修改都會立即刷新到主內存。
synchronize和加鎖也能能保證可見性,實現原理就是在釋放鎖之前其餘線程是訪問不到這個共享變量的。但是和 volatile 相比開銷較大。
順序性
以下這段代碼:
正常情況下的執行順序應該是 1>>2>>3。但是有時 JVM 為了提高整體的效率會進行指令重排導致執行的順序可能是 2>>1>>3。但是 JVM 也不能是什麼都進行重排,是在保證最終結果和代碼順序執行結果一致的情況下才可能進行重排。
重排在單線程中不會出現問題,但在多線程中會出現數據不一致的問題。
Java 中可以使用 volatile 來保證順序性,synchronize 和 lock 也可以來保證有序性,和保證原子性的方式一樣,通過同一段時間只能一個線程訪問來實現的。
除了通過 volatile 關鍵字顯式的保證順序之外, JVM 還通過 happen-before 原則來隱式的保證順序性。
其中有一條就是適用於 volatile 關鍵字的,針對於 volatile 關鍵字的寫操作肯定是在讀操作之前,也就是說讀取的值肯定是最新的。
volatile 的應用
雙重檢查鎖的單例模式
可以用 volatile 實現一個雙重檢查鎖的單例模式:
這裡的 volatile 關鍵字主要是為了防止指令重排。
如果不用 volatile ,singleton = new Singleton();,這段代碼其實是分為三步:
- 分配內存空間。(1)
- 初始化對象。(2)
- 將 singleton 對象指向分配的內存地址。(3)
加上 volatile 是為了讓以上的三步操作順序執行,反之有可能第二步在第三步之前被執行就有可能某個線程拿到的單例對象是還沒有初始化的,以致於報錯。
控制停止線程的標記
這裡如果沒有用 volatile 來修飾 flag ,就有可能其中一個線程調用了 stop()方法修改了 flag 的值並不會立即刷新到主內存中,導致這個循環並不會立即停止。
這裡主要利用的是 volatile 的內存可見性。
總結一下:
- volatile 關鍵字只能保證可見性,順序性, 不能保證原子性。
關注小編不迷路,今後會分析更多技術文章!
閱讀更多 Coder生活 的文章