歡迎訪問我的博客,同步更新: 楓山別院
HikariDataSource的getConnection()方法

HikariCP獲取連接的方法是com.zaxxer.hikari.HikariDataSource#getConnection(), 這個方法在HikariDataSource類中。HikariDataSource類中是 HikariCP 提供用戶使用的主要類,有獲取連接,關閉連接池,剔除連接等方法。我們主要看一下getConnection(), 這是對外暴露的獲取連接的方法,不管是Spring獲取連接還是我們自己手工調用 HikariCP,都是調用這個方法從連接池中取連接。
代碼如下:
public Connection getConnection() throws SQLException {
//①
if (isClosed()) {
throw new SQLException("HikariDataSource " + this + " has been closed.");
}
//②
if (fastPathPool != null) {
return fastPathPool.getConnection();
}
/**
* ③
* See http://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java
* GFC: 雙重檢查鎖
* https://www.cnblogs.com/xz816111/p/8470048.html
* 如果是使用無參構造{@link #HikariDataSource()}初始化的HikariDataSource,那么默認是延遲構建HikariDataSource,
* 在第一次獲取連接的時候才構建HikariDataSource
*/
HikariPool result = pool;
//B才執(zhí)行到這里
if (result == null) {
synchronized (this) {
result = pool;
if (result == null) {
validate();
//A 執(zhí)行到打印日志
LOGGER.info("{} - Started.", getPoolName());
pool = result = new HikariPool(this);
}
}
}
return result.getConnection();
}
其實一看,HikariDataSource的getConnection()代碼還是非常簡單的,更多的細節(jié),放在了HikariPool的getConnection()方法中。
但是,我們還是要分析一下的,畢竟,我們看開源代碼的目的是學習大師的設計和技巧。
①檢查連接池狀態(tài)
//①
if (isClosed()) {
throw new SQLException("HikariDataSource " + this + " has been closed.");
}
這里的代碼主要是判斷連接池是不是已經關閉了,如果isClosed()返回 true,那么連接池已經關閉, 那么直接拋出異常。雖然是一個簡單的判斷,其實也有值得我們學習的地方。
isClosed()方法實現(xiàn)只有一句代碼:return isShutdown.get();,這個isShutdown其實就是一個連接池的關閉狀態(tài)對吧?它有個get()方法,猜猜是個什么類型? OK,它的聲明是private final AtomicBoolean isShutdown = new AtomicBoolean();。
我們知道帶Atomic前綴的一些類型,都是原子操作,它是線程安全的,在高并發(fā)情況下,能保證isShutdown的值在各個線程中是一致的,類似的還有AtomicInteger,AtomicLong等等,那么AtomicBoolean就是一個線程安全的布爾類型,這樣就可以保證關閉連接池的時候,其他線程可以及時的感知到。
那么線程不安全的原因是什么?
CPU 有一級緩存,二級緩存,三級緩存,還有內存。一級緩存,二級緩存,三級緩存是每個 CPU 核獨享的,而內存是整個 CPU 共享的。在CPU計算的時候會把值從內存讀取到最近的一級緩存中,這樣的話,很可能在多個核之間,isShutdown的值不一致,這就是線程不安全。
那AtomicBoolean是如何保證多個核之間的線程數(shù)據(jù)一致呢?
AtomicBoolean內部,有一個private volatile int value;的屬性,用于記錄Boolean的值,0 是 false,1 是 true。關鍵就是volatile修飾符,可以強制 CPU 在修改value的時候,必須要同步到內存中,而讀取的時候,必須要從內存中讀取。這樣,各個線程之間就是數(shù)據(jù)一致了吧。但是,它也有個顯而易見的劣處,大家看出來了嗎,那就是會比較慢,因為它每次都有從內存中讀取數(shù)據(jù),這就是性能較差,對吧?所以我們只能在需要使用volatile的時候再用,不能濫用。
在我經驗不多的年紀,寫類似代碼標記一個狀態(tài)的時候,是直接在類中定義一個類成員變量,沒有用volatile?,F(xiàn)在想來還是太年輕了,好在那些狀態(tài)對實時的要求不高,也沒有出現(xiàn)什么問題。所以我們還是要多讀源碼,學習前輩的經驗。
不知道有沒有同學會感慨,都涉及到 CPU 了,好底層啊。那么大家繼續(xù)學習 HikariCP 的源碼會發(fā)現(xiàn),很多代碼都是考慮到了非常底層的優(yōu)化,比如控制了字節(jié)碼的大小,方便 JVM優(yōu)化代碼。另外大家也可以學習下Disruptor并發(fā)框架,也是一個涉及到 CPU 緩存優(yōu)化的框架,好多大數(shù)據(jù)框架學習了它的設計,據(jù)說性能高到能把 CPU 跑冒煙。
越是了解底層,越能寫出更好的代碼。學習了這些優(yōu)秀的框架,我的感慨是:那些年上大學睡的覺,終究是要還的,現(xiàn)在終于到時候了.......
② 兩個連接池?
//②
if (fastPathPool != null) {
return fastPathPool.getConnection();
}
這里的代碼,又是非常簡單,有沒有設計?有!
它的實現(xiàn)是直接調用了fastPathPool的getConnection()方法對吧。但是請大家注意最后的 return語句,是result.getConnection();,這個result是fastPathPool嗎?看下③處HikariPool result = pool;,這個result其實是pool。那么有點奇怪,HikariDataSource中有兩個連接池?不會吧,誰會這么設計呢 !那該如何解釋?
其實在HikariDataSource中,還真的有兩個連接池的成員變量。定義如下:
private final HikariPool fastPathPool;
private volatile HikariPool pool;
除了變量名字不同之外,他們的修飾符也不一樣,fastPathPool是final的,pool是volatile的。volatile在上面已經解釋過了,就是為了線程安全嘛,保證多線程情況下pool的值是一致的。fastPathPool呢,是final的,HikariDataSource初始化的時候必須賦值,之后就改不了了對吧。
其實這里涉及到了HikariCP 連接池的創(chuàng)建方式。HikariDataSource有兩個構造方法,第一個是無參構造:
public HikariDataSource() {
super();
fastPathPool = null;
}
第二個是有參的:
public HikariDataSource(HikariConfig configuration) {
configuration.validate();
configuration.copyState(this);
LOGGER.info("{} - Started.", configuration.getPoolName());
pool = fastPathPool = new HikariPool(this);
}
我們不在此詳細解析這兩個構造方法了,我們只看這兩個構造方法的最后一句,無參構造的是fastPathPool = null;,有參構造的是pool = fastPathPool = new HikariPool(this);。
那么, 我們可以推斷出,如果使用無參構造初始化HikariDataSource,fastPathPool就永遠是 null;如果使用有參構造初始化HikariDataSource,那么fastPathPool就永遠跟pool是一樣的。
fastPathPool和pool都是HikariPool類型的對吧,HikariPool其實是代表了連接池。那么我們最初的問題,為什么使用了兩個連接池的成員變量?我們在①處解析了volatile的劣處,性能略差,如果每次獲取連接都從pool讀取的話,是不是每次都要損失一些性能?所以我們在使用有參構造創(chuàng)建連接池的時候,將fastPathPool也賦值,那么我們從fastPathPool獲取連接,相當于變相的不使用volatile,這樣就能不損耗volatile的性能。volatile的主要目的就是在創(chuàng)建連接池的時候,如果有多個線程同時創(chuàng)建,不會創(chuàng)建出多個連接池。我們會在下面詳細描述。
除了學習到這種設計之外,我們還可以知道,使用有參構造來初始化HikariDataSource會有一些性能提升,官方也推薦大家使用有參構造來初始化 HikariCP。其實這種性能提升不是非常大,但是 Hikari作者還是不放過一點點的讓 HikariCP 更快的機會,這就是為什么 HikariCP 是最快的數(shù)據(jù)庫連接池。
詳細的性能測試結果,大家可以看下作者的回答:
https://groups.google.com/forum/#!msg/hikari-cp/yAtDD-3Qzgo/MgnNPLUkPqEJ
③雙重檢查鎖
//③
HikariPool result = pool;
//B才執(zhí)行到這里
if (result == null) {
synchronized (this) {
result = pool;
if (result == null) {
validate();
//A 執(zhí)行到打印日志
LOGGER.info("{} - Started.", getPoolName());
pool = result = new HikariPool(this);
}
}
}
return result.getConnection();
此處的代碼,我相信大家都能看懂,就是檢查連接池是不是 null,如果是 null,就創(chuàng)建一個連接池,然后從新創(chuàng)建的連接池中獲取連接返回。
如果我只寫到上面,那我就跟有一些源碼解析的文章一樣了,看了跟沒看一樣, 沒有任何收獲。這不是我們的目的。當初就是因為他們寫的不詳細,我看不明白,所以我才打算自己寫,大家也才能看到這篇文章。我們的目的就是學習到代碼背后的東西, 而不是寫一篇這個方法調用了這個方法,那個方法調用了那個方法這種沒有營養(yǎng)的東西,因為方法調用大家都能看懂。
閑話少敘,代碼背后的東西來了。這里的設計就是:雙重檢查鎖,英文名:double checked locking。其實在寫文章之前,我也不知道它叫什么,只會寫。那么,什么是雙重檢查鎖?其實就是在加鎖之前檢查一下對象是否為 null,加鎖之后再檢查一遍對象是否為 null,這種結構就是雙重檢查鎖。
為什么這么寫?已經有了鎖,肯定就只能有一個線程創(chuàng)建連接池啊,檢查兩次這不是多此一舉嗎?我曾經遇到一個多年經驗的老手也這么問我,由于我當時不知道雙重檢查鎖這個名字,我只能給他講了一遍如下過程:
我們假如有兩個線程(A, B)都在執(zhí)行這個方法。A 執(zhí)行快一點,拿到了鎖,執(zhí)行到了打印日志的地方,但是還沒有創(chuàng)建連接池,此時連接池pool還是 null。此時 B 執(zhí)行到了檢查pool是否是null 的地方,因為此時pool是 null,所以 B 要去申請鎖了。A 執(zhí)行完創(chuàng)建連接池了,此時pool不是 null 了,同時釋放了鎖。B 拿到了鎖,再判斷一次pool是否是null,此時pool不是null了,那么就不創(chuàng)建連接池了。如果沒有拿到鎖之后的第二次判斷,那么連接池會被 B再創(chuàng)建一次,這才是多此一舉!
還有人問:那么直接在獲取鎖之后檢查一次就可以了,為什么還要在獲取鎖之前檢查一次呢?
因為鎖這個東西,很耗性能,如果只有一個拿到鎖之后的檢查的話,相當于所有線程要排隊檢查是不是連接池已經創(chuàng)建了,相當于只能排隊獲取連接,這是不行的,我們要高性能!在拿鎖之前判斷的話,如果連接池已經創(chuàng)建了的話,我們就直接跳過拿鎖,直接獲取連接了,可以多線程,高并發(fā)!
到這里,這個雙重檢查鎖還不完美!我們繼續(xù)看:
我們知道,創(chuàng)建一個對象,可以大體分為 3 步:
- 分配內存空間
- 初始化對象
- 將對象指向剛分配的內存空間
有時候編譯器和CPU 會在保證最后結果不變的情況下,對指令重排序,這就是 CPU 的亂序執(zhí)行。上面的 3 步,可能會變成 132 來執(zhí)行。也就是說,pool可能不是 null 了,但是它沒有被初始化,這樣調用的時候也會報錯的。那怎么辦?答案還是volatile。pool是一個volatile的,大家還記得吧?我們上面說了,它是保證線程安全的。此處還要解釋volatile的第二個功能:可以阻止指令重排序。它是怎么阻止重排序的呢?它會對pool加入一個內存屏障,又稱內存柵欄,是一個CPU指令,可以阻止對指令的重排序,所有的寫(write)操作都將發(fā)生在讀(read)操作之前。
這樣,我們就可以完美的保證高并發(fā)下,連接池可以被正確的創(chuàng)建出來。
在 HikariCP 框架的使用上,我們可以得知,如果使用無參構造初始化HikariCP,其實是一個延遲初始化,在第一次獲取連接的時候,才能初始化連接池。如果大家的應用,在啟動之后可能有大量請求,導致大量數(shù)據(jù)庫連接創(chuàng)建,那么使用無參構造可以會不太合適,會導致請求有阻塞,數(shù)據(jù)庫壓力加大。所以,不管在什么情況下,還是要推薦大家使用有參構造初始化 HikariCP。
關于雙重檢查鎖,大家還可以參考如下資料繼續(xù)學習: