美文网首页java基础
ThreadLocal全面分析

ThreadLocal全面分析

作者: 风一样的行者 | 来源:发表于2018-04-24 16:32 被阅读0次

1.ThreadLocal的作用

ThreadLocal是维持 线程封闭性 的一种规范的方式,这个类能使线程中的 某个值保存值的对象 关联起来。ThreadLocal提供了get与set等一系列的访问方法,这些方法为每一个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前线程在调用set时设置的最新值。

首先明确一个概念,那就是ThreadLocal并不是用来 并发控制访问一个共同对象,而是为了给每个线程分配一个只属于该线程的变量,顾名思义它是local variable(线程局部变量)。它的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突,实现线程间的数据隔离。从线程的角度看,就好像每一个线程都完全拥有该变量。

大体来说有以下三点:
1、每个线程都有自己的局部变量
每个线程都有一个独立于其他线程的上下文来保存这个变量,一个线程的本地变量对其他线程是不可见的。
2、独立于变量的初始化副本
ThreadLocal可以给一个初始值,而每个线程都会获得这个初始化值的一个副本,这样才能保证不同的线程都有一份拷贝。
3、状态与某一个线程相关联
ThreadLocal 不是用于解决共享变量的问题的,不是为了协调线程同步而存在,而是为了方便每个线程处理自己的私有状态而引入的一个机制,理解这点对正确使用ThreadLocal至关重要。

2.ThreadLocal的实现原理

首先来看下主要的方法:

1. get 方法** 源码如下:

    /**
     * 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);//通过当前线程对象获取value
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();//如果没有设置值,则返回初始值
    }

第一句是取得当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。我们先来看一下ThreadLocalMap的组成结构:

 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.
         */
        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继承了WeakReference(为什么使用 弱引用 待会再说),并且使用ThreadLocal作为键值。

然后接着下面获取到<key,value>键值对,注意这里获取键值对传进去的是 this,而不是当前线程t。
如果获取成功,则返回value值。
如果map为空,或者Entry为空,则调用setInitialValue方法返回设置的默认value。下面来看看 setInitialValue 方法:

    /**
     * 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();//这是ThreadLocal的一个protected,可以复写该方法来设置初始值
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);//获取当前线程的ThreadLocalMap对象
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);//如果map不存在则创建
        return value;
    }

    一般的用法如下,只要重写initialValue方法即可:
    //创建一个Integer型的线程本地变量    
        public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>() {    
            @Override    
            protected Integer initialValue() {    
                return 0;    
            }    
        }; 

很容易了解,就是如果map不为空,就设置键值对,为空,再创建Map,看一下createMap的实现:

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

到此,可能大部分朋友已经明白了ThreadLocal是如何为每个线程创建变量的副本的:

1.每条线程内部都有一个 ThreadLocal.ThreadLocalMap的局部变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

2.刚开始时,threadLocals变量为空,当调用ThreadLocal的get或set方法时,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

3.然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

2.set(T) 方法 源码如下:

    /**
     * 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);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

set方法相对就比较简单了,先通过当前的thread去取到ThreadLocalMap,如果map不存在,则创建,如果存在,则将当前的ThreadLocal对象作为key存储。

3.remove方法,源码如下:

        /**
         * Remove the entry for key.
         */
        private void remove(ThreadLocal key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);//获取当前key的地址
            for (Entry e = tab[i];//遍历整个table查找对应的key
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();//将当前的引用置空
                    expungeStaleEntry(i);//移除当前的key
                    return;
                }
            }
        }

该方法在处理内存泄漏时有用

来了解一下ThreadLocal的存储结构

ThreadLocal存储结构.png

4.内存泄漏与弱引用(WeakReference)

我们知道,所谓的弱引用是用来描述一些非必需的对象引用,弱引用有一个特点,就是被弱引用关联的对象,生命周期只能生存到下一次垃圾收集器工作之前。当垃圾收集器工作时,无论当前系统的内存是否足够,弱引用的对象都会被回收。

每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap。ThreadLocalMap中的key为一个threadlocal实例。这个Map的确使用了弱引用,不过弱引用只是针对key。每个key都弱引用指向threadlocal。当把threadlocal实例置为null以后,没有任何强引用指threadlocal实例,所以threadlocal将会被gc回收。但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用。

只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, ThreadLocalMap, value将全部被GC回收.

注: 实线代表强引用,虚线代表弱引用.

ThreadLocal中的引用关系.png

所以得出一个结论就是只要 这个线程对象被gc回收,就不会出现内存泄露。但是value在threadLocal设为null和线程结束这段时间不会被回收,就发生了我们认为的“内存泄露”。

因此,最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的,就可能出现内存泄露。

为了最小化内存泄露的可能性和影响,在ThreadLocal的get,set的时候,遇到key为null的entry就会清除对应的value。

所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,或者get,set方法调用时依然没有遇到key为null的entry,那么这个期间就会发生真正的内存泄露。

使用ThreadLocal需要注意,每次执行完毕后,要使用remove()方法来清空对象,否则 ThreadLocal 存放大对象后,可能会OOM。

5.ThreadLocal 的使用场景

1.数据库连接池

2.Session管理

3.消息机制中的looper

相关文章

网友评论

    本文标题:ThreadLocal全面分析

    本文链接:https://www.haomeiwen.com/subject/bmzdlftx.html