讀HikariCP原始碼學Java(一)-- 通過ConcurrentBag類學習併發程式設計思想

繆若塵發表於2021-05-23

前言

ConcurrentBag是HikariCP中實現的一個池化資源的併發管理類。它是一個高效能的生產者-消費者佇列。

ConcurrentBag的併發效能優於LinkedBlockingQueue和LinkedTransferQueue

LinkedBlockingQueue 阻塞佇列

LinkedTransferQueue 資料傳送佇列

TransferQueue繼承自BlockingQueue介面 TransferQueue的改進:

保留與完成

保留是指消費者執行緒在消費時如果發現佇列為空,就生成一個空元素入隊,然後該消費者執行緒在這個資源的資料欄位上旋轉等待。

完成是當生產者執行緒要放入一個新資源時,如果發現首位元素的資料欄位為空,就把資料直接填充到這個元素中。

保留加完成,共同稱為資料的傳送。

為了提升併發效率,ConcurrentBag

  1. 優先使用ThreadLocal裡的資源,如果ThreadLocal的List裡沒有可用的資源了,再使用公共集合(資源池)裡的資源。
  2. 無論ThreadLocal還是公共集合,都使用CAS代替加鎖

IConcurrentBagEntry介面

ConcurrentBag中定義了一個public的成員介面IConcurrentBagEntry,並作為這個類的泛型,要求所有要接受ConcurrentBag管理的池化資源都要實現這個介面

public interface IConcurrentBagEntry
{
    int STATE_NOT_IN_USE = 0;
    int STATE_IN_USE = 1;
    int STATE_REMOVED = -1;
    int STATE_RESERVED = -2;

    boolean compareAndSet(int expectState, int newState);
    void setState(int newState);
    int getState();
}

兩個重要的方法

1. 借用池化資源

public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
  • 引數:
    • timeout: 超時時長
    • timeunit: 時長單位

首先嚐試請求ThreadLocal的資源

final List<Object> list = threadList.get();
for (int i = list.size() - 1; i >= 0; i--) {
    final Object entry = list.remove(i);
    @SuppressWarnings("unchecked")
    final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;
    if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
        return bagEntry;
    }
}

ThreadLocal裡沒有請求到資源,就去請求公共集合裡的資源

final int waiting = waiters.incrementAndGet(); // waiters是個Atomic Integer,表示等待的消費者數量
try {
    // 遍歷請求資源
    for (T bagEntry : sharedList) {
        if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            // 當前消費者執行緒獲取到的資源可能是別的消費者在等待的,為了不讓其他消費者執行緒因為搶佔而阻塞,呼叫建立新資源的執行緒,給在等待的消費者們
            if (waiting > 1) {
                listener.addBagItem(waiting - 1);
            }
            return bagEntry;
        }
    }

    // 另起一個建立新資源的執行緒,建立資源
    listener.addBagItem(waiting);

    // 等待獲取資源,超時控制此時才開始
    timeout = timeUnit.toNanos(timeout);
    do {
        final long start = currentTime();
        final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
        // 返回null表示超時
        if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            return bagEntry;
        }

        timeout -= elapsedNanos(start);
    } while (timeout > 10_000);

    return null;
}
finally {
    waiters.decrementAndGet(); // 釋放
}

實際上,建立新資源的消費者不會馬上就建立一個新資源,而是會先判斷當前是否還有在等待的消費者,這是因為在高併發下,可能有資源搶先被其他執行緒歸還,在等待的消費者就可以直接使用這個空閒的資源。

這種呼叫新執行緒建立資源的方法,比起其它執行緒池如果獲取不到資源直接當前執行緒建立一個新資源的方式,因為多了一次等待中的消費者的數量的判斷,所以既節省了建立資源的時間,提高了併發效能,又節省了記憶體佔用,還節省了執行緒池空間的佔用,可謂一舉三得。

2. 歸還借來的資源

public void requite(final T bagEntry)
  • 引數
    • bagEntry: 要歸還的資源

如果有執行緒正在等待,嘗試將資源歸還到handoffQueue,使用定期parkNanos和yield防止當前操作佔用了過多的CPU資源

for (int i = 0; waiters.get() > 0; i++) {
    if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
        return;
    }
    else if ((i & 0xff) == 0xff) { // 每嘗試256次,就阻塞10ms
        parkNanos(MICROSECONDS.toNanos(10));
    }
    else {
        Thread.yield(); // 讓出CPU排程
    }
}

如果沒有執行緒在等待,把資源歸還到當前執行緒的ThreadLocal,因為同一次操作裡很可能多次獲取連線,要提高一次操作的效率。

可以看出,與常規的生產者-消費者模型不同,每次借用完一定要歸還,因為borrow操作中沒有刪除資源的動作,GC是不可能去回收資源的,不及時歸還的話可能導致記憶體洩露。

綜合以上程式碼,對併發程式設計有以下啟發:

  1. 高併發場景下,儘量避免使用synchronized這種重量級的鎖,而是用Atomic、CopyOnWrite、CAS等輕量級的方式保證併發安全。
  2. 不能讓一個執行緒長時間佔用資源,要適當地給其它執行緒讓行。
  3. 要儘量用低消耗的操作替代高消耗的操作,如這裡的儘量不建立新資源。
  4. 常用的資源儘量放到ThreadLocal中。

相關文章