理解ThreadLocal

0 快速理解

ThreadLocal的实现机制不复杂,它将自身实例作为key,和需要保存的value版本值,一起存入到当前线程所持有的一个map当中,代码可以简单的描写为(当然实际的代码并不是这样):

`Thread.currentThread().threadLocals.put(this, value);// threadLocal.set(T)

Thread.currentThread().threadLocals.get(this);// threadLocal.get()`

每个线程都有自己的一个独立的map,这样就实现了每个线程独立地改变自己的副本,不会和其它线程冲突。

1 简介

ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量)。也许把它命名为ThreadLocalVar更加合适。

线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。

2 ThreadLocal的应用

  • 在Spring中应用

Spring使用ThreadLocal解决线程安全问题。一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,这样有状态的Bean就可以在多线程中共享了。

  • 在Slf4j 日志输出中的应用

Java Web项目中,通常使用实现了 Slf4j 的Logback或Log4j来进行日志输出,Slf4j 中定义了 MDC 接口,要求实现多线程间日志隔离,Logback 和 Log4j 正是利用ThreadLocal来实现的。更多内容将会在另一篇专门介绍MDC的文章中讲解。

3 实现原理

前面提到:
ThreadLocal将自身实例作为key,和需要保存的value(具体大变量副本值)一起存入到当前线程的一个map当中。
这段描述涉及三个类:Thread、ThreadLocal、ThreadLocalMap,它们实际的关系如下:

  • ThreadLocal中声明了一个静态内部类ThreadLocalMap。可以将它视为一个map容器,key是ThreadLocal类型,value是Object。
  • ThreadLocal类中定义了:
//静态内部类
	static class ThreadLocalMap {

        /**
         * 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.
         */
       ...
       
       }
  • Thread类中拥有一个ThreadLocalMap类型的成员变量。这个成员变量ThreadLocalMap便是此线程用来存储各个ThreadLocal变量在本线程中具体副本值的。
  • Thread类:
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
     //成员变量
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

下面来看一下ThreadLocal工作原理。
1) 给某个ThreadLocal变量, 设置一个与当前线程所绑定的具体值。

-> 使用ThreadLocal的set()方法:

    /**
     * Sets the current thread's copy of this thread-local variable to the specified value. 
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

其中:
(1)Thread.currentThread()是一个native方法,可以拿到当前的线程t。

(2)通过getMap(Thread t)方法获取当前线程所持有的ThreadLocalMap;

(3)然后将变量的副本值设置到这个ThreadLocalMap对象中(key=this,即当前ThreadLocal实例,value=传入的副本值value)。

(4)当然如果(2)中获取到的ThreadLocalMap对象为空,就通过createMap方法给线程t创建一个新ThreadLocalMap,并将key-value存入。

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

   /**
    * Create the map associated with a ThreadLocal. Overridden in
    * InheritableThreadLocal.
    *
    * @param t the current thread
    * @param firstValue value for the initial entry of the map
    */
   void createMap(Thread t, T firstValue) {
       t.threadLocals = new ThreadLocalMap(this, firstValue);
   }

2) 获取某个ThreadLocal变量, 与当前线程所绑定的具体副本值。

/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    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();
    }

其中:
(1)Thread.currentThread()是一个native方法,可以拿到当前的线程t。
(2)通过getMap(Thread t)方法获取当前线程所持有的ThreadLocalMap;
(3)如果ThreadLocalMap不为空,则读取map中的value;
(4)如果ThreadLocalMap为空,则创建一个map,并返回初始化的值。

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

从这里可以看出线程隔离的秘密就在于:
每个线程中都有一个独立的ThreadLocalMap,存放着它自己所拥有的各个ThreadLocal变量副本值。
在外部代码中对ThreadLocal进行get或set时,能够获取到当前线程(它所在的线程),进而可以获取到当前线程对应的map,所以实际操作的一直是本线程所对应的ThreadLocalMap版本。每个线程的ThreadLocalMap只能自己被读取和修改。
这样在不同线程中对ThreadLocal进行操作时,实际修改和读取的是不同的ThreadLocalMap。从而实现了变量访问在不同线程中的隔离。

4 内存泄漏问题

为什么会有内存泄漏

内存模型图

Thread是通过ThreadlocalMap的key的形式对Threadlocal产生了弱引用。
当threadlocal实例的强引用都被释放以后, threadlocal实例本身可能会在gc时被清除。由于key不存在了,所以map里面对应的value永远不会被访问到了。然而value却没有被回收,所以存在着内存泄露。

WeakReference是Java语言规范中为了区别直接的对象引用而定义的另外一种引用关系。它的标志性的特点是:reference实例不会影响到被引用对象的GC回收行为(可以理解为弱引用,不在GC前对对象被引用情况的统计范围内),只不过在被对象回收之后,reference实例想获得被应用的对象时程序会返回null。

扩展一下,Java中HashMap类型中对键是强引用。同时java也提供WeakHashMap,它对键是是弱引用。但是在WeakHashMap中,当key可以被GC时,整个Entry都会被清除。

为什么使用的是弱引用

当threadLocal实例可以被GC回收时,系统可以检测到该threadLocal对应的Entry是否已经过期来自动做一些清除工作,否则如果不清除的话容易产生内存无法释放的问题。
如果对threadLocal不是弱引用,即使除了作为threadLocalMap的value对象即使在其他位置不再使用,也会被所引用导致无法被GC回收。

避免内存泄漏的最佳实践

1)ThreadLocalMap会在set,get以及remove等方法中会对key已经被回收的entry做自动删除。
(set以及get不保证所有过期slots会在操作中会被删除,而resize则会删除threadLocalMap中所有的过期slots)

例如:在ThreadLocal的get方法中,使用了map.getEntry()方法,getEntry()中会对查找不到的key进行清除。

       /**
         * Get the entry associated with key.  This method
         * itself handles only the fast path: a direct hit of existing
         * key. It otherwise relays to getEntryAfterMiss.  This is
         * designed to maximize performance for direct hits, in part
         * by making this method readily inlinable.
         */
        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
                return getEntryAfterMiss(key, i, e);//此处着手清除key为null的Entry
        }

2)最安全的办法是调用ThreadLocal的remove方法,把当前ThreadLocal从当前线程的ThreadLocalMap中移除(包括key,value)。来彻底避免可能的内存泄露。

每次使用完ThreadLocal,都调用它的remove()方法,清除无用数据。

5 总结

ThreadLocal使用场合主要解决多线程中数据数据因并发产生不一致问题。
ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,但是大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。
ThreadLocal所保存的变量只能是Object类型,不能使用基本数据类型。

参考文献:深入分析 ThreadLocal 内存泄漏问题

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值