HikariCP源码分析之获取链接流程一

欢迎访问个人博客,同步更新: 枫山别院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的值在各个线程中是一致的,相似的还有AtomicIntegerAtomicLong等等,那么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();
}

这里的代码,又是很是简单,有没有设计?有!

它的实现是直接调用了fastPathPoolgetConnection()方法对吧。可是请你们注意最后的 return语句,是result.getConnection();,这个resultfastPathPool吗?看下③处HikariPool result = pool;,这个result实际上是pool。那么有点奇怪,HikariDataSource中有两个链接池?不会吧,谁会这么设计呢 !那该如何解释?

其实在HikariDataSource中,还真的有两个链接池的成员变量。定义以下:

private final HikariPool fastPathPool;
private volatile HikariPool pool;

除了变量名字不一样以外,他们的修饰符也不同,fastPathPoolfinal的,poolvolatile的。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是同样的。

fastPathPoolpool都是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 步:

  1. 分配内存空间
  2. 初始化对象
  3. 将对象指向刚分配的内存空间

有时候编译器和CPU 会在保证最后结果不变的状况下,对指令重排序,这就是 CPU 的乱序执行。上面的 3 步,可能会变成 132 来执行。也就是说,pool可能不是 null 了,可是它没有被初始化,这样调用的时候也会报错的。那怎么办?答案仍是volatile。``pool是一个volatile的,你们还记得吧?咱们上面说了,它是保证线程安全的。此处还要解释volatile的第二个功能:能够阻止指令重排序。它是怎么阻止重排序的呢?它会对pool加入一个内存屏障,又称内存栅栏,是一个CPU指令,能够阻止对指令的重排序,全部的写(write)操做都将发生在读(read)操做以前。

这样,咱们就能够完美的保证高并发下,链接池能够被正确的建立出来。

在 HikariCP 框架的使用上,咱们能够得知,若是使用无参构造初始化HikariCP,实际上是一个延迟初始化,在第一次获取链接的时候,才能初始化链接池。若是你们的应用,在启动以后可能有大量请求,致使大量数据库链接建立,那么使用无参构造能够会不太合适,会致使请求有阻塞,数据库压力加大。因此,无论在什么状况下,仍是要推荐你们使用有参构造初始化 HikariCP。

关于双重检查锁,你们还能够参考以下资料继续学习:

  1. http://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java

  2. https://www.cnblogs.com/xz816111/p/8470048.html