歡迎訪問個人博客,同步更新: 楓山別院html
HikariDataSource的getConnection()方法
7_hikari.png數據庫
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才執行到這裏 if (result == null) { synchronized (this) { result = pool; if (result == null) { validate(); //A 執行到打印日誌 LOGGER.info("{} - Started.", getPoolName()); pool = result = new HikariPool(this); } } } return result.getConnection(); }
其實一看,HikariDataSource的getConnection()
代碼仍是很是簡單的,更多的細節,放在了HikariPool的getConnection()
方法中。多線程
可是,咱們仍是要分析一下的,畢竟,咱們看開源代碼的目的是學習大師的設計和技巧。併發
①檢查鏈接池狀態
//① if (isClosed()) { throw new SQLException("HikariDataSource " + this + " has been closed."); }
這裏的代碼主要是判斷鏈接池是否是已經關閉了,若是isClosed()
返回 true
,那麼鏈接池已經關閉, 那麼直接拋出異常。雖然是一個簡單的判斷,其實也有值得咱們學習的地方。框架
isClosed()
方法實現只有一句代碼:return isShutdown.get();
,這個isShutdown
其實就是一個鏈接池的關閉狀態對吧?它有個get()
方法,猜猜是個什麼類型? OK,它的聲明是private final AtomicBoolean isShutdown = new AtomicBoolean();
。高併發
咱們知道帶Atomic
前綴的一些類型,都是原子操做,它是線程安全的,在高併發狀況下,能保證isShutdown
的值在各個線程中是一致的,相似的還有AtomicInteger
,AtomicLong
等等,那麼AtomicBoolean
就是一個線程安全的布爾類型,這樣就能夠保證關閉鏈接池的時候,其餘線程能夠及時的感知到。性能
那麼線程不安全的緣由是什麼?學習
CPU 有一級緩存,二級緩存,三級緩存,還有內存。一級緩存,二級緩存,三級緩存是每一個 CPU 核獨享的,而內存是整個 CPU 共享的。在CPU計算的時候會把值從內存讀取到最近的一級緩存中,這樣的話,極可能在多個核之間,isShutdown
的值不一致,這就是線程不安全。
那AtomicBoolean
是如何保證多個核之間的線程數據一致呢?
AtomicBoolean
內部,有一個private volatile int value;
的屬性,用於記錄Boolean的值,0 是 false,1 是 true。關鍵就是volatile
修飾符,能夠強制 CPU 在修改value
的時候,必需要同步到內存中,而讀取的時候,必需要從內存中讀取。這樣,各個線程之間就是數據一致了吧。可是,它也有個顯而易見的劣處,你們看出來了嗎,那就是會比較慢,由於它每次都有從內存中讀取數據,這就是性能較差,對吧?因此咱們只能在須要使用volatile
的時候再用,不能濫用。
在我經驗很少的年紀,寫相似代碼標記一個狀態的時候,是直接在類中定義一個類成員變量,沒有用volatile
。如今想來仍是太年輕了,好在那些狀態對實時的要求不高,也沒有出現什麼問題。因此咱們仍是要多讀源碼,學習前輩的經驗。
不知道有沒有同窗會感慨,都涉及到 CPU 了,好底層啊。那麼你們繼續學習 HikariCP 的源碼會發現,不少代碼都是考慮到了很是底層的優化,好比控制了字節碼的大小,方便 JVM優化代碼。另外你們也能夠學習下Disruptor併發框架,也是一個涉及到 CPU 緩存優化的框架,好多大數據框架學習了它的設計,聽說性能高到能把 CPU 跑冒煙。
越是瞭解底層,越能寫出更好的代碼。學習了這些優秀的框架,個人感慨是:那些年上大學睡的覺,終究是要還的,如今終於到時候了.......
② 兩個鏈接池?
//② if (fastPathPool != null) { return fastPathPool.getConnection(); }
這裏的代碼,又是很是簡單,有沒有設計?有!
它的實現是直接調用了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 鏈接池的建立方式。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
讀取的話,是否是每次都要損失一些性能?因此咱們在使用有參構造建立鏈接池的時候,將fastPathPool
也賦值,那麼咱們從fastPathPool
獲取鏈接,至關於變相的不使用volatile,這樣就能不損耗volatile
的性能。volatile
的主要目的就是在建立鏈接池的時候,若是有多個線程同時建立,不會建立出多個鏈接池。咱們會在下面詳細描述。
除了學習到這種設計以外,咱們還能夠知道,使用有參構造來初始化HikariDataSource會有一些性能提高,官方也推薦你們使用有參構造來初始化 HikariCP。其實這種性能提高不是很是大,可是 Hikari做者仍是不放過一點點的讓 HikariCP 更快的機會,這就是爲何 HikariCP 是最快的數據庫鏈接池。
詳細的性能測試結果,你們能夠看下做者的回答:
https://groups.google.com/forum/#!msg/hikari-cp/yAtDD-3Qzgo/MgnNPLUkPqEJ
③雙重檢查鎖
//③ HikariPool result = pool; //B才執行到這裏 if (result == null) { synchronized (this) { result = pool; if (result == null) { validate(); //A 執行到打印日誌 LOGGER.info("{} - Started.", getPoolName()); pool = result = new HikariPool(this); } } } return result.getConnection();
此處的代碼,我相信你們都能看懂,就是檢查鏈接池是否是 null,若是是 null,就建立一個鏈接池,而後重新建立的鏈接池中獲取鏈接返回。
若是我只寫到上面,那我就跟有一些源碼解析的文章同樣了,看了跟沒看同樣, 沒有任何收穫。這不是咱們的目的。當初就是由於他們寫的不詳細,我看不明白,因此我纔打算本身寫,你們也才能看到這篇文章。咱們的目的就是學習到代碼背後的東西, 而不是寫一篇這個方法調用了這個方法,那個方法調用了那個方法
這種沒有養分的東西,由於方法調用你們都能看懂。
閒話少敘,代碼背後的東西來了。這裏的設計就是:雙重檢查鎖,英文名:double checked locking。其實在寫文章以前,我也不知道它叫什麼,只會寫。那麼,什麼是雙重檢查鎖?其實就是在加鎖以前檢查一下對象是否爲 null,加鎖以後再檢查一遍對象是否爲 null,這種結構就是雙重檢查鎖。
爲何這麼寫?已經有了鎖,確定就只能有一個線程建立鏈接池啊,檢查兩次這不是畫蛇添足嗎?我曾經遇到一個多年經驗的老手也這麼問我,因爲我當時不知道雙重檢查鎖這個名字,我只能給他講了一遍以下過程:
咱們假若有兩個線程(A, B)都在執行這個方法。A 執行快一點,拿到了鎖,執行到了打印日誌的地方,可是尚未建立鏈接池,此時鏈接池pool仍是 null。此時 B 執行到了檢查pool是不是null 的地方,由於此時pool是 null,因此 B 要去申請鎖了。A 執行完建立鏈接池了,此時pool不是 null 了,同時釋放了鎖。B 拿到了鎖,再判斷一次pool是不是null,此時pool不是null了,那麼就不建立鏈接池了。若是沒有拿到鎖以後的第二次判斷,那麼鏈接池會被 B再建立一次,這纔是畫蛇添足!
還有人問:那麼直接在獲取鎖以後檢查一次就能夠了,爲何還要在獲取鎖以前檢查一次呢?
由於鎖這個東西,很耗性能,若是隻有一個拿到鎖以後的檢查的話,至關於全部線程要排隊檢查是否是鏈接池已經建立了,至關於只能排隊獲取鏈接,這是不行的,咱們要高性能!在拿鎖以前判斷的話,若是鏈接池已經建立了的話,咱們就直接跳過拿鎖,直接獲取鏈接了,能夠多線程,高併發!
到這裏,這個雙重檢查鎖還不完美!咱們繼續看:
咱們知道,建立一個對象,能夠大致分爲 3 步:
- 分配內存空間
- 初始化對象
- 將對象指向剛分配的內存空間
有時候編譯器和CPU 會在保證最後結果不變的狀況下,對指令重排序,這就是 CPU 的亂序執行。上面的 3 步,可能會變成 132 來執行。也就是說,pool
可能不是 null 了,可是它沒有被初始化,這樣調用的時候也會報錯的。那怎麼辦?答案仍是volatile。``pool
是一個volatile
的,你們還記得吧?咱們上面說了,它是保證線程安全的。此處還要解釋volatile
的第二個功能:能夠阻止指令重排序。它是怎麼阻止重排序的呢?它會對pool
加入一個內存屏障,又稱內存柵欄,是一個CPU指令,能夠阻止對指令的重排序,全部的寫(write)操做都將發生在讀(read)操做以前。
這樣,咱們就能夠完美的保證高併發下,鏈接池能夠被正確的建立出來。
在 HikariCP 框架的使用上,咱們能夠得知,若是使用無參構造初始化HikariCP,實際上是一個延遲初始化,在第一次獲取鏈接的時候,才能初始化鏈接池。若是你們的應用,在啓動以後可能有大量請求,致使大量數據庫鏈接建立,那麼使用無參構造能夠會不太合適,會致使請求有阻塞,數據庫壓力加大。因此,無論在什麼狀況下,仍是要推薦你們使用有參構造初始化 HikariCP。
關於雙重檢查鎖,你們還能夠參考以下資料繼續學習: