一、原子操作CAS
1.1 LongAdder
JDK1.8時,java.util.concurrent.atomic包中提供了一個新的原子類:LongAdder。
根據Oracle官方文檔的介紹,LongAdder在高併發的場景下會比它的前輩-->AtomicLong 具有更好的性能,代價是消耗更多的內存空間。
1.2 AtomicLong中的問題
AtomicLong是利用了底層的CAS操作來提供併發性的,調用了Unsafe類的getAndAddLong方法,該方法是個native方法,它的邏輯是採用自旋的方式不斷更新目標值,直到更新成功。
AtomicLong中有個內部變量value保存着實際的long值,所有的操作都是針對該變量進行。也就是說,高併發環境下,value變量其實是一個熱點,也就是N個線程競爭一個熱點。另外在併發量較低的環境下,線程衝突的概率比較小,自旋的次數不會很多。但是,高併發環境下,N個線程同時進行自旋操作,會出現大量失敗並不斷自旋的情況,此時AtomicLong的自旋會成爲瓶頸。
這就是LongAdder引入的初衷——解決高併發環境下AtomicLong的自旋瓶頸問題。
1.3 LongAdder設計思路
LongAdder的基本思路就是分散熱點,將value值分散到一個數組中,不同線程會命中到數組的不同槽中,各個線程只對自己槽中的那個值進行CAS操作,這樣熱點就被分散了,衝突的概率就小很多。如果要獲取真正的long值,只要將各個槽中的變量值累加返回。
這種做法和ConcurrentHashMap中的“分段鎖”其實就是類似的思路。
對於LongAdder來說,內部有一個base變量,一個Cell[]數組。
base變量:非競態條件下,直接累加到該變量上。
Cell[]數組:競態條件下,累加個各個線程自己的槽Cell[i]中。
所以,最終結果的計算應該是
public long sum() {
//獲取cells數據
Cell[] as = cells; Cell a;
//定義sum爲base值
long sum = base;
//如果cell數組不爲空0說明發生競爭
if (as != null) {
//遍歷數組裏的值進行累加
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
//如果cell數組爲空,說明沒有發生衝突競爭,直接返回base的值
return sum;
}
在實際運用的時候,只有從未出現過併發衝突的時候,base基數纔會使用到,一旦出現了併發衝突,之後所有的操作都只針對Cell[]數組中的單元Cell。
/**
* Adds the given value.
*
* @param x the value to add
*/
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
//如果cells數據是null,沒有競爭,直接通過cas操作將base的值加上x
if ((as = cells) != null || !casBase(b = base, b + x)) {
//如果cells數組不是null,說明發生競爭,設置uncontended 爲ture,並將數據放入cells數組中
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
//該方法主要是用一個死循環對cells數組中的元素進行操作,當要更新的位置的元素爲空時插入新的cell元素,
//否則在該位置進行CAS的累加操作,如果CAS操作失敗並且數組大小沒有超過核數就擴容cells數組
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe(); //返回當前線程的threadLocalRandomProbe值
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
Cell[] as; Cell a; int n; long v;
if ((as = cells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // cells數組中對應位置沒有數據則插入新對象
Cell r = new Cell(x);
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x)))) //對該位置的cell元素進行累加
break;
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale //判斷數組大小是否大於核數
else if (!collide)
collide = true;
else if (cellsBusy == 0 && casCellsBusy()) { //對cells數組進行擴容,直接擴容爲2倍
try {
if (cells == as) { // Expand table unless stale
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = advanceProbe(h);
}
else if (cellsBusy == 0 && cells == as && casCellsBusy()) { //cellsBusy這裏是做爲一個自旋鎖來使用的
boolean init = false;
try { // 初始化cells數組大小爲2
if (cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x)))) //對base進行CAS操作
break; // Fall back on using base
}
}
而LongAdder最終結果的求和,並沒有使用全局鎖,返回值不是絕對準確的,他只能保證最終一致性,因爲調用這個方法時還有其他線程可能正在進行計數累加,所以只能得到某個時刻的近似值,這也就是LongAdder並不能完全替代LongAtomic的原因之一。
而且從測試情況來看,線程數越多,併發操作數越大,LongAdder的優勢越大,線程數較小時,AtomicLong的性能還超過了LongAdder。
除了新引入LongAdder外,還有引入了它的三個兄弟類:LongAccumulator、DoubleAdder、DoubleAccumulator。
LongAccumulator是LongAdder的增強版。LongAdder只能針對數值的進行加減運算,而LongAccumulator提供了自定義的函數操作。
通過LongBinaryOperator,可以自定義對入參的任意操作,並返回結果(LongBinaryOperator接收2個long作爲參數,並返回1個long)。
LongAccumulator內部原理和LongAdder幾乎完全一樣。
DoubleAdder和DoubleAccumulator用於操作double原始類型。
1.4使用場景
LongAdder提供的API和AtomicLong比較接近,兩者都能以原子 的方式對long型變量進行增減。
但是AtomicLong提供的功能其實更豐富,尤其是addAndGet、decrementAndGet、compareAndSet這些方法。
addAndGet、decrementAndGet除了單純的做自增自減外,還可以立即獲取增減後的值,而LongAdder則需要做同步控制才能精確獲取增減後的值。如果業務需求需要精確的控制計數,做計數比較,AtomicLong也更合適。
另外,從空間方面考慮,LongAdder其實是一種“空間換時間”的思想,從這一點來講AtomicLong更適合。
總之,低併發、一般的業務場景下AtomicLong是足夠了。如果併發量很多,存在大量寫多讀少的情況,那LongAdder可能更合適。適合的纔是最好的,如果真出現了需要考慮到底用AtomicLong好還是LongAdder的業務場景,那麼這樣的討論是沒有意義的,因爲這種情況下要麼進行性能測試,以準確評估在當前業務場景下兩者的性能,要麼換個思路尋求其它解決方案。
測試code:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
/**
* 類說明:LongAdder,原子類以及同步鎖性能測試
*/
public class LongAdderDemo {
private static final int MAX_THREADS = 150;
private static final int TASK_COUNT = 300;
private static final int TARGET_COUNT = 10000000;
/*三個不同類型的long有關的變量*/
private AtomicLong acount = new AtomicLong(0L);
private LongAdder lacount = new LongAdder();
private long count = 0;
/*控制線程同時進行*/
private static CountDownLatch cdlsync = new CountDownLatch(TASK_COUNT);
private static CountDownLatch cdlatomic = new CountDownLatch(TASK_COUNT);
private static CountDownLatch cdladdr = new CountDownLatch(TASK_COUNT);
/*普通long的同步鎖測試方法*/
protected synchronized long inc() {
return ++count;
}
protected synchronized long getCount() {
return count;
}
/*普通long的同步鎖測試任務*/
public class SyncTask implements Runnable {
protected String name;
protected long starttime;
LongAdderDemo out;
public SyncTask(long starttime, LongAdderDemo out) {
this.starttime = starttime;
this.out = out;
}
@Override
public void run() {
long v = out.getCount();
while (v < TARGET_COUNT) {
v = out.inc();
}
long endtime = System.currentTimeMillis();
System.out.println("SyncTask spend:" + (endtime - starttime) + "ms" );
cdlsync.countDown();
}
}
/*普通long的執行同步鎖測試*/
public void testSync() throws InterruptedException {
ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS);
long starttime = System.currentTimeMillis();
SyncTask sync = new SyncTask(starttime, this);
for (int i = 0; i < TASK_COUNT; i++) {
exe.submit(sync);
}
cdlsync.await();
exe.shutdown();
}
/*原子型long的測試任務*/
public class AtomicTask implements Runnable {
protected String name;
protected long starttime;
public AtomicTask(long starttime) {
this.starttime = starttime;
}
@Override
public void run() {
long v = acount.get();
while (v < TARGET_COUNT) {
v = acount.incrementAndGet();
}
long endtime = System.currentTimeMillis();
System.out.println("AtomicTask spend:" + (endtime - starttime) + "ms" );
cdlatomic.countDown();
}
}
/*原子型long的執行測試*/
public void testAtomic() throws InterruptedException {
ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS);
long starttime = System.currentTimeMillis();
AtomicTask atomic = new AtomicTask(starttime);
for (int i = 0; i < TASK_COUNT; i++) {
exe.submit(atomic);
}
cdlatomic.await();
exe.shutdown();
}
/*LongAdder的測試任務*/
public class LongAdderTask implements Runnable {
protected String name;
protected long startTime;
public LongAdderTask(long startTime) {
this.startTime = startTime;
}
@Override
public void run() {
long v = lacount.sum();
while (v < TARGET_COUNT) {
lacount.increment();
v = lacount.sum();
}
long endtime = System.currentTimeMillis();
System.out.println("LongAdderTask spend:" + (endtime - startTime) + "ms");
cdladdr.countDown();
}
}
/*LongAdder的執行測試*/
public void testLongAdder() throws InterruptedException {
ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS);
long startTime = System.currentTimeMillis();
LongAdderTask longAdderTask = new LongAdderTask(startTime);
for (int i = 0; i < TASK_COUNT; i++) {
exe.submit(longAdderTask);
}
cdladdr.await();
exe.shutdown();
}
public static void main(String[] args) throws InterruptedException {
LongAdderDemo demo = new LongAdderDemo();
demo.testSync();
demo.testAtomic();
demo.testLongAdder();
}
}
二、StampLock
StampedLock是Java8引入的一種新的所機制,簡單的理解,可以認爲它是讀寫鎖的一個改進版本,讀寫鎖雖然分離了讀和寫的功能,使得讀與讀之間可以完全併發,但是讀和寫之間依然是衝突的,讀鎖會完全阻塞寫鎖,它使用的依然是悲觀的鎖策略.如果有大量的讀線程,他也有可能引起寫線程的飢餓。
而StampedLock則提供了一種樂觀的讀策略,這種樂觀策略的鎖非常類似於無鎖的操作,使得樂觀鎖完全不會阻塞寫線程。
它的思想是讀寫鎖中讀不僅不阻塞讀,同時也不應該阻塞寫。
讀不阻塞寫的實現思路:
在讀的時候如果發生了寫,則應當重讀而不是在讀的時候直接阻塞寫!即讀寫之間不會阻塞對方,但是寫和寫之間還是阻塞的!
StampedLock的內部實現是基於CLH的。
code:
import java.util.concurrent.locks.StampedLock;
/**
* 類說明:JDK1.8源碼自帶的示例
*/
public class StampedLockDemo {
//一個點的x,y座標
private double x,y;
/**Stamped類似一個時間戳的作用,每次寫的時候對其+1來改變被操作對象的Stamped值
* 這樣其它線程讀的時候發現目標對象的Stamped改變,則執行重讀*/
private final StampedLock sl = new StampedLock();
//【寫鎖(排它鎖)】
void move(double deltaX,double deltaY) {// an exclusively locked method
/**stampedLock調用writeLock和unlockWrite時候都會導致stampedLock的stamp值的變化
* 即每次+1,直到加到最大值,然後從0重新開始*/
long stamp =sl.writeLock(); //寫鎖
try {
x +=deltaX;
y +=deltaY;
} finally {
sl.unlockWrite(stamp);//釋放寫鎖
}
}
//【樂觀讀鎖】
double distanceFromOrigin() { // A read-only method
/**
* tryOptimisticRead是一個樂觀的讀,使用這種鎖的讀不阻塞寫
* 每次讀的時候得到一個當前的stamp值(類似時間戳的作用)
*/
long stamp = sl.tryOptimisticRead();
//這裏就是讀操作,讀取x和y,因爲讀取x時,y可能被寫了新的值,所以下面需要判斷
double currentX = x, currentY = y;
/**如果讀取的時候發生了寫,則stampedLock的stamp屬性值會變化,此時需要重讀,
* validate():比較當前stamp和獲取樂觀鎖得到的stamp比較,不一致則失敗。
* 再重讀的時候需要加讀鎖(並且重讀時使用的應當是悲觀的讀鎖,即阻塞寫的讀鎖)
* 當然重讀的時候還可以使用tryOptimisticRead,此時需要結合循環了,即類似CAS方式
* 讀鎖又重新返回一個stampe值*/
if (!sl.validate(stamp)) {//如果驗證失敗(讀之前已發生寫)
stamp = sl.readLock(); //悲觀讀鎖
try {
currentX = x;
currentY = y;
}finally{
sl.unlockRead(stamp);//釋放讀鎖
}
}
//讀鎖驗證成功後執行計算,即讀的時候沒有發生寫
return Math.sqrt(currentX *currentX + currentY *currentY);
}
//讀鎖升級爲寫鎖
void moveIfAtOrigin(double newX, double newY) { // upgrade
// 讀鎖(這裏可用樂觀鎖替代)
long stamp = sl.readLock();
try {
//循環,檢查當前狀態是否符合
while (x == 0.0 && y == 0.0) {
long ws = sl.tryConvertToWriteLock(stamp);
//如果寫鎖成功
if (ws != 0L) {
stamp = ws;// 替換stamp爲寫鎖戳
x = newX;//修改數據
y = newY;
break;
}
//轉換爲寫鎖失敗
else {
//釋放讀鎖
sl.unlockRead(stamp);
//獲取寫鎖(必要情況下阻塞一直到獲取寫鎖成功)
stamp = sl.writeLock();
}
}
} finally {
//釋放鎖(可能是讀/寫鎖)
sl.unlock(stamp);
}
}
}
三、CompleteableFuture
Future的不足
Future是Java 5添加的類,用來描述一個異步計算的結果。你可以使用isDone方法檢查計算是否完成,或者使用get阻塞住調用線程,直到計算完成返回結果,你也可以使用cancel方法停止任務的執行。
雖然Future以及相關使用方法提供了異步執行任務的能力,但是對於結果的獲取卻是很不方便,只能通過阻塞或者輪詢的方式得到任務的結果。阻塞的方式顯然和我們的異步編程的初衷相違背,輪詢的方式又會耗費無謂的CPU資源,而且也不能及時地得到計算結果,爲什麼不能用觀察者設計模式當計算結果完成及時通知監聽者呢?。
Java的一些框架,比如Netty,自己擴展了Java的 Future接口,提供了addListener等多個擴展方法,Google guava也提供了通用的擴展Future:ListenableFuture、SettableFuture 以及輔助類Futures等,方便異步編程。
同時Future接口很難直接表述多個Future 結果之間的依賴性。實際開發中,我們經常需要達成以下目的:
將兩個異步計算合併爲一個——這兩個異步計算之間相互獨立,同時第二個又依賴於第一個的結果。
等待 Future 集合中的所有任務都完成。
僅等待 Future集合中最快結束的任務完成(有可能因爲它們試圖通過不同的方式計算同一個值),並返回它的結果。
應對 Future 的完成事件(即當 Future 的完成事件發生時會收到通知,並能使用 Future 計算的結果進行下一步的操作,不只是簡單地阻塞等待操作的結果)
CompleteableFuture
JDK1.8才新加入的一個實現類CompletableFuture,實現了Future<T>, CompletionStage<T>兩個接口。實現了Future接口,意味着可以像以前一樣通過阻塞或者輪詢的方式獲得結果。
創建
除了直接new出一個CompletableFuture的實例,還可以通過工廠方法創建CompletableFuture的實例
工廠方法:
Asynsc表示異步,而supplyAsync與runAsync不同在與前者異步返回一個結果,後者是void.第二個函數第二個參數表示是用我們自己創建的線程池,否則採用默認的ForkJoinPool.commonPool()作爲它的線程池。
獲得結果的方法
public T get()
public T get(long timeout, TimeUnit unit)
public T getNow(T valueIfAbsent)
public T join()
getNow有點特殊,如果結果已經計算完則返回結果或者拋出異常,否則返回給定的valueIfAbsent值。
join返回計算的結果或者拋出一個unchecked異常(CompletionException),它和get對拋出的異常的處理有些細微的區別。
輔助方法
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)
allOf方法是當所有的CompletableFuture都執行完後執行計算。
anyOf方法是當任意一個CompletableFuture執行完後就會執行計算,計算的結果相同。
CompletionStage是一個接口,從命名上看得知是一個完成的階段,它代表了一個特定的計算的階段,可以同步或者異步的被完成。你可以把它看成一個計算流水線上的一個單元,並最終會產生一個最終結果,這意味着幾個CompletionStage可以串聯起來,一個完成的階段可以觸發下一階段的執行,接着觸發下一次,再接着觸發下一次,……….。
總結CompletableFuture幾個關鍵點:
1、計算可以由 Future ,Consumer 或者 Runnable 接口中的 apply,accept 或者 run等方法表示。
2、計算的執行主要有以下
a. 默認執行
b. 使用默認的CompletionStage的異步執行提供者異步執行。這些方法名使用someActionAsync這種格式表示。
c. 使用 Executor 提供者異步執行。這些方法同樣也是someActionAsync這種格式,但是會增加一個Executor 參數。
CompletableFuture裏大約有五十種方法,但是可以進行歸類
變換類 thenApply:
關鍵入參是函數式接口Function。它的入參是上一個階段計算後的結果,返回值是經過轉化後結果。
消費類 thenAccept:
關鍵入參是函數式接口Consumer。它的入參是上一個階段計算後的結果, 沒有返回值。
執行操作類 thenRun:
對上一步的計算結果不關心,執行下一個操作,入參是一個Runnable的實例,表示上一步完成後執行的操作。
結合轉化類:
需要上一步的處理返回值,並且other代表的CompletionStage 有返回值之後,利用這兩個返回值,進行轉換後返回指定類型的值。
兩個CompletionStage是並行執行的,它們之間並沒有先後依賴順序,other並不會等待先前的CompletableFuture執行完畢後再執行。
結合轉化類
對於Compose可以連接兩個CompletableFuture,其內部處理邏輯是當第一個CompletableFuture處理沒有完成時會合併成一個CompletableFuture,如果處理完成,第二個future會緊接上一個CompletableFuture進行處理。
第一個CompletableFuture 的處理結果是第二個future需要的輸入參數。
結合消費類:
需要上一步的處理返回值,並且other代表的CompletionStage 有返回值之後,利用這兩個返回值,進行消費
運行後執行類:
不關心這兩個CompletionStage的結果,只關心這兩個CompletionStage都執行完畢,之後再進行操作(Runnable)。
取最快轉換類:
兩個CompletionStage,誰計算的快,我就用那個CompletionStage的結果進行下一步的轉化操作。現實開發場景中,總會碰到有兩種渠道完成同一個事情,所以就可以調用這個方法,找一個最快的結果進行處理。
取最快消費類:
兩個CompletionStage,誰計算的快,我就用那個CompletionStage的結果進行下一步的消費操作。
取最快運行後執行類:
兩個CompletionStage,任何一個完成了都會執行下一步的操作(Runnable)。
異常補償類:
當運行時出現了異常,可以通過exceptionally進行補償。
運行後記錄結果類:
action執行完畢後它的結果返回原始的CompletableFuture的計算結果或者返回異常。所以不會對結果產生任何的作用。
運行後處理結果類:
運行完成時,對結果的處理。這裏的完成時有兩種情況,一種是正常執行,返回值。另外一種是遇到異常拋出造成程序的中斷。
四、擴充知識點- Disruptor
應用背景和介紹
Disruptor是英國外匯交易公司LMAX開發的一個高性能隊列,研發的初衷是解決內部的內存隊列的延遲問題,而不是分佈式隊列。基於Disruptor開發的系統單線程能支撐每秒600萬訂單,2010年在QCon演講後,獲得了業界關注。
據目前資料顯示:應用Disruptor的知名項目有如下的一些:Storm, Camel, Log4j2,還有目前的美團點評技術團隊也有很多不少的應用,或者說有一些借鑑了它的設計機制。
Disruptor是一個高性能的線程間異步通信的框架,即在同一個JVM進程中的多線程間消息傳遞。
傳統隊列問題
在JDK中,Java內部的隊列BlockQueue的各種實現,仔細分析可以得知,隊列的底層數據結構一般分成三種:數組、鏈表和堆,堆這裏是爲了實現帶有優先級特性的隊列暫且不考慮。
在穩定性和性能要求特別高的系統中,爲了防止生產者速度過快,導致內存溢出,只能選擇有界隊列;同時,爲了減少Java的垃圾回收對系統性能的影響,會盡量選擇 Array格式的數據結構。這樣篩選下來,符合條件的隊列就只有ArrayBlockingQueue。但是ArrayBlockingQueue是通過加鎖的方式保證線程安全,而且ArrayBlockingQueue還存在僞共享問題,這兩個問題嚴重影響了性能。
ArrayBlockingQueue的這個僞共享問題存在於哪裏呢,分析下核心的部分源碼,其中最核心的三個成員變量爲
是在ArrayBlockingQueue的核心enqueue和dequeue方法中經常會用到的,這三個變量很容易放到同一個緩存行中,進而產生僞共享問題。
高性能的原理
引入環形的數組結構:數組元素不會被回收,避免頻繁的GC,
無鎖的設計:採用CAS無鎖方式,保證線程的安全性
屬性填充:通過添加額外的無用信息,避免僞共享問題
環形數組結構是整個Disruptor的核心所在。
首先因爲是數組,所以要比鏈表快,而且根據我們對上面緩存行的解釋知道,數組中的一個元素加載,相鄰的數組元素也是會被預加載的,因此在這樣的結構中,cpu無需時不時去主存加載數組中的下一個元素。而且,你可以爲數組預先分配內存,使得數組對象一直存在(除非程序終止)。這就意味着不需要花大量的時間用於垃圾回收。此外,不像鏈表那樣,需要爲每一個添加到其上面的對象創造節點對象—對應的,當刪除節點時,需要執行相應的內存清理操作。環形數組中的元素採用覆蓋方式,避免了jvm的GC。
其次結構作爲環形,數組的大小爲2的n次方,這樣元素定位可以通過位運算效率會更高,這個跟一致性哈希中的環形策略有點像。在disruptor中,這個牛逼的環形結構就是RingBuffer,既然是數組,那麼就有大小,而且這個大小必須是2的n次方
其實質只是一個普通的數組,只是當放置數據填充滿隊列(即到達2^n-1位置)之後,再填充數據,就會從0開始,覆蓋之前的數據,於是就相當於一個環。
每個生產者首先通過CAS競爭獲取可以寫的空間,然後再進行慢慢往裏放數據,如果正好這個時候消費者要消費數據,那麼每個消費者都需要獲取最大可消費的下標。
同時,Disruptor 不像傳統的隊列,分爲一個隊頭指針和一個隊尾指針,而是隻有一個角標(上圖的seq),它屬於一個volatile變量,同時也是我們能夠不用鎖操作就能實現Disruptor的原因之一,而且通過緩存行補充,避免僞共享問題。該指針是通過一直自增的方式來獲取下一個可寫或者可讀數據。
本章主要講了LongAdder,StampLock,CompleteableFuture等java8中新增的一些特性,以及簡單介紹了Disruptor,希望大家在開發中可以有更多的選擇。
其他閱讀
併發編程專題九-併發容器ConcurrentHashMap源碼分析