完全理解ThreadLocal

完全理解ThreadLocal

参考:http://www.iteye.com/topic/103804
http://www.iteye.com/topic/777716java

源码分析

  为了解释ThreadLocal类的工做原理,必须同时介绍与其工做甚密的其余几个类多线程

  • ThreadLocalMap(内部类)
  • Thread

  首先,在Thread类中有一行:并发

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

  其中ThreadLocalMap类的定义是在ThreadLocal类中,真正的引用倒是在Thread类中。同时,ThreadLocalMap中用于存储数据的entry定义:函数

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

  从中咱们能够发现这个Map的key是ThreadLocal类的实例对象,value为用户的值,并非网上大多数的例子key是线程的名字或者标识。ThreadLocal的set和get方法代码:源码分析

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    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();
    }

其中的getMap方法:this

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

给当前Thread类对象初始化ThreadlocalMap属性:线程

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

  到这里,咱们就能够理解ThreadLocal到底是如何工做的了设计

  1. Thread类中有一个成员变量属于ThreadLocalMap类(一个定义在ThreadLocal类中的内部类),它是一个Map,他的key是ThreadLocal实例对象。
  2. 当为ThreadLocal类的对象set值时,首先得到当前线程的ThreadLocalMap类属性,而后以ThreadLocal类的对象为key,设定value。get值时则相似。
  3. ThreadLocal变量的活动范围为某线程,是该线程“专有的,独自霸占”的,对该变量的全部操做均由该线程完成!也就是说,ThreadLocal 不是用来解决共享对象的多线程访问的竞争问题的,由于ThreadLocal.set() 到线程中的对象是该线程本身使用的对象,其余线程是不须要访问的,也访问不到的。当线程终止后,这些值会做为垃圾回收。
  4. 由ThreadLocal的工做原理决定了:每一个线程独自拥有一个变量,并不是是共享的,下面给出一个例子:
public class Son implements Cloneable{
    public static void main(String[] args){
        Son p=new Son();
        System.out.println(p);
        Thread t = new Thread(new Runnable(){  
            public void run(){
                ThreadLocal<Son> threadLocal = new ThreadLocal<>();
                System.out.println(threadLocal);
                threadLocal.set(p);
                System.out.println(threadLocal.get());
                threadLocal.remove();
                try {
                    threadLocal.set((Son) p.clone());
                    System.out.println(threadLocal.get());
                } catch (CloneNotSupportedException e) {
                    e.printStackTrace();
                }
                System.out.println(threadLocal);
            }}); 
        t.start();
    }
}

输出:code

Son@7852e922
java.lang.ThreadLocal@3ffc8195
Son@7852e922
Son@313b781a
java.lang.ThreadLocal@3ffc8195

也就是若是把一个共享的对象直接保存到ThreadLocal中,那么多个线程的ThreadLocal.get()取得的仍是这个共享对象自己,仍是有并发访问问题。 因此要在保存到ThreadLocal以前,经过克隆或者new来建立新的对象,而后再进行保存。
  ThreadLocal的做用是提供线程内的局部变量,这种变量在线程的生命周期内起做用。做用:提供一个线程内公共变量(好比本次请求的用户信息),减小同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度,或者为线程提供一个私有的变量副本,这样每个线程均可以随意修改本身的变量副本,而不会对其余线程产生影响。对象

如何实现一个线程多个ThreadLocal对象,每个ThreadLocal对象是如何区分的呢?
查看源码,能够看到:

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
      return nextHashCode.getAndAdd(HASH_INCREMENT);
}

  对于每个ThreadLocal对象,都有一个final修饰的int型的threadLocalHashCode不可变属性,对于基本数据类型,能够认为它在初始化后就不能够进行修改,因此能够惟一肯定一个ThreadLocal对象。
  可是如何保证两个同时实例化的ThreadLocal对象有不一样的threadLocalHashCode属性:在ThreadLocal类中,还包含了一个static修饰的AtomicInteger([əˈtɒmɪk]提供原子操做的Integer类)成员变量(即类变量)和一个static final修饰的常量(做为两个相邻nextHashCode的差值)。因为nextHashCode是类变量,因此每一次调用ThreadLocal类均可以保证nextHashCode被更新到新的值,而且下一次调用ThreadLocal类这个被更新的值仍然可用,同时AtomicInteger保证了nextHashCode自增的原子性。

为何不直接用线程id来做为ThreadLocalMap的key?
  这一点很容易理解,由于直接用线程id来做为ThreadLocalMap的key,没法区分放入ThreadLocalMap中的多个value。好比咱们放入了两个字符串,你如何知道我要取出来的是哪个字符串呢?
  而使用ThreadLocal做为key就不同了,因为每个ThreadLocal对象均可以由threadLocalHashCode属性惟一区分或者说每个ThreadLocal对象均可以由这个对象的名字惟一区分(下面的例子),因此能够用不一样的ThreadLocal做为key,区分不一样的value,方便存取。

public class Son implements Cloneable{
    public static void main(String[] args){
        Thread t = new Thread(new Runnable(){  
            public void run(){
                ThreadLocal<Son> threadLocal1 = new ThreadLocal<>();
                threadLocal1.set(new Son());
                System.out.println(threadLocal1.get());
                ThreadLocal<Son> threadLocal2 = new ThreadLocal<>();
                threadLocal2.set(new Son());
                System.out.println(threadLocal2.get());
            }}); 
        t.start();
    }
}

ThreadLocal的内存泄露问题
  根据上面Entry方法的源码,咱们知道ThreadLocalMap是使用ThreadLocal的弱引用做为Key的。下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:

  如上图,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设计时的对上面问题的对策:
ThreadLocalMap的getEntry函数的流程大概为:

  1. 首先从ThreadLocal的直接索引位置(经过ThreadLocal.threadLocalHashCode & (table.length-1)运算获得)获取Entry e,若是e不为null而且key相同则返回e;
  2. 若是e为null或者key不一致则向下一个位置查询,若是下一个位置的key和当前须要查询的key相等,则返回对应的Entry。不然,若是key值为null,则擦除该位置的Entry,并继续向下一个位置查询。在这个过程当中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,天然会被回收。仔细研究代码能够发现,set操做也有相似的思想,将key为null的这些Entry都删除,防止内存泄露。
      可是光这样仍是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的getEntry函数或者set函数。这固然是不可能任何状况都成立的,因此不少状况下须要使用者手动调用ThreadLocal的remove函数,手动删除再也不须要的ThreadLocal,防止内存泄露。因此JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,因为一直存在ThreadLocal的强引用,因此ThreadLocal也就不会被回收,也就能保证任什么时候候都能根据ThreadLocal的弱引用访问到Entry的value值,而后remove它,防止内存泄露。

关于ThreadLocalMap内部类的简单介绍   初始容量16,负载因子2/3,解决冲突的方法是再hash法,也就是:在当前hash的基础上再自增一个常量。

相关文章
相关标签/搜索