在多線程的場景中,我們需要保證數據安全,就會考慮同步的方案,通常會使用synchronized或者lock來處理,使用了synchronized意味著內核態的一次切換。這是一個很重的操作。
有沒有一種方式,可以比較便利的實現一些簡單的數據同步,比如計數器等等。concurrent包下的atomic提供我們這么一種輕量級的數據同步的選擇。
class MyThread implements Runnable {
static AtomicInteger ai=new AtomicInteger(0);
public void run() {
for (int m = 0; m < 1000000; m++) {
ai.getAndIncrement();
}
}
};
public class TestAtomicInteger {
public static void main(String[] args) throws InterruptedException {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
t1.start();
t2.start();
Thread.sleep(500);
System.out.println(MyThread.ai.get());
}
}
在以上代碼中,使用AtomicInteger聲明了一個全局變量,并且在多線程中進行自增,代碼中并沒有進行顯示的加鎖。以上代碼的輸出結果,永遠都是2000000。如果將AtomicInteger換成Integer,打印結果基本都是小于2000000。
也就說明AtomicInteger聲明的變量,在多線程場景中的自增操作是可以保證線程安全的。接下來我們分析下其原理。
原理:
這里,我們來看看AtomicInteger是如何使用非阻塞算法來實現并發控制的。
AtomicInteger的關鍵域只有一下3個:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
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); }
}
private volatile int value;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* Creates a new AtomicInteger with initial value {@code 0}.
*/
public AtomicInteger() {
}
......
}
這里, unsafe是java提供的獲得對對象內存地址訪問的類,注釋已經清楚的寫出了,它的作用就是在更新操作時提供“比較并替換”的作用。實際上就是AtomicInteger中的一個工具。
valueOffset是用來記錄value本身在內存的編譯地址的,這個記錄,也主要是為了在更新操作在內存中找到value的位置,方便比較。
value是用來存儲整數的時間變量,這里被聲明為volatile。volatile只能保證這個變量的可見性。不能保證他的原子性。
可以看看getAndIncrement這個類似i++的函數,可以發現,是調用了UnSafe中的getAndAddInt。
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
/**
* Atomically sets to the given value and returns the old value.
*
* @param newValue the new value
* @return the previous value
*/
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
public final int getAndSetInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
//使用unsafe的native方法,實現高效的硬件級別CAS
} while(!this.compareAndSwapInt(var1, var2, var5, var4));
return var5;
}
如何保證原子性:自旋 + CAS(樂觀鎖)。在這個過程中,通過compareAndSwapInt比較更新value值,如果更新失敗,重新獲取舊值,然后更新。
優缺點:
CAS相對于其他鎖,不會進行內核態操作,有著一些性能的提升。但同時引入自旋,當鎖競爭較大的時候,自旋次數會增多。cpu資源會消耗很高。
換句話說,CAS+自旋適合使用在低并發有同步數據的應用場景。
Java 8做出的改進和努力
在Java 8中引入了4個新的計數器類型,LongAdder、LongAccumulator、DoubleAdder、DoubleAccumulator。他們都是繼承于Striped64。
在LongAdder 與AtomicLong有什么區別?
Atomic*遇到的問題是,只能運用于低并發場景。因此LongAddr在這基礎上引入了分段鎖的概念。可以參考《JDK8系列之LongAdder解析》一起看看做了什么。
大概就是當競爭不激烈的時候,所有線程都是通過CAS對同一個變量(Base)進行修改,當競爭激烈的時候,會將根據當前線程哈希到對于Cell上進行修改(多段鎖)。
/**
* Table of cells. When non-null, size is a power of 2.
*/
transient volatile Cell[] cells;
/**
* Base value, used mainly when there is no contention, but also as
* a fallback during table initialization races. Updated via CAS.
*/
transient volatile long base;
/**
* Padded variant of AtomicLong supporting only raw accesses plus CAS.
*
* JVM intrinsics note: It would be possible to use a release-only
* form of CAS here, if it were provided.
*/
@sun.misc.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long valueOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
可以看到大概實現原理是:通過CAS樂觀鎖保證原子性,通過自旋保證當次修改的最終修改成功,通過降低鎖粒度(多段鎖)增加并發性能。