通过阅读本遍你将获取的知识
- ThreadLocal 使用方法
- ThreadLocal 适合使用的场景
- ThreadLocal实现方法与原理
- ThreadLocalMap实现方法与原理
- Thread如何存储ThreadLocalMap
- ThreadLocalMap内存泄漏原因与避坑方法
ThreadLocal被解释为线程本地变量,生命周期与和它绑定的线程相同,在新创建一个线程时会初始化一个ThreadLocal实例,线程销毁时与会被一同销毁,在线程内部只能访问到本线程自己的ThreadLocal实例变量。
下面通过一个例子来理解ThreadLocal
public static class ConnectionManger{
private static Connection connection;
public static Connection openConnection() throws SQLException {
if(connection==null){
connection=DriverManager.getConnection("");
}
return connection;
}
public static void close() throws SQLException {
if(connection!=null){
connection.close();
connection=null;
}
}
}
public void demo() throws SQLException {
Connection connection = ConnectionManger.openConnection();
connection.close();
}
ConnectionManger 类为一个管理数据库连接的类,在内部有一个共享变量connection
。
单线程的应用环境下调用demo方法不会出现什么问题,运行也正常。
多线程环境下就不好使了,调用demo方法的结果将会不可控,可能什么出现多个线程使用现一个Connection
对象,一个关闭连接后其它的出现异常。
解决多线程中的问题可以在ConnectionManager的方法中添加同步块,通过同步块保证多线程环境下的线程安全,但ConnectionManager将会成为多线程环境下访问数据库能力上的瓶颈,所有的数据读写都将串行化执行。
解决性能瓶颈的办法是将ConnectionManager
也多线程化,将它做为一个连接存储池(数据库连接池),通过动态配置池的大小来控制数据读写能力。那如何解决业务上多线程使用Connection
实例的问题呢?
ThreadLocal
主角登场,通过上面问题的分析,每一个业务线程在需要使用数据库连接时,通过调用**ConnectionManger.openConnection()方法获取一个连接,在业务线程中通过connection实例完成业务操作,再通过ConnectionManger.close()**关闭连接;业务之前是相互隔离的,每一个业务分配一个connection
就可以解决串行化的问题。
每个线程都有一个ThreadLocal实例,将共享的connection
实例存储在ThreadLocal,在线程中执行的业务只需要获取ThreadLocal中的connection实例完成数据读写即可,下面看下优化后的ConnectionManager对象:
public void demo() throws SQLException {
Connection connection = ConnectionManger.openConnection();
connection.close();
}
public static class ConnectionManger {
static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
//模拟连接池获取连接
private static Connection openNewConnection() {
try {
return DriverManager.getConnection("");
} catch (SQLException ignore) {
}
return null;
}
public static Connection openConnection() throws SQLException {
threadLocal.set(ConnectionManger.openNewConnection());
return threadLocal.get();
}
public static void close() throws SQLException {
threadLocal.get().close();
threadLocal.remove();
}
}
在ConnectionManager对象中引用ThreadLocal<Connection>
变量类型存储当前线程中使用的Connection实例。
通过优化解决了多线程中线程对资源竞争的问题,通过添加同步块导致的性能问题。
你好奇ThreadLocal怎么实现的吗?我好奇
ThreadLocal实现
ThreadLocal实例供外部使用的三个方法分别是:
ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
threadLocal.get();//获取值
threadLocal.set(<Object>);//设置值
threadLocal.remove();//移除值
get()
用于获取存储在当前线程中值,set()
用于将值存储在当前线程中,remove()
用于移除当前线程中的值。
下面来简单过下各个方法是怎么实现的:
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取线程中的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//获取Map中的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//没有map时进行初始化
return setInitialValue();
}
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取线程中的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//将值存储在Map
map.set(this, value);
} else {
//新创建一个Map
createMap(t, value);
}
}
public void remove() {
//获取当前线程的Map
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
//将值从Map中移除
m.remove(this);
}
}
//获取线程的Map
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
从上面的源码中可以得到几个关键知识点:
- Thread使用ThreadLocalMap管理引用的值,ThreadLocalMap是ThreadLocal内部类
- ThreadLocalMap实例存储在线程的
threadLocals
变量中 - ThreadLocal的
set
get
remove
实例操作的是当前线程中存储在threadLocals变量中的值 - Thread中的threadLocals可能为null,为null时
get
set
时会进行初始化
ThreadLocalMap
要完全搞清楚ThreadLocal的底层原理,ThreadLocalMap的实现肯定是要搞清楚的。
在内部和Map的思路比较像,但存储池改为了数组,Entry对象继承自WeakReference弱引用类型,和Map类型相似点在于有扩容操作,ThreadLocalMap不继承于Map类型,只在思想上相同。
可能有小伙伴要问了,为什么不直接用Map类型来存储呢,在存储数据量比较多时查找性能优于数组;这里解释下没有用Map类型的原因于一般每个线程中存储在ThreadLocalMap中的数据不会特别多,在这样的情况下使用数组的性能就要优于Map类型了,查找时不用计算Hash值,数据量大于扩容也比较简单。
ThreadLocalMap内存泄漏
ThreadLocalMap使用内部Entry做为存储数据的结构,先来看下实现代码:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
在Entry内部将ThreadLocal实例做为Key,将值做为value变量,ThreadLocalMap的存储池中将直接存储Entry实例,从Entry类的结构上可以看出继承了WeakReference
类型,在构建方法中对TheadLocal实例的引用值调用 super(k)
,这样做的结果是Entry类型中的key对ThreadLocal的值引用为弱引用类型;
在JVM的GC回收中,如果一个值只是被弱引用了,那么将在GC回收时对这个引用进行回收。
这又怎样能导致内存泄漏呢?
根据弱引用的特性,一个值只被弱引用时会被GC回收,那一起来分析下下面的示例代码:
public static class ConnectionManger {
private static Connection openNewConnection() {
try {
return DriverManager.getConnection("");
} catch (SQLException ignore) {
}
return null;
}
public static Connection openConnection() throws SQLException {
ThreadLocal<Connection> local = ThreadLocal.withInitial(ConnectionManger::openNewConnection);
return local.get();
}
}
设上面的代码运行在多线程环境,且线程使用线程池管理,线程池没有停步且在任务队列中还有等待执行的任务。
在openConnection
中使用ThreadLocal.withInitial
创建了一个ThreadLocal实例,随后使用get
方法返回值。
在这个示例的openConnection
方法中没有对ThreadLocal实例做强引用,在方法执行完成后局部变量将会被回收,随后也没有调用ThreadLocal
的实例方法remove
将引用的值移除掉。
现在我们来分析下内存中JVM进行GC后的情况:
- 使用
ThreadLocal.withInitial
创建TheadLocal实例后再使用get
方法获取时会将初始值存入线程对应的ThreadLocalMap中,在TheadLocalMap中会使用Entry类型将ThreadLocal类型值做为key,将Connection类型值做为value - JVM开始执行GC对内存进行回收,回收时发现Key是弱引用,在其它地址没有再引用(没有强引用),JVM对Key的值进行回收
在JVM执行完GC后key被成功回收,但value引用的值并没有被回收,这时ThreadLocalMap中的Entry的Key将变为null,而value则没有变,因为没有执行remove
操作,这条记录将会一直存在于ThreadLocalMap中直到线程被销毁后导致线程引用的ThreadLocalMap实例被回收才会将泄漏的内存进行回收。
精彩推荐:
包邮赠书!《阿里巴巴Java开发手册 第二版》
知识点:Java sychronized 内部锁实现原理
知识点: Java FutureTask 使用详解
Java ClassLoader详解双亲委派的实现原理