java并发编程之ThreadLocal

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需要注意的有两点:

  1. ThreadLocal实例本身是不存储值,它只是提供了一个在当前线程中找到副本值得key。
  2. 是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小伙伴会弄错他们的关系。

ThreadLocal的应用场景

  1. 最常见的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) {
            // 异常处理
        }
    }
}
  1. 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()方法,清除数据,避免造成内存泄露。
奋斗不一定成功,不奋斗一定不会成功!
原文地址:https://www.cnblogs.com/xucoding/p/11798976.html