讲透ThreadLocal
讲透ThreadLocal
一. 简介
ThreadLocal是JDK提供的一个工具类,其作用是在多线程共享资源的情况下,使每个线程持有一份该资源的副本,每个线程的副本都是独立互不影响的。线程操作各自的副本,这样就避免了资源竞争引发的线程安全问题。
二. 使用示例
模拟Spring中的事务管理,每个事务与当前线程绑定,不同线程的事务之间相互独立互不影响。代码如下:
public class TransactionManager {
//业务线程
private static final class BizTask implements Runnable{
private final ThreadLocal<Transaction> transaction;
public BizTask(){
//创建事务,并与当前线程绑定
transaction = ThreadLocal.withInitial(() -> {
long id = Thread.currentThread().getId();
return new Transaction(id);
});
}
@Override
public void run() {
transaction.get().begin();
System.err.println("执行业务逻辑");
try {
Thread.sleep(1000L);
transaction.get().commit();
transaction.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//事务
private static final class Transaction {
private long id;
private TransactionStatus status;
public Transaction(long id) {
this.id = id;
}
public void begin(){
status = TransactionStatus.BEGIN;
System.err.println("开启事务, id: " + id + ", status: " + status);
}
public void commit() {
status = TransactionStatus.COMMIT;
System.err.println("提交事务, id: " + id + ", status: " + status);
}
public void rollback() {
status = TransactionStatus.ROLLBACK;
System.err.println("回滚事务, id: " + id + ", status: " + status);
}
}
//事务状态
private enum TransactionStatus {
BEGIN,
COMMIT,
ROLLBACK,
}
public static void main(String[] args) throws InterruptedException {
int threadNum = 5;
ExecutorService executor = Executors.newFixedThreadPool(threadNum);
//执行业务逻辑,可以看到每个线程的事务相互独立,互不影响
for (int i = 0; i < threadNum; i++) {
executor.submit(new BizTask());
}
executor.shutdown();
}
}
结果如下:
开启事务, id: 17, status: BEGIN
执行业务逻辑
开启事务, id: 14, status: BEGIN
执行业务逻辑
开启事务, id: 13, status: BEGIN
执行业务逻辑
开启事务, id: 16, status: BEGIN
执行业务逻辑
开启事务, id: 15, status: BEGIN
执行业务逻辑
提交事务, id: 14, status: COMMIT
提交事务, id: 17, status: COMMIT
提交事务, id: 13, status: COMMIT
提交事务, id: 16, status: COMMIT
提交事务, id: 15, status: COMMIT
三. ThreadLocal源码分析
ThreadLocal,线程本地变量,该变量为每个线程私有。ThreadLocal类有一个内部类,名为ThreadLocalMap,可以理解为一个简化版的HashMap。源码如下:
static class ThreadLocalMap {
//该Map的Entry,Key为ThreadLocal实例,Value为ThreadLocal对象所引用的值。
//这里使用了WeakReference弱引用,当Entry为null时可以尽快被GC
static class Entry extends WeakReference<ThreadLocal<?>> {
//与ThreadLocal关联的对象引用,为当前Entry的value
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//初始容量16
private static final int INITIAL_CAPACITY = 16;
//使用数组保存了所有的线程本地变量
private Entry[] table;
}
其中Entry为ThreadLocalMap的一个内部类,与HashMap的Entry结构类似,都是key-value对的形式。它的Key为ThreadLocal实例,Value为ThreadLocal对象所引用的对象。ThreadLocalMap使用一个Entry[]数组保存了所有的线程本地变量,因为一个线程可以维护多个ThreadLocal实例。
ThreadLocalMap内部保存了众多的ThreadLocal对象,既然说ThreadLocal是线程私有的,那么ThreadLocalMap是存放在哪里呢?
Thread类有一个成员变量——threadLocals,它就是保存了与当前Thread关联的一个ThreadLocalMap,源码如下:
//当前线程内部维护的ThreadLocalMap对象,用于保存所有ThreadLocal实例
ThreadLocal.ThreadLocalMap threadLocals = null;
可以看到,ThreadLocalMap对象保存在了Thread的内部,也即当前线程的私有内存中。通过上面的分析,我们可以了解ThreadLocal变量的大致内存结构如下:
ThreadLocal的主要方法为get()、set()和initialValue()。首先看set():
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程关联的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//创建一个Entry,以当前ThreadLocal对象为Key,待存储对象为Value,保存在ThreadLocalMap中
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
可以看到,set()的逻辑很简单,从当前线程中获取ThreadLocalMap,然后将该ThreadLocal的值保存在里面。
再看get()方法:
public T get() {
//获取当前线程对象
Thread t = Thread.currentThread();
//获取当前线程关联的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//从ThreadLocalMap获取以ThreadLocal为key的Entry的value
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果当前ThreadLocalMap不存在,则调用setInitialValue()方法,获取初始值
return setInitialValue();
}
ThreadLocal还有一个方法initialValue(),该方法提供给子类覆盖,以在创建ThreadLocal时指定初始值。
四. 应用场景
ThreadLocal最常见的使用场景为管理数据库连接Connection对象等。Spring中使用ThreadLocal来设计TransactionSynchronizationManager类,实现了事务管理与数据访问服务的解耦,同时也保证了多线程环境下connection的线程安全问题。
五. ThreadLocal的内存泄漏问题
- JVM引用类型
从源码中可以看出,ThreadLocalMap中Entry的key为ThreadLocal对象,并将其声明成了一个WeakReference弱引用。在分析其设计思想之前,先简单回顾下JVM中的几种引用类型:
- 强应用:普通的引用类型,被强引用的对象是一定不会被GC回收的。
- 软引用SoftReference:软引用一般用来实现内存敏感的缓存,如果有空闲内存就可以保留缓存,当内存不足时就清理掉,这样就保证使用缓存的同时不会耗尽内存。
- 弱引用WeakReference:它的生命周期比软引用还要短,在GC的时候,不管内存空间是否够用,都会回收WeakReference对象。
- 虚引用PhantomReference(较少使用):任何时候可能被GC回收,就像没有引用一样。
ThreadLocal之所以设计成WeakReference,目的就是当外部不再持有ThreadLocal的强引用时,尽快回收该ThreadLocalMap中对应的key。
- ThreadLocal的内存泄漏问题 如前文所述,当ThreadLocal没有外部强引用来引用它时,ThreadLocal对象就会在下次JVM垃圾收集时被回收,这个时候就会出现Entry中Key已经被回收,但是Value仍然所在,即所谓的"null Key"情况。此时外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就会一直存在一条强引用链:Thread --> ThreadLocalMap–>Entry–>Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收了,进而造成内存泄漏。
- JDK对于ThreadLocal内存泄漏的解决方案 JDK团队已经考虑到这样的情况,并做了一些措施来使得ThreadLocal尽量不会出现内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候,会清除掉ThreadLocalMap的所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。 以ThreadLocal的get()方法为例:
public T get() {
//获取当前线程实例
Thread t = Thread.currentThread();
//获取当前线程中的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//通过ThreadLocalMap的getEntry()方法获取Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
- 它会调用ThreadLocalMap的getEntry()方法获取Entry实例:
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
//如果Entry获取不到,则调用getEntryAfterMiss()
return getEntryAfterMiss(key, i, e);
}
- 如果获取不到,说明该key不存在或已经被回收了,则进入getEntryAfterMiss()方法:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
//如果key为null,则清除该Entry项
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
- 注意这里的k == null的情况,如果key为null,则执行expungeStaleEntry清除该Entry项。 此外,JDK推荐当ThreadLocal对象不再使用时,显式调用其remove()方法,清除该线程本地变量,最终也会调用上面的ThreadLocalMap.getEntryAfterMiss()方法。 综上,JDK也提供了相应的策略尽量避免ThreadLocal导致的内存泄漏问题,主要是在get()和remove()操作时清除已被回收的Entry项。但是该策略也并不是完美的,如果用户将ThreadLocal初始化后,再也不调用get()或remove()方法,则还是有内存泄漏的风险。
- 为什么要使用WeakReference? 从前文的描述中,很可能造成一种错觉:ThreadLocal由于使用了WeakReference而导致了内存泄漏。这其实是没有真正理解ThreadLocal内存泄漏的本质。首先我们来看看为什么要使用弱引用。下面是官方文档的说法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
Entry的key使用WeakReference弱引用,来处理大内存、长生命周期的线程的使用问题。
- 我们分别考虑下不使用WeakReferences和使用WeakReferences的情况下,分别会造成什么问题:
- 不使用WeakReferences,而使用强引用:外部引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致整个Entry内存泄漏。
- 使用使用WeakReferences:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set()、get()或remove()的时候会被清除。如果没有调用,则可能造成Entry的Value的内存泄漏。
由此可以看出,无论是否使用WeakReference,都有可能产生内存泄漏的情况,其根本原因在于ThreadLocalMap的生命周期与线程绑定。如果线程存活时间较长,且没有显式remove掉ThreadLocal对象,就有可能出现问题。而使用了WeakReference,至少可以保证无用的ThreadLocal对象被回收,不会出现整个Entry的内存泄漏,在一定程度上缓解了该问题。
- 总结 ThreadLocal的内存泄漏问题,根本原因是ThreadLocalMap的生命周期与Thread绑定,如果线程执行时间较长,则ThreadLocalMap就会一直不被GC回收。如果不显式调用remove()方法移除过期的ThreadLocal,则有可能造成内存泄漏。因此建议使用ThreadLocal时线程生命周期不要过长,且ThreadLocal对象使用完后显式调用remove()方法进行移除。
- RabbitMQ高可用集群配置
- zookeeper curator处理会话过期session expired
- redis事务
- 数据库表反向生成(一) MyBatis-generator与IDEA的集成
- 数据库表反向生成(二) Django ORM inspectdb
- RabbitMQ与AMQP协议
- 大数据算法设计模式(2) - 左外链接(leftOuterJoin) spark实现
- hs_err_pid
- django celery的分布式异步之路(二) 高并发
- django celery的分布式异步之路(一) 起步
- SpringMVC拦截器Interceptor
- 元宵快乐:看SQL大师们用SQL绘制的团圆
- Python Redis pipeline操作
- python concurrent.futures
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Greenplum集群扩容总结
- Leetcode刷题 237. 删除链表中的节点 两行代码实现
- Leetcode刷题 206. 反转链表 递归迭代两种方法实现
- MySQL索引和查询优化
- Elasticsearch:Index 生命周期管理入门
- springboot面试杀手锏-自动配置原理
- flink 1.11.2 学习笔记(1)-wordCount
- 我是如何开发维护8千多行代码组件的
- 我对JS延迟异步脚本的思考
- 大数据表查询优化 - 表分区
- 日志系统rsync和日志切割logrotate-Linux每日一练(9)
- Canvas 绘制点线相交
- Canvas监测雷达
- Canvas线条花环
- JQ俄罗斯方块儿