目录
ThreadLocal是什么?
- ThreadLocal,看名字很多第一次见到的人会认为是一个线程,将其归结到Thread。其实,我们可以这样想ThreadLocal是Thread修饰的Local,这个Local才是主角。ThreadLocal并不是一个Thread,而是Thread的局部变量。线程局部变量(ThreadLocal)的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。
- 通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。概括起来说,ThreadLocal为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
API
1. ThreadLocal()
创建一个线程本地变量。
2. T get()
返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本。
3. protected T initialValue()
返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用 get() 方法访问变量的时候。如果线程先于 get 方法调用 set(T) 方法,则不会在线程中再调用 initialValue 方法。
若该实现只返回 null;如果程序员希望将线程局部变量初始化为 null 以外的某个值,则必须为 ThreadLocal 创建子类,并重写此方法。通常,将使用匿名内部类。initialValue 的典型实现将调用一个适当的构造方法,并返回新构造的对象。
4. void remove()
移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。
5. void set(T value)
将此线程局部变量的当前线程副本中的值设置为指定值。
ThreadLocal使用示例
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(){
@Override
public void run(){
System.out.println("当前线程: " + Thread.currentThread().getName() + ", 已分配id: " + ThreadId.get());
}
}.start();
}
}
static class ThreadId{
private static final AtomicInteger id = new AtomicInteger(0);
private static final ThreadLocal<Integer> local = new ThreadLocal<>(){
@Override
protected Integer initialValue(){
return id.getAndIncrement();
}
};
// 返回当前线程的唯一的序列,如果第一次get,会先调用initialValue,后面看源码就了解了
public static int get(){
return local.get();
}
}
}
控制台打印结果:
当前线程: Thread-2, 已分配id: 1
当前线程: Thread-0, 已分配id: 2
当前线程: Thread-3, 已分配id: 4
当前线程: Thread-1, 已分配id: 0
当前线程: Thread-4, 已分配id: 3
ThreadLocal源码分析
1. public void set(T value)
public void set(T value) {
Thread t = Thread.currentThread(); // 取当前线程
ThreadLocalMap map = getMap(t); // 和当前线程关联的Map对象
if (map != null) {
map.set(this, value); // this是当前ThreadLocal对象,将value映射到和当前线程相关的Map中
} else {
createMap(t, value); // 不存在则创建
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到ThreadLocalMap是Thread对象中的一个属性。每个Thread对象都有一个ThreadLocal.ThreadLocalMap成员变量,ThreadLocal.ThreadLocalMap是一个ThreadLocal类的静态内部类(如下所示),所以Thread类可以进行引用.所以每个线程都会有一个ThreadLocal.ThreadLocalMap对象的引用。
通俗的讲,每一个Thread对象都有一个ThreadLocal.ThreadLocalMap成员变量(作为线程私有变量,从而也是线程安全的),而这些成员变量可以代理给ThreadLocal进行管理。
2. public T get()
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); // 如果Map中已经存在值,不管是set()方法设置,还是已经初始化过,都不再调用
}
对于ThreadLocal需要注意的有两点:
- ThreadLocal实例本身是不存储值,它只是提供了一个在当前线程中找到副本值得key。
- 是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小伙伴会弄错他们的关系。
ThreadLocal的应用场景
- 最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等
/**
* 数据库连接管理类
*/
public class ConnectionManager {
/** 线程内共享Connection,ThreadLocal通常是全局的,支持泛型 */
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
public static Connection getCurrConnection() {
// 获取当前线程内共享的Connection
Connection conn = threadLocal.get();
try {
// 判断连接是否可用
if(conn == null || conn.isClosed()) {
// 创建新的Connection赋值给conn(略)
// 保存Connection
threadLocal.set(conn);
}
} catch (SQLException e) {
// 异常处理
}
return conn;
}
/**
* 关闭当前数据库连接
*/
public static void close() {
// 获取当前线程内共享的Connection
Connection conn = threadLocal.get();
try {
// 判断是否已经关闭
if(conn != null && !conn.isClosed()) {
// 关闭资源
conn.close();
// 移除Connection
threadLocal.remove();
conn = null;
}
} catch (SQLException e) {
// 异常处理
}
}
}
- Hiberante的Session 工具类HibernateUtil
public class HibernateUtil {
private static Log log = LogFactory.getLog(HibernateUtil.class);
private static final SessionFactory sessionFactory; //定义SessionFactory
static {
try {
// 通过默认配置文件hibernate.cfg.xml创建SessionFactory
sessionFactory = new Configuration().configure().buildSessionFactory();
} catch (Throwable ex) {
log.error("初始化SessionFactory失败!", ex);
throw new ExceptionInInitializerError(ex);
}
}
//创建线程局部变量session,用来保存Hibernate的Session
public static final ThreadLocal session = new ThreadLocal();
/**
* 获取当前线程中的Session
* @return Session
* @throws HibernateException
*/
public static Session currentSession() throws HibernateException {
Session s = (Session) session.get();
// 如果Session还没有打开,则新开一个Session
if (s == null) {
s = sessionFactory.openSession();
session.set(s); //将新开的Session保存到线程局部变量中
}
return s;
}
public static void closeSession() throws HibernateException {
//获取线程局部变量,并强制转换为Session类型
Session s = (Session) session.get();
session.set(null);
if (s != null)
s.close();
}
}
ThreadLocal为什么会内存泄漏
一张图来看一下ThreadLocal对象以及和其相关的引用:
可以看出,以用有两个引用
ThreadLocal ---> 堆对象
Current Thread ---> Thread ---> ThreadLocalMap ---> Entry ---> ThreadLocal ---> 堆对象
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。但是这些被动的预防措施并不能保证不会内存泄漏:
使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。
ThreadLocal 最佳实践
综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
总结
- ThreadLocal 不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制。这点至关重要。
- 每个Thread内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量,该成员变量用来存储实际的ThreadLocal变量副本。
- ThreadLocal并不是为线程保存对象的副本,它仅仅只起到一个索引的作用。它的主要木得视为每一个线程隔离一个类的实例,这个实例的作用范围仅限于线程内部。
- 每次使用完ThreadLocal,都调用它的remove()方法,清除数据,避免造成内存泄露。