ThreadLocal源码解析

ThreadLocal类

维持线程封闭性除了Ad-hoc封闭和栈封闭,更规范的方法是使用ThreadLocal,这个类是java lang包下的一个类。他能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立地副本,因此get总能返回由当前执行线程在调用set时设置的最新值。

源码分析

set方法:

  /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();//先拿到当前线程
        ThreadLocalMap map = getMap(t);//再拿到当前线程的ThreadLocalMap 
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

ThreadLocal和ThreadLocalMap :
其实ThreadLocalMap 就是线程线程自身的一个成员属性threadLocals的类型。也就是线程本地数据都存在这个threadLocals应用的ThreadLocalMap中。

/**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

再看看ThreadLocalMap :
它里面有个静态内部类Entry,构造方法有两个参数:一个是ThreadLocal对象,一个是需要隔离访问的变量。

/**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {//两个变量
                super(k);
                value = v;
            }
        }

再看看它和ThreadLocalMap的关系:
Entry对象存储在这个table数组中,数组的下标是threadLocal的threadLocalHashCode&(INITIAL_CAPACITY-1),因为数组的大小是2的n次方,那其实这个值就是threadLocalHashCode%table.length,用&而不用%,其实是提升效率。只要数组的大小不变,这个索引下标是不变的,这也方便去set和get数据。

 /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];//用数组保存,可能有多个变量需要线程隔离访问
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

看到这里大家应该就明白了,每个线程自身都维护着一个ThreadLocalMap,用来存储线程本地的数据,可以简单理解成ThreadLocalMap的key是ThreadLocal变量,value是线程本地的数据。就这样很简单的实现了线程本地数据存储和交互访问。如图:

场景:

ThreadLocal对象通常用于防止对可变的但实例变量(Singleton)或全局变量进行共享。例如:在单线程应用程序中可能会维持一个全局的数据库连接,并在层序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个Connection对象。由于JDBC的连接对象不一定是线程安全的,因此,当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的。通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接。如:

private static ThreadLocal<Connection> conmect ionHolder = new ThreadLocal<Connection> (){
public Connection initialValue(){
return DriverManager . getConnection(DB URL) ;
	}
};
public static Connection getConnection(){
return connectionHolder .get () ;
}

当某个频繁执行的操作需要一个临时对象,例如一个缓存区,而又同时希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。

ThreadLocal引发的问题

内存泄漏问题

因为其中ThreadLocalMap是弱引用,挡GC发生时会被回收,其中引用链如图
在这里插入图片描述
当ThreadLocal指向null时,没有任何强引用指向实例,所以ThreadLocal会被gc回收,所以就会出现key为空的Entry,对应的value也将无法被访问,便存在着内存泄漏,而只有当前thread结束后,current thread不在栈中,强引用断开,gc才能回收。所以最好的方法就是在ThreadLocal不用是,用remove()来清除数据。

而仔细看get() set() 源码,则会发现其中都调用了expungeStaleEntry()来清除Entry中key为空的数据,但是这是不及时的,还是会存在内存泄漏。只有remove()中显式调用了expungeStaleEntry()。

从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得 思考:为什么使用弱引用而不是强引用?

下面我们分两种情况讨论

key 使用强引用:对ThreadLocal 对象实例的引用被置为 null 了,但是 ThreadLocalMap 还持有这个 ThreadLocal 对象实例的强引用,如果没有手动删除, ThreadLocal 的对象实例不会被回收,导致 Entry 内存泄漏。

key 使用弱引用:对 ThreadLocal 对象实例的引用被被置为 null 了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 的 对象实例也会被回收。value 在下一次 ThreadLocalMap 调用 set,get,remove 都 有机会被回收。

比较两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可 以多一层保障。

因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引用。

总结

  • JVM 利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。
  • JVM 利用调用 remove、get、set 方法的时候,回收弱引用。
  • 当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,而不再去调用 remove、 get、set 方法,那么将导致内存泄漏。
  • 使用线程池+ ThreadLocal 时要小心,因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了 value 可能造成累积的情况。

安全性问题

ThreadLocalMap 中保存的其实是对象的一个引用,这样的话,当有其 他线程对这个引用指向的对象实例做修改时,其实也同时影响了所有的线程持有 的对象引用所指向的同一个对象实例。
所以在存在多个对同一数据的写操作存在竞争时,容易引发安全性问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值