原子變量最主要的一個特點就是所有的操作都是原子的,synchronized關鍵字也可以做到對變量的原子操作。只是synchronized的成本相對較高,需要獲取鎖對象,釋放鎖對象,如果不能獲取到鎖,還需要阻塞在阻塞隊列上進行等待。而如果單單只是爲了解決對變量的原子操作,建議使用原子變量。關於原子變量的介紹,主要涉及以下內容:

  • 原子變量的基本概念
  • 通過AtomicInteger瞭解原子變量的基本使用
  • 通過AtomicInteger瞭解原子變量的基本原理
  • AtomicReference的基本使用
  • 使用FieldUpdater操作非原子變量的字段屬性
  • 經典的ABA問題的解決

一、原子變量的基本概念

原子變量保證了該變量的所有操作都是原子的,不會因爲多線程的同時訪問而導致髒數據的讀取問題。我們先看一段synchronized關鍵字保證變量原子性的代碼:

Java併發編程之原子變量

簡單的count++操作,線程對象首先需要獲取到Counter 類實例的對象鎖,然後完成自增操作,最後釋放對象鎖。整個過程中,無論是獲取鎖還是釋放鎖都是相當消耗成本的,一旦不能獲取到鎖,還需要阻塞當前線程等等。

對於這種情況,我們可以將count變量聲明成原子變量,那麼對於count的自增操作都可以以原子的方式進行,就不存在髒數據的讀取了。Java給我們提供了以下幾種原子類型:

  • AtomicInteger和AtomicIntegerArray:基於Integer類型
  • AtomicBoolean:基於Boolean類型
  • AtomicLong和AtomicLongArray:基於Long類型
  • AtomicReference和AtomicReferenceArray:基於引用類型

在本文的餘下內容中,我們將主要介紹AtomicInteger和AtomicReference兩種類型,AtomicBoolean和AtomicLong的使用和內部實現原理幾乎和AtomicInteger一樣。

二、AtomicInteger的基本使用

首先看它的兩個構造函數:

Java併發編程之原子變量

可以看到,我們在通過構造函數構造AtomicInteger原子變量的時候,如果指定一個int的參數,那麼該原子變量的值就會被賦值,否則就是默認的數值0。

也有獲取和設置這個value值的方法:

Java併發編程之原子變量

當然,這兩個方法並不是原子的,所以一般也很少使用,而以下的這些基於原子操作的方法則相對使用的頻繁,至於它們的具體實現是怎樣的,我們將在本文的後續小節中進行簡單的學習。

Java併發編程之原子變量

下面我們實現一個計數器的例子,之前我們使用synchronized實現過,現在我們使用原子變量再次實現該問題。

Java併發編程之原子變量

Java併發編程之原子變量

很顯然,使用原子變量要比使用synchronized要簡潔的多並且效率也相對較高。

三、AtomicInteger的內部基本原理

AtomicInteger的實現原理有點像我們的包裝類,內部主要操作的是value字段,這個字段保存就是原子變量的數值。value字段定義如下:

private volatile int value;

首先value字段被volatile修飾,即不存在內存可見性問題。由於其內部實現原子操作的代碼幾乎類似,我們主要學習下incrementAndGet方法的實現。

在揭露該方法的實現原理之前,我們先看另一個方法:

Java併發編程之原子變量

compareAndSet方法又被稱爲CAS,該方法調用unsave的一個compareAndSwapInt方法,這個方法是native,我們看不到源碼,但是我們需要知道該方法完成的一個目標:比較當前原子變量的值是否等於expect,如果是則將其修改爲update並返回true,否則直接返回false。當然,這個操作本身就是原子的,較爲底層的實現。

在jdk1.7之前,我們的incrementAndGet方法是這樣實現的:

Java併發編程之原子變量

方法體是一個死循環,current獲取到當前原子變量中的值,由於value被修飾volatile,所以不存在內存可見性問題,數據一定是最新的。然後current加一後賦值給next,調用我們的CAS原子操作判斷value是否被別的線程修改過,如果還是原來的值,那麼將next的值賦值給value並返回next,否則重新獲取當前value的值,再次進行判斷,直到操作完成。

incrementAndGet方法的一個很核心的思想是,在加一之前先去看看value的值是多少,真正加的時候再去看一下,如果發現變了,不操作數據,否則爲value加一。

但是在jdk1.8以後,做了一些優化,但是最後還是調用的compareAndSwapInt方法。但基本思想還是沒變。

四、AtomicReference的基本使用

對於一些自定義類或者字符串等這些引用類型,Java併發包也提供了原子變量的接口支持。AtomicReference內部使用泛型來實現的。

Java併發編程之原子變量

有關其他的一些原子方法如下:

Java併發編程之原子變量

AtomicReference中少了一些自增自減的操作,但是對於value的修改依然是原子的。

五、使用FieldUpdater操作非原子變量的字段屬性

FieldUpdater允許我們不必將字段設置爲原子變量,利用反射直接以原子方式操作字段。例如:

Java併發編程之原子變量

然後我們創建一百個線程隨機調用同一個Counter對象的addCount方法,無論運行多少次,結果都是一百。這種方式實現的原子操作,對於被操作的變量不需要被包裝成原子變量,但是卻可以直接以原子方式操作它的數值。

六、經典的ABA問題

我們的原子變量都依賴一個核心的方法,那就是CAS。這個方法最核心的思想就是,更改變量值之前先獲取該變量當前最新的值,然後在實際更改的時候再次獲取該變量的值,如果沒有被修改,那麼進行更改,否則循環上述操作直至更改操作完成。假如一個線程想要對變量count進行修改,實際操作之前獲取count的值爲A,此時來了一個線程將count值修改爲B,又來一個線程獲取count的值爲B並將count修改爲A,此時第一個線程全然不知道count的值已經被修改兩次了,雖然值還是A,但是實際上數據已經是髒的。

這就是典型的ABA問題,一個解決辦法是,對count的每次操作都記錄下當前的一個時間戳,這樣當我們原子操作count之前,不僅查看count的最新數值,還記錄下該count的時間戳,在實際操作的時候,只有在count的數值和時間戳都沒有被更改的情況之下才完成修改操作。

Java併發編程之原子變量

AtomicStampedReference 的CAS方法要求傳入四個參數,該方法的內部會同時比較count和stamp,只有這兩個值都沒有發生改變的前提下,CAS纔會修改count的值。

上述我們介紹了有關原子變量的最基本內容,最後我們比較下原子變量和synchronized關鍵字的區別。

從思維模式上看,原子變量代表一種樂觀的非阻塞式思維,它假定沒有別人會和我同時操作某個變量,於是在實際修改變量的值的之前不會鎖定該變量,但是修改變量的時候是使用CAS進行的,一旦發現衝突,繼續嘗試直到成功修改該變量。

而synchronized關鍵字則是一種悲觀的阻塞式思維,它認爲所有人都會和我同時來操作某個變量,於是在將要操作該變量之前會加鎖來鎖定該變量,進而繼續操作該變量。

相關文章