前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一、HikariCP获取连接流程源码分析一

一、HikariCP获取连接流程源码分析一

原创
作者头像
用户1422411
发布2022-06-25 17:47:12
1.2K0
发布2022-06-25 17:47:12
举报
文章被收录于专栏:HikariCP源码解析系列

欢迎访问我的博客,同步更新: 枫山别院

源代码版本2.4.5-SNAPSHOT

HikariDataSource的getConnection()方法

HikariCP获取连接的方法是com.zaxxer.hikari.HikariDataSource#getConnection(), 这个方法在HikariDataSource类中。HikariDataSource类中是 HikariCP 提供用户使用的主要类,有获取连接,关闭连接池,剔除连接等方法。我们主要看一下getConnection(), 这是对外暴露的获取连接的方法,不管是Spring获取连接还是我们自己手工调用 HikariCP,都是调用这个方法从连接池中取连接。

代码如下:

代码语言:java
复制
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()方法中。

但是,我们还是要分析一下的,毕竟,我们看开源代码的目的是学习大师的设计和技巧。

①检查连接池状态

代码语言:java
复制
//①

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 跑冒烟。

越是了解底层,越能写出更好的代码。学习了这些优秀的框架,我的感慨是:那些年上大学睡的觉,终究是要还的,现在终于到时候了.......

② 两个连接池?

代码语言:java
复制
//②

if (fastPathPool != null) {

   return fastPathPool.getConnection();

}

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

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

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

代码语言:java
复制
private final HikariPool fastPathPool;

private volatile HikariPool pool;

除了变量名字不同之外,他们的修饰符也不一样,fastPathPoolfinal的,poolvolatile的。volatile在上面已经解释过了,就是为了线程安全嘛,保证多线程情况下pool的值是一致的。fastPathPool呢,是final的,HikariDataSource初始化的时候必须赋值,之后就改不了了对吧。

其实这里涉及到了HikariCP 连接池的创建方式。HikariDataSource有两个构造方法,第一个是无参构造:

代码语言:java
复制
public HikariDataSource() {

      super();

      fastPathPool = null;

   }

第二个是有参的:

代码语言:java
复制
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

③双重检查锁

代码语言:java
复制
//③

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • HikariDataSource的getConnection()方法
    • ①检查连接池状态
      • ② 两个连接池?
        • ③双重检查锁
        相关产品与服务
        数据库
        云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档