Java編程思想——多線程的三大核心源碼層解密

筆者曾供職於華為,三星,騰訊,是一個資深碼農,歡迎大家

關注我,歡迎評論轉發

對於Java併發編程,一般來說有以下的關注點:

  • 1.線程安全性,正確性。
  • 2.線程的活躍性(死鎖,活鎖)
  • 3.性能

其中線程的安全性問題是首要解決的問題,線程不安全,運行出來的結果和預期不一致,那就連基本要求都沒達到了。

保證線程的安全性問題,本質上就是保證線程同步,實際上就是線程之間的通信問題。我們知道,在操作系統中線程通信有以下幾種方式:

1.信號量 2.信號 3.管道 4.共享內存 5.消息隊列 6.socket

java中線程通信主要使用共享內存的方式。共享內存的通信方式首先要關注的就是可見性有序性。而原子性操作一般都是必要的,所以主要關注這三個問題。

1、原子性(Atomicity)

原子性是指在一個操作中就是cpu不可以在中途暫停然後再調度,既不被中斷操作,要不執行完成,要不就不執行。

JMM只是保證了基本的原子性,但類似於i++之類的操作,看似是原子操作,其實裡面涉及到:

獲取 i 的值。

自增。

再賦值給 i。

這三步操作,所以想要實現i++這樣的原子操作就需要用到synchronize或者是lock進行加鎖處理。

如果是基礎類的自增操作可以使用AtomicInteger這樣的原子類來實現(其本質是利用了CPU級別的 的CAS指令來完成的)。

其中用的最多的方法就是:incrementAndGet()以原子的方式自增。 源碼如下:

Java編程思想——多線程的三大核心源碼層解密

首先是獲得當前的值,然後自增 +1。接著則是最核心的compareAndSet()來進行原子更新。

Java編程思想——多線程的三大核心源碼層解密

其邏輯就是判斷當前的值是否被更新過,是否等於current,如果等於就說明沒有更新過然後將當前的值更新為next,如果不等於則返回false進入循環,直到更新成功為止。

還有其中的get()方法也很關鍵,返回的是當前的值,當前值用了volatile關鍵詞修飾,保證了內存可見性。

private volatile int value;

2、可見性

可見性就是指當一個線程修改了線程共享變量的值,其它線程能夠立即得知這個修改。

現代計算機中,由於CPU直接從主內存中讀取數據的效率不高,所以都會對應的CPU高速緩存,先將主內存中的數據讀取到緩存中,線程修改數據之後首先更新到緩存,之後才會更新到主內存。如果此時還沒有將數據更新到主內存其他的線程此時來讀取就是修改之前的數據。

Java編程思想——多線程的三大核心源碼層解密

如上圖所示。

volatile關鍵字就是用於保證內存可見性,當線程A更新了 volatile 修飾的變量時,它會立即刷新到主線程,並且將其餘緩存中該變量的值清空,導致其餘線程只能去主內存讀取最新值。

使用volatile關鍵詞修飾的變量每次讀取都會得到最新的數據,不管哪個線程對這個變量的修改都會立即刷新到主內存。

synchronize和加鎖也能能保證可見性,實現原理就是在釋放鎖之前其餘線程是訪問不到這個共享變量的。但是和volatile相比開銷較大。

3、順序性

Java內存模型中的程序天然有序性可以總結為一句話:如果在本線程內觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有操作都是無序的。前半句是指“線程內表現為串行語義”,後半句是指“指令重排序”現象和“工作內存中主內存同步延遲”現象。

Java編程思想——多線程的三大核心源碼層解密

正常情況下的執行順序應該是1>>2>>3。但是有時JVM為了提高整體的效率會進行指令重排導致執行的順序可能是2>>1>>3。但是JVM也不能是什麼都進行重排,是在保證最終結果和代碼順序執行結果一致的情況下才可能進行重排。

重排在單線程中不會出現問題,但在多線程中會出現數據不一致的問題。

Java 中可以使用volatile來保證順序性, 和 lock也可以來保證有序性,和保證原子性的方式一樣,通過同一段時間只能一個線程訪問來實現的。

除了通過volatile關鍵字顯式的保證順序之外,JVM還通過happen-before原則來隱式的保證順序性。

其中有一條就是適用於volatile關鍵字的,針對於volatile關鍵字的寫操作肯定是在讀操作之前,也就是說讀取的值肯定是最新的。

volatile 的應用

雙重檢查鎖的單例模式

可以用volatile實現一個雙重檢查鎖的單例模式:

Java編程思想——多線程的三大核心源碼層解密

這裡的volatile關鍵字主要是為了防止指令重排。 如果不用volatile,singleton = new Singleton();,這段代碼其實是分為三步:

分配內存空間。(1)

初始化對象。(2)

將singleton對象指向分配的內存地址。(3)

加上volatile是為了讓以上的三步操作順序執行,反之有可能第二步在第三步之前被執行就有可能某個線程拿到的單例對象是還沒有初始化的,以致於報錯。

控制停止線程的標記

Java編程思想——多線程的三大核心源碼層解密

這裡如果沒有用 volatile 來修飾 flag ,就有可能其中一個線程調用了stop()方法修改了 flag 的值並不會立即刷新到主內存中,導致這個循環並不會立即停止。

這裡主要利用的是volatile的內存可見性。

總結一下:

volatile關鍵字只能保證可見性,順序性,不能保證原子性。

筆者曾供職於華為,三星,騰訊,是一個資深碼農,歡迎大家《關注》我,歡迎評論轉發

Java編程思想——多線程的三大核心源碼層解密


分享到:


相關文章: