Java笔记:ThreadLocal源码分析

Java笔记:ThreadLocal源码分析

简述

  顾名思义,ThreadLocal指的是线程本地变量,使用该实例对象进行读写的数据在同一线程中保持一致,多个线程不能共享该数据,使得每个线程能够单独维护自身内部的线程本地变量,其他线程无从修改,保证了线程间数据的隔离。
  ThreadLocal对象本身不存储本地变量值,而是提供存取本地变量值的接口,真正的本地变量值存储在线程的本地变量表中。同时,一个ThreadLocal对象只能对一个线程本地变量进行多次存取操作。而一个线程可以通过创建多个ThreadLocal对象管理其内部的多个本地变量。

本地变量表的数据结构

  首先,在Thread类中含有一个属性:

ThreadLocal.ThreadLocalMap threadLocals = null;

  此对象是ThreadLocal.ThreadLocalMap(ThreadLocal的静态内部类)类实例对象,为该线程对象的本地变量表,初始值为null。该属性由传入每个线程中的ThreadLocal对象分别进行维护,从而实现ThreadLocal类对象对线程本地变量的控制。
  静态内部类ThreadLocalMap对象为一个键值对数组,类似于HashMap(但结构非数组+链表解决哈希冲突)。该键值对总是以创建该Entry的ThreadLocal作为key,从而表示一个ThreadLocal对象对该对象维护的本地变量的映射关系。
  该Entry类又是ThreadLocalMap的静态内部类,继承了WeakReference类(即如果该对象没有被其他强引用引用时,发生一次GC操作,不管当前内存是否足够,该对象都会被回收)然而查看该类的构造方法:

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

  发现由构造方法传入该Entry对象,并且作为键值对key的ThreadLocal对象传入了父类(即WeakReference)的构造器。也就是只有传入的ThreadLocal对象是有弱引用特性的,使用父类中区域进行存储(Reference类下的一个泛型属性)。
  可能期望达到的情景是:在ThreadLocal对象外部强引用被释放后,不可能存在访问本地变量表中该ThreadLocal对应值的接口,若本地变量表中对ThreadLocal对象也为强引用,则对应的内存区域就会无用并不可访问,造成了内存泄露,因此需要将本地变量表中的引用设置为弱引用便于无用内存区域的回收,避免内存泄露。
  而我们希望对于本地变量value值,在存储进本地变量表后,即使外部引用被释放,本地变量表仍应保留着对原内存区域的引用,所以该内存区域不被释放,仍能对其进行访问,所以设置为强引用。
  这样一来就出现了一个现象:键值对的key被GC回收而value未回收,因而value不可能被访问到,产生了内存泄露。因此在ThreadLocalMap中提供了一些方法来处理本地变量表中的键值对key为null的情况,分析ThreadLocalMap方法时会提及。

ThreadLocal中的方法

初始值的设置

  使用ThreadLocal类获取线程的本地变量,是存在初始值的,默认为null,通过一个方法调用获取默认值null。

protected T initialValue() {
    return null;
}

  该方法主要由setInitialValue()方法调用,setInitialValue()方法如下:

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

  该方法的主要作用是向当前线程的本地变量表中加入一个初始化的键值对,键为本ThreadLocal对象,值为null。
  然而,该类向外暴露的公共接口只有set(T value)、get()和remove(),即对线程单个本地变量的设置、获取与移除,是封装性的体现。

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

  该方法主要流程为:获取当前线程的本地变量表,如果是第一次设置,获取到的本地变量表为null,就为该线程创建本地变量表,传入value值;若非第一次设置,则map不为空,调用map的set方法为本地变量表插入“本ThreadLocal - 传入方法的value”这样一个键值对。

get()方法

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

  该方法主要流程为:获取当前线程的本地变量表,若为空(说明在线程外没有通过调用set(T value)方法使用createMap()方法来对线程的本地变量表进行创建对象的操作),调用setInitialValue()方法,在该方法内部:因此时线程本地变量表为null,使用createMap()方法创建本地变量表,返回初始默认值null;若不为空,根据本ThreadLocal对象从线程的本地变量表中获取到键值对,获取键值对的value值强转后返回,若获取不到相应键值对,则同map为空的情况。

remove()方法

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

  该方法主要流程:获取当前线程的本地变量表,调用该本地变量表的remove()方法,传入自身作为参数,删除自身ThreadLocal对应的本地变量的值。
  通过分析ThreadLocal类中的源码可以发现:ThreadLocal实际上是对对线程内部本地变量表ThreadLocalMap对象操作的进一步封装,本ThreadLocal对象与其对应的本地变量以键值对的方式存储在线程内部的本地变量表中,所以进一步分析ThreadLocalMap类中的方法是关键。

ThreadLocalMap中的方法

createMap(Thread t, T firstValue)方法

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

  初始化线程t的本地变量表,将外部类(ThreadLocal)对象与传入的值作为该本地变量表的第一个键值对。

set(ThreadLocal key, Object value)方法

private void set(ThreadLocal key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

  该方法主要由ThreadLocal的setInitialValue()设置初始值时、以及set方法设置线程本地变量时调用,可以看到该哈希表的哈希函数为:key.threadLocalHashCode & (len-1),threadLocalHashCode是ThreadLocal的一个属性,由原子操作类AtomicInteger类型的类对象控制,每创建出一个ThreadLocal对象,该类对象都会自增0x61c88647的一个值(ThreadLocal类的静态属性private static final int HASH_INCREMENT),至于为什么是这样一个数,大概是为了减少哈希冲突,保证元素在哈希表中能够做到均匀散列。
  之后使用一个循环,查找并将元素放在合适的区域上,从循环的方法中可以看出该哈希表是使用线性探测法解决哈希冲突的(此次哈希得到的位置不允许,数组下标加一,跳到临近的下一个位置),执行完元素放入操作的情景如下:
  1、跳出循环,找到一个空位,将ThreadLocal对象和value形成的键值对放上去。
  2、找到之前设置的键值对,将其value设置为新的value值。
  根据哈希函数算出了存放的位置,且存放键值对不为空,但是其key为空。说明发生了这样的情况:该ThreadLocal对象外部强引用被释放,则根据可达性分析,没有链路可以到达原来的引用指向的对象的位置,而键值对的键由弱引用保管,于是被垃圾回收机制回收。对于外部强引用被释放,不再被需要的引用,需要使用replaceStaleEntry方法对其进行清理。

replaceStaleEntry(ThreadLocal key, Object value, int staleSlot)方法

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).
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // Find either the key or trailing null slot of run, whichever
    // occurs first
    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) {
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            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.
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

  该方法是用来处理在set方法过程中,发现因ThreadLocal强引用被释放导致本地变量表中该ThreadLocal弱引用也被释放,本地变量表键值对key为null,仍想向本地变量表中加入value的情况。
  方法入参:ThreadLocal对象、value值和key为空的Entry下标。
  首先,slotToExpunge变量表示的下标对应键值对肯定是应该清理的键值对。
  从第一个循环中跳出时,e应该表示的是在staleSlot之前,与staleSlot距离最近且为空的键值对,slotToExpunge记载的则是从一开始到当前e之间key为null的键值对下标(e.get()方法为Reference类下的方法,返回key值),或之间没有键值对为空的Entry,记载的仍为传入值。也就是说,找出了两个key为null的键值对,下标分别是statesSlot和slotToExpunge(存在两个值相等的情况)。
  之后进入第二个循环,从方法传入的stateSlot向后环形查找,直到遍历到一个空Entry。在中间的过程中,若找到其key与传入的ThreadLocal相同的键值对,进行一次交换,交换后当前下标i存储着key为空的键值对而staleSlot存储着完好的一如入参键和值一般的键值对,可以说是修复了key为空的键值对,而仍然需要处理此时下标为i的键值对。
  交换之后的if条件判断,若slotToExpunge和stateSlot相等,说明在第一个循环的向前环形查找中未找到stateSlot之前的脏键值对,因此只需要处理下标i存储的键值对。之后调用cleanSomeSlots方法清理slotToExpunge之后的脏数据,直接返回。
  如果在循环的过程中又发现了key为null的键值对,且在前项环形查找中没有发现key为空的键值对,需要清理的键值对下标变成了循环变量i。
  跳出循环后,由于未对stateSlot进行操作,该下标对应的键值对key仍为null,此时释放其value值,创建新的节点。
  如果slotToExpunge与stateSolt不等,说明在向后的环形查找中又发现了key为null的键值对(对应第二个循环中最后一个if条件k == null && slotToExpunge == staleSlot发生的情况)。在跳出循环后已经将staleSlot下标的键值对进行创建,之后就应清理slotToExpunge处的key为null的键值对。
  总而言之,该方法在已经出现了key为空的键值对之前和之后各进行了一次环形扫描,不论出现了哪种情况,slotToExpunge下标都是最靠前的key为空的键值对,处理办法都是将其传给expungeStaleEntry方法(expungeStaleEntry方法在文章靠后位置,暂且知道该方法的用途是清除从传入值下标开始的空key键值对,返回距离传入值最近的键值对为空的值,确保传入值与返回值之间不会产生脏数据),其返回值再传给cleanSomeSlots方法,完成slotToExpunge下标之后key为空键值对的清理。

replaceStaleEntry(int i, int n)方法

private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

  该方法使用一个循环以及expungeStaleEntry方法来排除本地变量表中下标为i之后key为null的键值对,单次循环中,若当前i下标对应key为null的键值对,则重置n为本地变量表长度,整理i之后的部分键值对,确保入参i到expungeStaleEntry方法返回值之间不会出现key为null的键值对。每次循环条件都会位右移一位(除以2,即最多会执行log2(n)次),扫描到key为null的键值对后,n又变为本地变量表长度,从而使循环次数再次增加到log2(n)次。

getEntry(ThreadLocal key)方法

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

  该方法就是从线程的本地变量表中获取键值对,以便ThreadLocal对象的get()方法获取该对象对应的键值对中的本地变量值。使用传入的ThreadLocal对象的threadLocalHashCode进行一次哈希计算,若命中缓存且键值对的key未被回收,则返回该键值对;否则执行未命中的方法getEntryAfterMiss。

getEntryAfterMiss(ThreadLocal key, int i, Entry e)方法

private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

  在第一次哈希计算未命中时该方法被调用。由于向本地变量哈希表中存储数据时,处理哈希冲突的方法是线性探测法,所以在此处也是在当前未命中下标之后一个一个环形寻找,找到返回,不能找到返回空。同时在遍历的过程中使用expungeStaleEntry方法去除本地变量表中的脏数据(即键值对中key为null的数据)。

expungeStaleEntry(int staleSlot)方法

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

  此方法又是一个处理键值对中key为null情况(外部ThreadLocal对象被放弃,对应value不可能被访问,需要丢弃)的方法。
  首先会将确定key为空的键值对本身和值value释放掉。
  之后进入一个循环,从被清除的下标之后的一个Entry开始,检查非空的Entry对象的key是否为空,若为空,则清除键值对本身和值value。
  若循环变量e的key非空,首先取该key(ThreadLocal对象)再次进行哈希运算,得到的哈希值h若非当前i值(说明该键值对放在当前位置是由于在位置h发生了哈希冲突),将本地变量表中i下标对应Entry置为空(此时原Entry仍由循环e引用,不会被回收),之后仍使用线性探测的方法,将循环变量e引用的Entry交给更邻近本地变量表中第一次哈希得到下标值且为空的Entry位。该操作将因之前发生哈希冲突的键值对放到了能更快以线性探测法找到的位置,提高了查找的速度。
  方法的返回值是传入的应被丢弃的Entry下标值之后最近的Entry为null的下标值。

remove(ThreadLocal key)方法

private void remove(ThreadLocal key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

  根据传入ThreadLocal类型key,哈希计算后以线性探测的方法查找到对应键值对,找到相应键值对后执行clear方法,将key置为null,之后使用expungeStaleEntry方法处理到该key为null的键值对。

总结

  1、Thread类中存在一个本地变量表,ThreadLocalMap类型的、一个以线性探测法维护哈希冲突的键值对数组,仅供本线程访问。
  2、键值对以ThreadLocal对象为键,本地变量为值。一个ThreadLocal对象对应本地变量表中一个本地变量。
  3、该键值对key属于弱引用,因此需要解决key为空,从而value永远不可访问的内存泄露情况。
  4、ThreadLocal自身只提供操作本地变量表的方法,本身不存储本地变量。
  5、ThreadLocalMap使用一系列方法来处理key为空的键值对(replaceStaleEntry、replaceStaleEntry和expungeStaleEntry方法)。

你可能感兴趣的:(Java笔记:ThreadLocal源码分析)