1.利用Java併發工具而非synchronized來保證執行緒安全
synchronized是重量級的鎖,在HikariPool中沒有一處使用,都是通過Java併發工具類來解決執行緒安全問題。我們來看一些例子:
1.1、通過volatile關鍵字保證可見性
volatile關鍵字定義的變數並不能保證執行緒安全,但他能保證一個執行緒的修改對另外一個執行緒立即可見。例如在PoolEntry和ConcurrentBag中都使用了volatile關鍵字。
1.2、使用JUC包下的Atomic類
例如:
1.ConcurrentBag中用AtomicInteger來記錄等待獲取連線的執行緒數量。
2.HikariDataSource中用AtomicBoolean記錄資料來源是否已經關閉。
3.在PoolEntry中用AtomicIntegerFieldUpdater來更新PoolEntry的狀態。
stateUpdater = AtomicIntegerFieldUpdater.newUpdater(PoolEntry.class, "state");
複製程式碼
這樣使得PoolEntry類的state屬性的更新可以保證原子性。
4.在ConcurrentBag中使用CopyOnWriteArrayList來記錄資料庫連線
CopyOnWriteArrayList適用於讀多寫少的場景,讀取時不加鎖,寫時才加鎖,但這樣怎麼保證執行緒安全?
通常我們設計一個資源池,會將未使用資源放入一個可用資源池中,如果池中還有資源就從池中取出,否則就等待或者超時報錯,直到有新的資源回收到資源池中。
獲取資源和釋放資源的程式碼如下:Resource resource = resourcePool.remove(); // 從池中獲取資源,池中資源數量減少
reourcePool.add(resource); // 將資源釋放會池中,池中資源數量增加
複製程式碼
為了保證執行緒安全,這兩個方法均要用synchronized關鍵字修飾。
而在HikariPool中對於可用資源不是直接通過資源池的資源數量來決定,而是通過資源的狀態來決定,資源定義瞭如下幾個狀態:
// 池化資源的狀態定義
int STATE_NOT_IN_USE = 0;
int STATE_IN_USE = 1;
int STATE_REMOVED = -1;
int STATE_RESERVED = -2;
複製程式碼
在獲取資源時通過遍歷資源池並判斷資源狀態得到可用資源:
//ConcurrentBag.java
try {
// 遍歷所有資源
for (T bagEntry : sharedList) { // 這裡非執行緒安全
// 獲得未使用資源並更新狀態為可用
if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { // 這裡是執行緒安全的
// If we may have stolen another waiter's connection, request another bag add.
if (waiting > 1) {
listener.addBagItem(waiting - 1);
}
return bagEntry;
}
}
複製程式碼
因此,雖然CopyOnWriteArrayList的讀操作非執行緒安全,但可通過AtomicIntegerFieldUpdater來保證對池中的資源PoolEntry在狀態更新時的執行緒安全,因此整個操作是執行緒安全的。
這樣就避免了對池資源的出池和入池加鎖,效能得到提升。
2、對效能的追求
我們通過如何獲取連線來看下HikariPool對效能的追求。
在上一節我們已經提及瞭如何獲取資源,但實際的獲取過程還不僅如此,HikariPool獲取資源的過程如下:
2.1、先從ThrodLocal變數中獲取
//ConcurrentBag.java
// Try the thread-local list first
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;
}
}
複製程式碼
我們知道ThrodLocal變數的特點是該變數在同一個執行緒中可見,這樣可不需要通過方法引數傳遞變數,並且是執行緒安全得,而在一次業務操作中有可能多次獲取資料庫連線(注意:多個連線意味著事務問題需要解決),這時HikariPool會將釋放的連線放入ThrodLocal變數中,當前執行緒如果要再次使用連線就可以直接從ThrodLocal變數中獲取。
//ConcurrentBag.java
final List<Object> threadLocalList = threadList.get();
if (threadLocalList.size() < 50) {
threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
}
複製程式碼
2.2、從資源池中獲取
這一步前面已介紹,從資源池中遍歷資源,通過判斷資源狀態是否可用來獲取資源。
2.3、資源不足時獲取資源的方式
一般的,當資源不足時,如果沒有超過最大資源數限制,就會新建一個新資源並返回,而HikariPool不是,它的獲取過程如下:
1.獲取資源的執行緒獲取資源
2.發現資源不足,則會非同步呼叫建立資源的執行緒去建立資源
3.然後就開始等待資源返回
//ConcurrentBag.java
// 非同步呼叫建立資源執行緒建立資源,其中waiting是等待獲取資源的執行緒數
listener.addBagItem(waiting);
timeout = timeUnit.toNanos(timeout);
do {
final long start = currentTime();
// 等待獲取資源
final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
return bagEntry;
}
timeout -= elapsedNanos(start);
} while (timeout > 10_000);
複製程式碼
4.建立資源執行緒非同步建立資源,建立資源時會判斷是否有需要獲取資源的執行緒在等待資源,如果有才建立,否則就不建立
//HikariPool.java
// connectionBag.getWaitingThreadCount() > 0 判斷有等待資源的執行緒才會繼續建立資源
private synchronized boolean shouldCreateAnotherConnection() {
return getTotalConnections() < config.getMaximumPoolSize() &&
(connectionBag.getWaitingThreadCount() > 0 || getIdleConnections() < config.getMinimumIdle());
}
複製程式碼
5.其他使用資源的執行緒使用完資源後,會釋放資源,這時資源池中有了可用資源,會分給等待執行緒使用
//ConcurrentBag.java
// 使用資源的執行緒釋放資源
public void requite(final T bagEntry)
{
bagEntry.setState(STATE_NOT_IN_USE);
for (int i = 0; waiters.get() > 0; i++) {
if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
return;
}
else if ((i & 0xff) == 0xff) { // 0xff 是255, 每隔256進去一次
parkNanos(MICROSECONDS.toNanos(10));
}
else {
yield();
}
}
final List<Object> threadLocalList = threadList.get();
if (threadLocalList.size() < 50) {
threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
}
}
複製程式碼
HikariPool這麼做的好處是:
- 圖中a和b操作誰先執行完就用誰的資源,大併發情況下,也可能b比a快,這樣效能有提升。
- 如果b先執行完,等待執行緒獲取到資源後,如果沒有新的等待執行緒,a就不會建立新資源,這樣就節省了一個資源,少了佔用連線,也節省了記憶體。
以上這個巧妙的處理方式藉助了SynchronousQueue來實現,我們可以模擬下以上處理方式:
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
public class SynchronousQueueTest {
// 入參為true,公平鎖,保證FIFO
private SynchronousQueue<PoolEntry> queue = new SynchronousQueue(true);
public static void main(String[] args) throws InterruptedException {
SynchronousQueueTest queueTest = new SynchronousQueueTest();
queueTest.execute();
}
public void execute() {
// 模擬生產者建立資源
new Producer("Producer-generate-poolentry", queue, 2000).start();
// 模擬其他消費者釋放資源
new Producer("OtherConsumer-release-poolentry", queue, 5000).start();
// 等待上面兩個執行緒啟動
sleep();
// 模擬消費者
new Consumer(queue).start();
}
private void sleep() {
try {
Thread.sleep(2000);
} catch (InterruptedException E) {
Thread.currentThread().interrupt();
}
}
}
// 用來模擬生產者和釋放資源的消費者
class Producer extends Thread {
private SynchronousQueue<PoolEntry> queue;
// 模擬執行耗時
private long executeCostTimeMillis;
public Producer(String name, SynchronousQueue queue, long executeCostTimeMillis) {
this.queue = queue;
setName(name);
this.executeCostTimeMillis = executeCostTimeMillis;
}
@Override
public void run() {
try {
while(true) {
int random = (int) (Math.random()*10);
PoolEntry poolEntry = new PoolEntry(random);
System.out.println(Thread.currentThread().getName() + ", " + poolEntry.toString());
// 資源入隊
while(!queue.offer(poolEntry)) {
yield();
}
Thread.sleep(executeCostTimeMillis);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Consumer extends Thread {
private SynchronousQueue<PoolEntry> queue;
public Consumer(SynchronousQueue queue) {
this.queue = queue;
setName("Consumer");
}
@Override
public void run() {
try {
while(true) {
long timeout = 200;
// 資源出隊
PoolEntry poolEntry = queue.poll(timeout, TimeUnit.MILLISECONDS);
if (poolEntry != null) {
System.out.println(Thread.currentThread().getName() + ", " + poolEntry.toString());
} else {
// System.out.println("queue is null.");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 資源類
class PoolEntry {
int num;
public PoolEntry(int i) {
this.num = i;
}
@Override
public String toString() {
return "PoolEntry instance " + num;
}
}
複製程式碼
輸出:
Producer-generate-poolentry, PoolEntry instance 4
OtherConsumer-release-poolentry, PoolEntry instance 5
Consumer, PoolEntry instance 5
Consumer, PoolEntry instance 4
Producer-generate-poolentry, PoolEntry instance 8
Consumer, PoolEntry instance 8
Producer-generate-poolentry, PoolEntry instance 7
Consumer, PoolEntry instance 7
OtherConsumer-release-poolentry, PoolEntry instance 6
Consumer, PoolEntry instance 6
Producer-generate-poolentry, PoolEntry instance 6
Consumer, PoolEntry instance 6
Producer-generate-poolentry, PoolEntry instance 2
Consumer, PoolEntry instance 2
OtherConsumer-release-poolentry, PoolEntry instance 1
Consumer, PoolEntry instance 1
Producer-generate-poolentry, PoolEntry instance 2
Consumer, PoolEntry instance 2
Producer-generate-poolentry, PoolEntry instance 3
Consumer, PoolEntry instance 3
Producer-generate-poolentry, PoolEntry instance 7
Consumer, PoolEntry instance 7
OtherConsumer-release-poolentry, PoolEntry instance 9
Consumer, PoolEntry instance 9
Producer-generate-poolentry, PoolEntry instance 7
Consumer, PoolEntry instance 7
複製程式碼
可以看出:
1.生產者和其他消費者誰先把資源入隊,消費者就先使用哪個資源
2.沒有可用資源,消費者會一致等待
在使用池化資源大併發場景下,又追求極致效能時,這種處理方式值得借鑑。
3、使用弱引用節省記憶體
弱引用在呼叫垃圾回收後會被釋放,對於通過ThreadLocal變數快取的資源,為了避免執行緒生命週期結束後資源不被及時回收,使用了弱引用來儲存資源,這樣當記憶體不足,呼叫GC操作時就會被回收,減少記憶體佔用。
//ConcurrentBag.java
final List<Object> threadLocalList = threadList.get();
if (threadLocalList.size() < 50) {
threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
}
複製程式碼
4、使用空方法使得程式碼處理邏輯統一
實現類使用空方法使得處理邏輯統一,不需要新增if判斷來處理。類似編碼規範中對於返回一個集合的方法,建議不要返回NULL,而返回一個大小為0的集合,這樣外部處理邏輯統一,不需要額外增加為NULL的判斷,或者引起空指標異常。
4.1、ProxyLeakTask
4.1.1、空方法實現類
//ProxyLeakTask.java
static
{
// 不需要監控連線洩露的ProxyLeakTask的實現類
NO_LEAK = new ProxyLeakTask() {
@Override
void schedule(ScheduledExecutorService executorService, long leakDetectionThreshold) {}
@Override
public void run() {} // 預設啥都不做
@Override
public void cancel() {} // 預設啥都不做
};
}
複製程式碼
4.1.2、例項化
//ProxyLeakTaskFactory.java
ProxyLeakTask schedule(final PoolEntry poolEntry)
{
// 根據配置來建立不同的代理洩露監控類
return (leakDetectionThreshold == 0) ? ProxyLeakTask.NO_LEAK : scheduleNewTask(poolEntry);
}
複製程式碼
4.1.3、呼叫點
//ProxyLeakTaskFactory.java
private ProxyLeakTask scheduleNewTask(PoolEntry poolEntry) {
ProxyLeakTask task = new ProxyLeakTask(poolEntry);
// 這裡就不用加是否為NULL的判斷
task.schedule(executorService, leakDetectionThreshold);
return task;
}
複製程式碼
5、總結
- 充分利用JUC工具解決併發問題和提升效能。
- 池化資源可以通過資源狀態來獲取可用資源,而不需要通過idle池的出隊,入隊來獲取,減少鎖的使用,提高效能。
- 在對使用記憶體有嚴格要求時,例如低端機不能佔用過多記憶體時,使用好弱引用,軟引用。
- 極致效能要考慮很多細節,如文中獲取資源的例子,一般情況下不會想這麼細。
- 使用空方法實現來統一外部處理邏輯。
end.
相關閱讀:
HikariPool原始碼(一)初識
Google guava原始碼之EventBus
Java極客站點: javageektour.com/