實現全局自增id最簡單有效的方式是什么?java.util.concurrent.atomic
包定義了一些常見類型的原子變量。這些原子變量為我們提供了一種操作單一變量無鎖(lock-free)的線程安全(thread-safe)方式。實際上該包下面的類為我們提供了類似volatile
變量的特性,同時還提供了諸如boolean compareAndSet(expectedValue, updateValue)
的功能。不使用鎖實現線程安全聽起來似乎很不可思議,這其實是通過CPU的compare and swap指令實現的,由於硬件指令支持當然不需要加鎖了。
先不去討論這些細節,我們來看一下原子變量的用法。一個典型的用法是可以使用原子變量輕松實現全局自增id,就像下面這樣:
// 線程安全的序列id生成器
class Sequencer {
private final AtomicLong sequenceNumber = new AtomicLong(0);
public long next() {
return sequenceNumber.getAndIncrement();
}
}
上述代碼利用AtomicLong創建了一個Sequencer類,不斷調用該類的next()方法就可以得到線程安全的自增id,用起來非常簡單直觀。下面我們給出每種原子變量類型的用法說明。
AtomicInteger and AtomicLong
AtomicInteger和AtomicLong分別代表原子類型的整型和長整型,這兩個類提供十分相似的功能,僅僅是位寬不同。如上例所示,原子整型可用於多線程下全局自增id,除此之外還提供了原子比較-賦值等操作,諸如compareAndSet(expect, update)
, decrementAndGet()
,getAndDecrement()
,getAndSet(newValue)
等等,更全面的接口描述可參考JDK文檔。需要提醒的是這些函數都是通過原子CPU指令實現,執行效率較高。
原子整型看似跟普通整型(Integer, Long)類型相似,但不能使用原子整型替代普通整型,因為原子整型是可變的,而普通整型不可變。由於這個原因,使用原子整型作為Map的key並不是個好主意。
你可能會想當然的以為應該有AtomicFloat和AtomicDouble,遺憾的是類庫里並沒有這兩個類型,AtomicByte和AtomicShort也沒有。如果需要替代方案是使用AtomicInteger和AtomicLong。可通過Float.floatToRawIntBits(float)
和Float.intBitsToFloat(int)
將Float存儲到AtomicInteger中,類似的Double類型也可以存儲到AtomicLong中。
AtomicReference
AtomicReference用於存放一個可以原子更新的對象引用。該類包含get()
, set()
, compareAndSet()
, getAndSet()
等原子方法來獲取和更新其代表的對象引用。
AtomicXXXArray
atomic包下面有三種原子數組:AtomicIntegerArray
, AtomicLongArra
, AtomicReferenceArray
,分別代表整型、長整型和引用類型的原子數組。原子數組使得我們可以線程安全的方式去修改和訪問數組里的單個元素。簡單示例如下:
// 原子數組示例
AtomicLongArray longArray = new AtomicLongArray(10);// 創建長度為10的原子數組
longArray.set(1, 100);
long v = longArray.getAndIncrement(1);
AtomicReferenceArray<String> referenceArray = new AtomicReferenceArray<>(16);
referenceArray.set(3, "love");
referenceArray.compareAndSet(3, "love", "you");
簡單來說原子數組就是一種支持線程安全的數組,仍然具有數組“定長”的性質,如果訪問元素超過了數組的長度,將會拋出IndexOutOfBoundsException
。你可能已經想到了,可以使用線程安全的容器來避免容量不足,我們會在后續章節介紹。
什么是線程安全?
線程安全是指多線程訪問是時,無論線程的調度策略是什么,程序能夠正確的執行。導致線程不安全的一個原因是狀態不一致,如果線程A修改了某個共享變量(比如給id++),而線程B沒有及時知道,就會導致B在錯誤的狀態上執行,結果的正確性也就無法保證。原子變量為我們提供了一種保證單個狀態一致的簡單方式,一個線程修改了原子變量,另外的線程立即就能看到,這比通過鎖實現的方式效率要高;如果要同時保證多個變量狀態一致,就只能使用鎖了。