并发基础(十) 线程局部副本ThreadLocal之正解

2018/02/28 23:33
阅读数 5

  本文将介绍ThreadLocal的用法,并且指出大部分人对ThreadLocal 的误区。

先来看一下ThreadLocal的API:

1、构造方法摘要

ThreadLocal(): 创建一个线程本地变量。

2、方法摘要

void set(T value): 将此线程局部变量的当前线程副本中的值设置为指定值。 T get(): 返回此线程局部变量的当前线程副本中的值。 void remove():移除此线程局部变量当前线程的值。 protected T initialValue():返回此线程局部变量的当前线程的“初始值”。是protected方法,是为了让子类继承而设计的。

ThreadLocal 类很简单,下面接着抛出两个误区,以这两个误区为起点,进行分析,逐步揭开ThreadLocal 的真面目。

一、两大误区

1、误区一 ThreadLlocal 的出现是为了解决多线程共享对象的问题。

  网上不少的文章对ThreadLocal有着很糟糕的错误认识,认为ThreadLocal可以为每一个共享对象保持一个副本,这样就可以解决多线程并发竞争资源的问题。本人在入门并发时,也是这么认为的,但随着工作实战的经验增加,根本就不是那么一回事。   我们来分析一下。假设ThreadLocal是能够解决多线程共享对象的问题,于是我们为每一个线程都维护一个该对象的独立副本(先不考虑内存的问题)。如果都是读线程,那么问题不大。但如果有写线程呢?<font color="blue">写线程修改了副本,但是其他读线程读取到还是旧的值,这样线程之间无法通信,共享对象就失去意义了(共享对象是线程通信的一种方式,在一个线程修改了,另一个线程也应该看的见)。</font>如果仅仅都是读线程,要维护这么多副本,消耗大量内存,而且在多线程的环境下,只能读取的话,可以不加锁,那么竞争就不存在,不需要额外维护多个副本。<font color="red">经过上面的分析,ThreadLocal 是不可能解决多线程共享对象的问题</font>

那么 ThreadLocal 的真正作用是什么呢?

  看一下JDK的源码注释:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

对应的中文应该是这样的:

该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

好像还是不太懂,那么再具体一点就是: 如果当类中的某个变量希望根据不同的线程提供不同的值,而且任意一个线程修改这个变量不会影响到其他线程,那么这个变量就应该是线程的局部变量。 一般的用法是用 private static 修饰变量,这是因为ThreadLocalMap中的key值是一个弱引用,是以ThreadLocal为key,所以要用static来延长ThreadLocal的生存时间,后续讲到。

误区二 ThreadLocal 的底层实现是一个Map,key是当前线程,value是局部变量

  ThreadLocal的底层维护着一个Map,key是 Thread.currentThread(当前线程),value 则是要保持的局部变量。这种设计思路是所有人最容易想到的,也是最容易被大家所误解的方案,其实这也是早期JDK的设计方案(好像是JDK1.2)。(简称为 方案A

  但后期的JDK中,改善了ThreadLocal的设计,也是本文的重点,先简单说一下设计:<font color="blue">不同于方案A的只有一个Map的设计,此方案的每一个Thread 对象中各自维护着一个ThreadLocal.ThreadLocalMap 对象(可以看成是一个简答的Map),此Map对象是线程私有的,key是ThreadLocal对象,value是线程的局部变量。而ThreadLocal中没有维护着Map对象。</font>(简称为 方案B

方案A的设计有什么问题?为什么被抛弃?

  方案A之所以被抛弃,因为以下几点原因

  • 线程需要竞争ThreadLocal中的Map。 一般情况下,是多个线程程共享着一个ThreadLocal 对象,按照方案A的设计,意味着多个线程共享着一个Map对象,所以访问这个Map对象时,需要进行同步互斥访问,访问速度将下降。
  • 线程的局部变量在线程死亡时难以回收或者难以及时回收。 ThreadLocal的Map存储了多个线程的局部变量,当其中任意一个线程销毁时,其局部变量也应该跟着销毁,以释放内存。但是按照方案A中的设计,可能要遍历所有的Map,逐一判断线程(key值)的状态是否死亡,才能释放内存。如果这样做,不仅性能低,且无法及时释放内存,甚至可能会造成Map过大,内存溢出。

方案B有什么优点?方案B又是怎么解决的?

  针对方案A的遇到的问题,方案B(目前方案)中都能得到解决:

  • 不需要竞争访问Map。 在方案B中,是每个Thread对象都维护着一个ThreadLocalMap,所以Map是线程私有的,不需要竞争。而且私有的Map只存储一个线程的局部变量,存储的元素的数量更少,那么hash冲突就少。这两点都大大地提高访问速度。
  • 所有局部变量随线程一起被销毁回收。 因为Map是维护在线程Thread中,当线程被销毁回收时,Map自然一起被销毁回收。
  • key值是弱引用,尽可能地释放过时的键值对Entry,回收内存。key值是指向ThreadLlocal的对象,采用了弱引用的设计,一旦此TreadLlocal对象没有了强引用指向,将会在下次的GC中被回收,那么key值就会为null,对应的Entry对象也最终会被释放,从而减少内存溢出的情况。

二、ThreadLocal的源码解析

  上面仅仅简单地介绍了ThreadLocal的误区和设计思路,并没有深入去了解,也许你还是不太懂,那么接下来的部分将会通过源码,深入分析线程局部变量的机制。

1、ThreadLocalMap 与 Thread、ThreadLocal 的关系

1.1、ThreadLocalMap 类是 ThreadLocal的静态内部类


    static class ThreadLocalMap {
        //.....
    }

1.2、 ThreadLocalMap 对象是Thread的一个成员变量

//每个线程都维护着一个 存储局部变量的Map
    ThreadLocal.ThreadLocalMap threadLocals = null;

1.3、ThreadLocalMap的几个属性

  Entry[] table table数组是用来存储键值对的。键值对的key值为ThreadLocal对象、value是线程局部变量

    static class ThreadLocalMap {
         /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0
        //........
    }

2、ThreadLocal 分析

2.1、ThreadLocal的三个属性

  ThreadLocal的属性就只有以下三个,用于计算、保存ThreadLocal对象中的 threadLocalHashCode 的值,而且每个ThreadLocal对象的 threadLocalHashCode 是不一样的,以此来区别它们,从而在 ThreadLocalMap 中减少hash冲突。

//当前的ThreadLocal 对象的hash值
private final int threadLocalHashCode = nextHashCode();

//静态变量,用于计算下一个hash值
private static AtomicInteger nextHashCode = new AtomicInteger();

//hash增量值,参与下一个hash值的计算
private static final int HASH_INCREMENT = 0x61c88647;

/**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

2.2、ThreadLocal.set() 方法

 public void set(T value) {
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap 对象
        ThreadLocalMap map = getMap(t);
        if (map != null)//判断Map是否创建
            map.set(this, value);//this 指代当前 threadLocl对象
        else
            createMap(t, value);//为当前线程创建ThreadLocalMap对象
    }

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

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

  ThreadLocalset方法很简单。用当前线程中的 ThreadLocalMap 对象去存储局部变量, map.set(this, value) key值为this所指代对象,也即调用了此set方法的ThreadLocal对象。

2.3、ThreadLocal.get() 方法

    public T get() {
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap对象
        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();//初始的value值为null
    }

  get()方法就更简单了,调用 ThreadLocalMap.getEntry()方法,以当前调用get方法的ThreadLocal对象为key值,获取对应的value值。

3、弱引用 与 ThreadLocalMap 的内存回收

  先来看一下 Entry的源代码,Entry类是定义在 ThreadLocalMap中的静态内部类。

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

            Entry(ThreadLocal<?> k, Object v) {
                //创建了一个ThreadLocal对象的弱引用
                super(k);
                value = v;
            }
        }

  ThreadLocalMap.Entry 继承了弱引用类 WeakReference 类,而且弱引用类包裹了key值。这意味着key值是一个弱引用。一旦key值所指向的ThreadLocal没有了强引用指向,那么便会被下一次的GC回收。然后key值便会为null,但是对应的Entry对象还在,并没有释放内存,那ThreadLocalMap是如何回收内存的呢?   <font color="red">ThreadLocalMap 的内存回收:是在getEntry()set()remove()时遍历Map,将key值为null的Entry判断为过时的Entry,然后便释放掉这个Entry。</font>下面是重点讲解set()方法。

 private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            //通过key值(ThreadLocal对象)的散列值threadLocalHashCode计算出 Entry的索引位置
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i]; e != null;e = tab[i = nextIndex(i, len)]) {
                //获取元素的key值
                ThreadLocal<?> k = e.get();

                if (k == key) {//hash命中,直接设置value值
                    e.value = value;
                    return;
                }

                if (k == null) {//没有命中,但找到了过时的Entry对象,即key值为null
                    //替换掉此过时的Entry
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            //如果即没有命中,而且表中也没有发现过时的Entry对象,则在当前空的位置创建并插入一个新的Entry来吃存储
            tab[i] = new Entry(key, value);
            //表的大小增加
            int sz = ++size;
            //threshold = len * 2 / 3;判断是否需要重hash
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();//重hash
        }

  set()在设置值时,先计算出初始索引值,然后循环遍历table数组,判断table数组中的每个Entry是否匹配目标key,如果匹配则直接修改value值,如果发现有Entry过时,则调用replaceStaleEntry方法来替换掉这个过时的Entry,插入新的Entry,看一下replaceStaleEntry的源码:

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
            //slotToExpunge记录过时Entry的索引值
            int slotToExpunge = staleSlot;
            //以当前的过时Entry的索引staleSlot为起点,往后遍历,寻找过时的Entry
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)//判断是否是过时的Entry
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first。以staleSlot为起点,继续往后遍历
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                if (k == key) {//发现hash命中
                    e.value = value;//直接修改value值
                    //命中的Entry与过时的Entry交换位置
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // 判断前面的往后遍历循环是否发现新的过时的Entry对象,即slotToExpunge记录了新的索引值
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);//清除这个新发现的过时Entry
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                //发现了过时的Entry对象,如果前一个往后遍历的循环没有发现过时的Entry对象,才记录当前的索引值。
                //优先释放靠前的过时Entry对象
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
           //hash依旧没有命中,那么就将当前的过时Entry给替换成新的Entry对象
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            if (slotToExpunge != staleSlot)//判断是否发现新的过时Entry对象
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

  replaceStaleEntry()有点复杂,但是可以看出主要是两点:1、以当前的staleSlot为起点分别往前往后寻找过时的Entry对象并释放;2、无论是否找到目标key所对应的Entry,都替换掉staleSlot位置的过时Entry,换上新的Entry。

从JDK1.2版本开始,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

1.强引用

 以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

2.软引用(SoftReference) 如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

3.弱引用(WeakReference) 如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

4.虚引用(PhantomReference) "虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。 虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃 圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解 被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

特别注意,在世纪程序设计中一般很少使用弱引用与虚引用,使用软用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部