AtomicInteger的原理
java的並發原子包里面提供了很多可以進行原子操作的類,比如:
- AtomicInteger
- AtomicBoolean
- AtomicLong
- AtomicReference
等等,一共分為四類:原子更新基本類型(3個)、原子更新數組、原子更新引用和原子更新屬性(字段)。、提供這些原子類的目的就是為了解決基本類型操作的非原子性導致在多線程並發情況下引發的問題。那么非原子性的操作會引發什么問題呢?下面我們通過一個示例來看一下。
1. i++引發的問題
我們知道基本類型的賦值操作是原子操作,但是類似這種i++
的操作並不是原子操作,通過反編譯代碼我們可以大致了解此操作分為三個階段:
tp1 = i; //1
tp2 = tp1 + 1; //2
i = tp2; //3
如果有兩個線程m和n要執行i++操作,因為重排序的影響,代碼執行順序可能會發生改變。如果代碼的執行順序是m1 - m2 - m3 - n1 - n2 - n3,那么結果是沒問題的,如果代碼的執行順序是m1 - n1 - m2 - n2 - m3 - n3那么很明顯結果就會出錯。
測試代碼
package com.wangjun.thread;
public class AtomicIntegerTest {
private static int n = 0;
public static void main(String[] args) throws InterruptedException {
//i++引發的線程問題
Thread t1 = new Thread() {
public void run() {
for(int i = 0; i < 1000; i++) {
n++;
}
};
};
Thread t2 = new Thread() {
public void run() {
for(int i = 0; i < 1000; i++) {
n++;
}
};
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最終n的值為:" + n);
}
}
如果i++是原子操作,那么結果應該就是2000,反復運行幾次發現結果大部分情況下都不是2000,這也證明了i++的非原子性在多線程下產生的問題。當然我們可以通過加鎖的方式保證操作的原子性,但本文的重點是使用原子類的解決這個問題。
最終n的值為:1367
---
最終n的值為:1243
---
最終n的值為:1380
2. AtomicInteger的原子操作
上面的問題可以使用AtomicInteger來解決,我們更改一下代碼如下:
package com.wangjun.thread;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerTest {
private static AtomicInteger n2 = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
public void run() {
for(int i = 0; i < 1000; i++) {
n2.incrementAndGet();
}
};
};
Thread t2 = new Thread() {
public void run() {
for(int i = 0; i< 1000; i++) {
n2.incrementAndGet();
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最終n2的值為:" + n2.toString());
}
}
多次運行,發現結果永遠是2000,由此可以證明AtomicInteger的操作是原子性的。
最終n2的值為:2000
那么AtomicInteger是通過什么機制來保證原子性的呢?接下來,我們對源碼進行一下分析。
3. AtomicInteger源碼分析
構造函數
private volatile int value;
/*
* AtomicInteger內部聲明了一個volatile修飾的變量value用來保存實際值
* 使用帶參的構造函數會將入參賦值給value,無參構造器value默認值為0
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
自增函數
import sun.misc.Unsafe;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
/*
* 可以看到自增函數中調用了Unsafe函數的getAndAddInt方法
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
那么這個getAndAddInt方法是干嘛的呢,首先來了解一下Unsafe這個類。
Unsafe類是在sun.misc包下,不屬於Java標准。但是很多Java的基礎類庫,包括一些被廣泛使用的高性能開發庫都是基於Unsafe類開發的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe類在提升Java運行效率,增強Java語言底層操作能力方面起了很大的作用。
Unsafe類使Java擁有了像C語言的指針一樣操作內存空間的能力,同時也帶來了指針的問題。過度的使用Unsafe類會使得出錯的幾率變大,因此Java官方並不建議使用的,官方文檔也幾乎沒有。
通常我們最好也不要使用Unsafe類,除非有明確的目的,並且也要對它有深入的了解才行。
再來說Unsafe的getAndAddInt,通過反編譯可以看到實現代碼:
/*
* 其中getIntVolatile和compareAndSwapInt都是native方法
* getIntVolatile是獲取當前的期望值
* compareAndSwapInt就是我們平時說的CAS(compare and swap),通過比較如果內存區的值沒有改變,那么就用新值直接給該內存區賦值
*/
public final int getAndAddInt(Object paramObject, long paramLong, int paramInt)
{
int i;
do
{
i = getIntVolatile(paramObject, paramLong);
} while (!compareAndSwapInt(paramObject, paramLong, i, i + paramInt));
return i;
}
incrementAndGet
是將自增后的值返回,還有一個方法getAndIncrement
是將自增前的值返回,分別對應++i
和i++
操作。同樣的decrementAndGe
t和getAndDecrement
則對--i
和i--
操作。
4. CAS中ABA問題的解決
CAS也並非完美的,它會導致ABA問題,就是說,當前內存的值一開始是A,被另外一個線程先改為B然后再改為A,那么當前線程訪問的時候發現是A,則認為它沒有被其他線程訪問過。在某些場景下這樣是存在錯誤風險的。比如在鏈表中。
那么如何解決這個ABA問題呢,大多數情況下樂觀鎖的實現都會通過引入一個版本號標記這個對象,每次修改版本號都會變話,比如使用時間戳作為版本號,這樣就可以很好的解決ABA問題。
在JDK中提供了AtomicStampedReference類來解決這個問題,思路是一樣的。這個類也維護了一個int類型的標記stamp,每次更新數據的時候順帶更新一下stamp。
下面我們通過代碼演示來看一下AtomicStampedReference的使用:
package com.wangjun.thread;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABA {
// 普通的原子類,存在ABA問題
AtomicInteger a1 = new AtomicInteger(10);
// 帶有時間戳的原子類,不存在ABA問題,第二個參數就是默認時間戳,這里指定為0
AtomicStampedReference<Integer> a2 = new AtomicStampedReference<Integer>(10, 0);
public static void main(String[] args) {
ABA a = new ABA();
a.test();
}
public void test() {
new Thread1().start();
new Thread2().start();
new Thread3().start();
new Thread4().start();
}
class Thread1 extends Thread {
@Override
public void run() {
a1.compareAndSet(10, 11);
a1.compareAndSet(11, 10);
}
}
class Thread2 extends Thread {
@Override
public void run() {
try {
Thread.sleep(200); // 睡0.2秒,給線程1時間做ABA操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AtomicInteger原子操作:" + a1.compareAndSet(10, 11));
}
}
class Thread3 extends Thread {
@Override
public void run() {
try {
Thread.sleep(500); // 睡0.5秒,保證線程4先執行
} catch (InterruptedException e) {
e.printStackTrace();
}
int stamp = a2.getStamp();
a2.compareAndSet(10, 11, stamp, stamp + 1);
stamp = a2.getStamp();
a2.compareAndSet(11, 10, stamp, stamp + 1);
}
}
class Thread4 extends Thread {
@Override
public void run() {
int stamp = a2.getStamp();
try {
Thread.sleep(1000); // 睡一秒,給線程3時間做ABA操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AtomicStampedReference原子操作:" + a2.compareAndSet(10, 11, stamp, stamp + 1));
}
}
}
可以看到使用AtomicStampedReference進行compareAndSet的時候,除了要驗證數據,還要驗證時間戳。
如果數據一樣,但是時間戳不一樣,那么這個數據其實也被修改過了。
參考:
java的Unsafe類:https://www.cnblogs.com/pkufork/p/java_unsafe.html
Java CAS 和ABA問題https://www.cnblogs.com/549294286/p/3766717.html