从数据库连接与线程的关系说起
我们知道,数据库连接是很宝贵的资源,以MySQL为例,一台MySQL服务器最大连接数默认是100, 最大可以达到16384。但现实中最多是到200,再多MySQL服务器就承受不住了。因为mysql连接用的是tcp协议,稳定的同时意味着需要消耗更多时间和性能去建立维持连接。为了能最大利用如此宝贵的数据库连接,我们希望数据库连接能被复用而不需要频繁创建,所以利用池化技术建立数据库连接池。
那么数据库连接与线程是什么关系呢?
了解连接池在多线程场景中是如何工作的。
一个连接池包含多个连接对象。我们可以配置池的大小。
当多个线程需要并发访问一个数据库时,它们会从连接池中请求连接对象。
如果池中仍有空闲连接,线程将获取连接对象并开始其数据库操作。线程完成工作后,会将连接返回到池中。
如果池中没有空闲连接,线程将等待另一个线程将连接对象返回到池中。
所以,一个连接在同一时间只能被一个线程所持有,一个线程在同一时间也只能申请到一个连接,官方也不鼓励在多个线程之间共享连接对象
明白了这点之后再看下线程,我们知道一个线程在生命周期内会做很多事情,比如参数校验,权限验证,数值计算,然后持久化结果。其中可能只有持久化结果环节需要访问JDBC数据库连接,其余的时间范围内,JDBC数据库连接 都是空闲状态。所以如果线程整个生命周期中独占JDBC数据库连接的话,那么这个连接空闲率就很高也就变得很浪费,因为一旦占用了数据库连接可以不限次数执行事务SQL请求,增删查改各种sql操作请求都可以执行。为了提高数据库连接的使用率,目前普遍的解决方案是:当线程需要做数据库操作时,才会真正请求获取JDBC数据库连接,线程使用完了之后,立即释放,被释放的JDBC数据库连接等待下次分配使用
多线程访问同一个数据库连接
前面已经说了,数据库连接在同一时间只能被一个线程所持有,线程在申请数据库连接时也是线程安全的。
Java多线程访问同一个java.sql.Connection会导致事务错乱。解决多个线程访问同一个Connection对象时,必须遵循两个基本原则:
- 以资源互斥的方式访问Connection对象;
- 在线程执行结束时,应当最终及时提交(commit)或回滚(rollback)对Connection的影响;不允许存在尚未被提交或者回滚的语句。
ThreadLocal的原理
想了解下ThreadLocal的原理可以看下这篇文章:ThreadLocal就是这么简单
ThreadLocal和Thread的关系如下图:
ThreadLocal里面定义了ThreadLocalMap这个静态内部类。而Thread类里持有了ThreadLocalMap的引用。
- ThreadLocal在初始化时会对当前线程所持有的ThreadLocalMap引用进行实例化。
- ThreadLocal的set方法即是拿出当前线程所持有的ThreadLocalMap引用,然后将所要保存的值塞进这个map里,key是ThreadLocal
- ThreadLocal的get方法是根据当前线程所持有的ThreadLocalMap引用,用ThreadLocal 这个key拿出value。
ThreadLocal 与多线程还有数据库连接的关系
我们经常听说ThreadLocal的目的是为了解决多线程访问资源时的共享问题。这种说法是完全错误的。
ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
举个例子,一个线程的生命周期要做这么几件事情:参数校验,业务逻辑1,数据持久化,业务逻辑2,数据库切换,业务逻辑3。
该线程在进行数据持久化时需要申请数据库连接,当做完之后就将连接释放回线程池,但之后进行数据库切换时也需要数据库连接,这时候继续申请吗?不是的,这时ThreadLocal就派上用场了,在第一次数据持久化操作做完之后,将这个数据库连接暂存在ThreadLocal中,在之后的数据库切换中需要用到时再拿出来用。由此可见,ThreadLocal对线程来说相当于提供一个局部变量,线程把资源用完之后先放到ThreadLocal里,之后需要用到时再拿出来。在多租户场景下,因为经常需要切换数据库,所以ThreadLocal很常用。
那么在多线程场景下ThreadLocal的表现是咋样呢?
如图所示,多线程下,不同线程会持有不同的ThreadLocalMap引用,但这些ThreadLocalMap的key都是指向同个ThreadLocal对象,所以可以理解为什么代码中在用到ThreadLocal时都会设为static了吧,就是为了让多线程下ThreadLocalMap的key都使用的是同一个ThreadLocal。将ThreadLocalMap的key设为ThreadLocal是JDK1.3之后的改进,JDK1.3之前key是Thread,Thread有多少个就会导致key有多少个,现在是ThreadLocal的数量,这样设计之后每个Map的Entry数量变小了,能提高性能。
为什么ThreadLocal会造成内存泄漏
从上文可以知道,当为ThreadLocal 变量赋值时,实际上就是以当前 ThreadLocal 实例为 key,值为 Entry 往这个 ThreadLocalMap 中存放。ThreadLocal是弱引用,当ThreadLocal被GC回收时,ThreadLocalMap就会出现 key 为 null 的 Entry,也就没办法访问这些 key 对应的 value,而ThreadLocalMap作为被Thread持有的引用,其生命周期是跟随线程类的,在实际项目里线程为了复用又是不会主动结束的,所以这些 key 为 null 的 value 就会一直存在一条强引用链:Thread --> ThreadLocalMap-->Entry-->Value,无法回收就会造成内存泄漏。
举个例子,假设web服务器是Tomcat,Tomcat存在线程池,每当有http请求过来时就会为这个请求分配一个线程,当线程执行完后会被归还到线程池,但线程没有被销毁。此时如果不在线程被归还时调用Threadlocal.remove方法的话,因为线程一直存在,所以该线程的ThreadLocalMap的这个Entry键值对(Threadlocal - 值)也同样一直存在,从而造成内存泄漏。
为了避免这个问题,在每次使用完 ThreadLocal 之后,最好明确调用 ThreadLocal 的 remove 方法来删除与当前线程关联的值。这样可以确保线程再次使用时不会存储旧的、不再需要的值,从⽽避免内存泄漏