理解ThreadLocal

问题:在多线程环境下,如何防止自己的变量被其它线程篡改

答案:ThreadLocal.

ThreadLocal 不是用来解决共享对象的多线程访问的竞争问题的,因为ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。当线程终止后,这些值会作为垃圾回收。

ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用。作用:提供一个线程内公共变量(比如本次请求的用户信息),减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度,或者为线程提供一个私有的变量副本,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

ThreadLocal简介

ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLocal 用一种存储变量与线程绑定的方式,在每个线程中用自己的 ThreadLocalMap 安全隔离变量,如为每个线程创建一个独立的数据库连接。因为是与线程绑定的,所以在很多场景也被用来实现线程参数传递,如 Spring 的 RequestContextHolder。也因为每个线程拥有自己唯一的 ThreadLocalMap ,所以 ThreadLocalMap 是天然线程安全的。

ThreadLocal 存储结构

首先我们来聊一聊 ThreadLocal 在多线程运行时,各线程是如何存储变量的,假如我们现在定义两个 ThreadLocal 实例如下:

static ThreadLocal<User> threadLocal_1 = new ThreadLocal<>();
static ThreadLocal<Client> threadLocal_2 = new ThreadLocal<>();

我们分别在三个线程中使用 ThreadLocal,伪代码如下:

// thread-1中
threadLocal_1.set(user_1);
threadLocal_2.set(client_1);

// thread-2中
threadLocal_1.set(user_2);
threadLocal_2.set(client_2);

// thread-3中
threadLocal_2 .set(client_3);

这三个线程都在运行中,那此时各线程中的存数数据应该如下图所示:

ThreadLocal用法

ThreadLocal用于保存某个线程共享变量:对于同一个static ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。

1、ThreadLocal.get: 获取ThreadLocal中当前线程共享变量的值。

2、ThreadLocal.set: 设置ThreadLocal中当前线程共享变量的值。

3、ThreadLocal.remove: 移除ThreadLocal中当前线程共享变量的值。

4、ThreadLocal.initialValue: ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法,返回此方法值。

public T get() { }  
public void set(T value) { }  
public void remove() { }  
protected T initialValue() { } 

get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,

set()用来设置当前线程中变量的副本,

remove()用来移除当前线程中变量的副本,

initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法,下面会详细说明。

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 的 getEntry 方法
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
  	// 如果还没有设置,可以用子类重写initialValue,自定义初始值。
    return setInitialValue();
}

第一句是取得当前线程,

然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。

然后接着下面获取到<key,value>键值对,注意这里获取键值对传进去的是 this(当前threadLocal对象),而不是当前线程t。

如果获取成功,则返回value值。

如果map为空,则调用setInitialValue方法返回value。

我们上面的每一句来仔细分析:

首先看一下getMap方法中做了什么:

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

可能大家没有想到的是,在getMap中,是调用当期线程t,返回当前线程t中的一个成员变量threadLocals。

那么我们继续取Thread类中取看一下成员变量threadLocals是什么:

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

实际上就是一个ThreadLocalMap,这个类型是ThreadLocal类的一个内部类,

我们继续取看ThreadLocalMap的实现:

/**
 * ThreadLocalMap is a customized hash map suitable only for
 * maintaining thread local values. No operations are exported
 * outside of the ThreadLocal class. The class is package private to
 * allow declaration of fields in class Thread.  To help deal with
 * very large and long-lived usages, the hash table entries use
 * WeakReferences for keys. However, since reference queues are not
 * used, stale entries are guaranteed to be removed only when
 * the table starts running out of space.
 */
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作为键。

然后再继续看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();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

很容易了解,就是如果map不为空,就设置键值对,为空,再创建Map,

看一下createMap的实现:

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

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

    首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键为当前ThreadLocal变量,value为变量副本(即T类型的变量,传入的参数)。

    初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

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

下面通过一个例子来证明通过ThreadLocal能达到在每个线程中创建变量副本的效果:

package com.milo.jdk.lang;
/**
 * You need to setting before getting it, otherwise there will be a NullPointerException
 * @author MILO
 *
 */
public class MiloTheadLocal {
  ThreadLocal<Long> longLocal = new ThreadLocal<Long>();  
  ThreadLocal<String> stringLocal = new ThreadLocal<String>();  

  public void set() {  
    longLocal.set(Thread.currentThread().getId());
    stringLocal.set(Thread.currentThread().getName());
  }  

  public long getLong() {  
    return longLocal.get();  
  }  

  public String getString() {  
    return stringLocal.get();  
  }  

  public static void main(String[] args) throws InterruptedException {  
    final MiloTheadLocal test = new MiloTheadLocal();  

    test.set();  
    System.out.println(test.getLong());  
    System.out.println(test.getString());  


    Thread thread=new Thread() {
      public void run() {
        test.set();
        System.out.println(test.getLong());  
        System.out.println(test.getString());  
      }
    };
    thread.start();
    //thread.join():用来指定当前主线程等待其他线程执行完毕后,再来继续执行Thread.join()后面的代码
    thread.join();
    System.out.println(test.getLong());  
    System.out.println(test.getString());  
  }  
}

运行结果:

1
main
10
Thread-0
1
main

从这段代码的输出结果可以看出,在main线程中和thread1线程中,longLocal保存的副本值和stringLocal保存的副本值都不一样。最后一次在main线程再次打印副本值是为了证明在main线程中和thread1线程中的副本值确实是不同的。

总结一下:

    1)实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;

    2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;

    3)在进行get之前,必须先set,否则会报空指针异常;

如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。

因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。

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

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

protected T initialValue() {
	return null;
}

看下面这个例子,把初始的set()方法屏蔽掉,会报空指针异常:

package com.milo.jdk.lang;
/**
 * You need to setting before getting it, otherwise there will be a NullPointerException
 * @author MILO
 *
 */
public class MiloTheadLocal {
  ThreadLocal<Long> longLocal = new ThreadLocal<Long>();  
  ThreadLocal<String> stringLocal = new ThreadLocal<String>();  

  public void set() {  
    longLocal.set(Thread.currentThread().getId());
    stringLocal.set(Thread.currentThread().getName());
  }  

  public long getLong() {  
    return longLocal.get();  
  }  

  public String getString() {  
    return stringLocal.get();  
  }  

  public static void main(String[] args) throws InterruptedException {  
    final MiloTheadLocal test = new MiloTheadLocal();  

    //test.set();  
    System.out.println(test.getLong());  
    System.out.println(test.getString());  


    Thread thread=new Thread() {
      public void run() {
        test.set();
        System.out.println(test.getLong());  
        System.out.println(test.getString());  
      }
    };
    thread.start();
    //thread.join():用来指定当前主线程等待其他线程执行完毕后,再来继续执行Thread.join()后面的代码
    thread.join();
    System.out.println(test.getLong());  
    System.out.println(test.getString());  
  }  
}

运行结果:

Exception in thread "main" java.lang.NullPointerException
	at com.milo.jdk.lang.MiloTheadLocal.getLong(MiloTheadLocal.java:17)
	at com.milo.jdk.lang.MiloTheadLocal.main(MiloTheadLocal.java:28)

在main线程中,没有先set,直接get的话,运行时会报空指针异常。

但是如果改成下面这段代码,即重写了initialValue方法:

package com.milo.jdk.lang;
/**
 * You not need to setting before getting it.
 * @author MILO
 *
 */
public class MiloTheadLocal2 {
  ThreadLocal<Long> longLocal = new ThreadLocal<Long>() {  
    //重写initialValue方法,进行赋值,此后可以不使用set方法且不报错
    @Override
    protected Long initialValue() {  
      return Thread.currentThread().getId();  
    };  
  };  

  ThreadLocal<String> stringLocal = new ThreadLocal<String>() {  
    protected String initialValue() {  
      return Thread.currentThread().getName();  
    };  
  };  

  public void set() {  
    longLocal.set(Thread.currentThread().getId());  
    stringLocal.set(Thread.currentThread().getName());  
  }  

  public long getLong() {  
    return longLocal.get();  
  }  

  public String getString() {  
    return stringLocal.get();  
  }  

  public static void main(String[] args) throws InterruptedException {  
    final MiloTheadLocal2 test = new MiloTheadLocal2();  

    //test.set();  
    System.out.println(test.getLong());  
    System.out.println(test.getString());  

    Thread thread1 = new Thread() {  
      public void run() {  
        //test.set();  
        System.out.println(test.getLong());  
        System.out.println(test.getString());  
      };  
    };  
    thread1.start();  
    thread1.join();  

    System.out.println(test.getLong());  
    System.out.println(test.getString());  
  }  
}

运行结果:

1  
main  
8  
Thread-0  
1  
main  

此时重写了initialValue方法,就算不用set方法,也不会报空指针错误了,因为在重写的过程中,已经提前赋值了

ThreadLocalMap核心方法

1.如何实现一个线程多个ThreadLocal对象,每一个ThreadLocal对象是如何区分的呢?

答案:ThreadLocalMap 之 key 的 hashCode 计算

查看源码,可以看到:

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自增的原子性。

这里写个demo看一下基于这种方式产生的hash分布多均匀:

public class ThreadLocalTest {

    public static void main(String[] args) {
        printAllSlot(8);
        printAllSlot(16);
        printAllSlot(32);
    }

    static void printAllSlot(int len) {
        System.out.println("********** len = " + len + " ************");
        for (int i = 1; i <= 64; i++) {
            ThreadLocal<String> t = new ThreadLocal<>();
            int slot = getSlot(t, len);
            System.out.print(slot + " ");
            if (i % len == 0)
                System.out.println(); // 分组换行
        }
    }

    /**
     * 获取槽位
     * 
     * @param t ThreadLocal
     * @param len 模拟map的table的length
     * @throws Exception
     */
    static int getSlot(ThreadLocal<?> t, int len) {
        int hash = getHashCode(t);
        return hash & (len - 1);
    }

    /**
     * 反射获取 threadLocalHashCode 字段,因为其为private的
     */
    static int getHashCode(ThreadLocal<?> t) {
        Field field;
        try {
            field = t.getClass().getDeclaredField("threadLocalHashCode");
            field.setAccessible(true);
            return (int) field.get(t);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 0;
    }
}

上述代码模拟了 ThreadLocal 做为 key 的hashCode产生,看看完美槽位分配:
********** len = 8 ************
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
********** len = 16 ************
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
********** len = 32 ************
10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 7 14 21 28 3 
10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 7 14 21 28 3

解释:

​ 传入数组长度8(既main方法中的数字8),则会产生[0,7]8个值,对应数组下标

​ 传入数组长度16(既main方法中的数字16),则会产生[0,15]16个值,对应数组下标

​ 传入数组长度32(既main方法中的数字32),则会产生[0,31]32个值,对应数组下标

为什么不直接用线程id来作为ThreadLocalMap的key?

  这一点很容易理解,因为直接用线程id来作为ThreadLocalMap的key,无法区分放入ThreadLocalMap中的多个value。比如我们放入了两个字符串,你如何知道我要取出来的是哪一个字符串呢?
  而使用ThreadLocal作为key就不一样了,由于每一个ThreadLocal对象都可以由threadLocalHashCode属性唯一区分或者说每一个ThreadLocal对象都可以由这个对象的名字唯一区分,所以可以用不同的ThreadLocal作为key,区分不同的value,方便存取。

2.ThreadLocalMap 之 set() 方法

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

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1); // 用key的hashCode计算槽位
    // hash冲突时,使用开放地址法
    // 因为独特和hash算法,导致hash冲突很少,一般不会走进这个for循环
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) { // key 相同,则覆盖value
            e.value = value; 
            return;
        }

        if (k == null) { // key = null,说明 key 已经被回收了,进入替换方法
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 新增 Entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清除一些过期的值,并判断是否需要扩容
        rehash(); // 扩容
}

这个 set 方法涵盖了很多关键点:

  1. 开放地址法:与我们常用的Map不同,java里大部分Map都是用链表发解决hash冲突的,而 ThreadLocalMap 采用的是开发地址法。
  2. hash算法:hash值算法的精妙之处上面已经讲了,均匀的 hash 算法使其可以很好的配合开方地址法使用;
  3. 过期值清理:关于过期值的清理是网上讨论比较多了,因为只要有关于可能内存溢出的话题,就会带来很多噱头和流量。

简单介绍一下开放地址法和链表法:

开放地址法:容易产生堆积问题;不适于大规模的数据存储;散列函数的设计对冲突会有很大的影响;插入时可能会出现多次冲突的现象,删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂;结点规模很大时会浪费很多空间;

链地址法:处理冲突简单,且无堆积现象,平均查找长度短;链表中的结点是动态申请的,适合构造表不能确定长度的情况;相对而言,拉链法的指针域可以忽略不计,因此较开放地址法更加节省空间。插入结点应该在链首,删除结点比较方便,只需调整指针而不需要对其他冲突元素作调整。

ThreadLocalMap 为什么采用开放地址法?

个人认为由于 ThreadLocalMap 的 hashCode 的精妙设计,使hash冲突很少,并且 Entry 继承 WeakReference, 很容易被回收,并开方地址可以节省一些指针空间;然而恰恰由于开方地址法的使用,使在处理hash冲突时的代码很难懂,比如在replaceStaleEntry,cleanSomeSlotsexpungeStaleEntry 等地方,然而真正调用这些方法的几率却比较小;要把上述方法搞清楚,最好画一画开方地址法发生hash冲突的状态图,容易理解一点,本文不详细探讨。


下面对 set 方法里面的几个关键方法展开:

a. replaceStaleEntry
因为开发地址发的使用,导致 replaceStaleEntry 这个方法有些复杂,它的清理工作会涉及到slot前后的非null的slot。

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

    // 往前寻找过期的slot
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // 找到 key 或者 直到 遇到null 的slot 才终止循环
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // 如果找到了key,那么需要将它与过期的 slot 交换来维护哈希表的顺序。
        // 然后可以将新过期的 slot 或其上面遇到的任何其他过期的 slot 
        // 给 expungeStaleEntry 以清除或 rehash 这个 run 中的所有其他entries。

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

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

            // 如果存在,则开始清除前面过期的entry
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 如果我们没有在向前扫描中找到过期的条目,
        // 那么在扫描 key 时看到的第一个过期 entry 是仍然存在于 run 中的条目。
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // 如果没有找到 key,那么在 slot 中创建新entry
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 如果还有其他过期的entries存在 run 中,则清除他们
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

上文中的 run 不好翻译,理解为开放地址中一个slot中前后不为null的连续entry

b.cleanSomeSlots
cleanSomeSlots 清除一些slot(一些?是不是有点模糊,到底是哪些?)

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);  // n = n / 2, 对数控制循环 
    return removed;
}

当新元素被添加时,或者另一个过期元素已被删除时,会调用cleanSomeSlots。该方法会试探性地扫描一些 entry 寻找过期的条目。它执行 对数 数量的扫描,是一种 基于不扫描(快速但保留垃圾)所有元素扫描之间的平衡。

上面说到的对数数量是多少?循环次数 = log2(N) (log以2为底N的对数),此处N是map的size,如:

log2(4) = 2
log2(5) = 2
log2(18) = 4

因此,此方法并没有真正的清除,只是找到了要清除的位置,而真正的清除在 expungeStaleEntry(int staleSlot) 里面

c.expungeStaleEntry(int staleSlot)

这里是真正的清除,并且不要被方法名迷惑,不仅仅会清除当前过期的slot,还回往后查找直到遇到null的slot为止。开发地址法的清除也较难理解,清除当前slot后还有往后进行rehash。

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

    // 清除当前过期的slot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash 直到 null 的 slot
    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;

                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

3. ThreadLocalMap 之 getEntry() 方法

getEntry() 主要是在 ThreadLocal 的 get() 方法里被调用

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key) // 无hash冲突情况
        return e;
    else
        return getEntryAfterMiss(key, i, e); // 有hash冲突情况
}

该方法比较简洁,首先运算槽位 i ,然后判断 table[i] 是否是目标entry,不是则进入 getEntryAfterMiss(key, i, e)

下面展开 getEntryAfterMiss 方法:

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

这个方法是在遇到 hash 冲突时往后继续查找,并且会清除查找路上遇到的过期slot。

4. ThreadLocalMap 之 rehash() 方法

private void rehash() {
    expungeStaleEntries();

   // 在上面的清除过程中,size会减小,在此处重新计算是否需要扩容
   // 并没有直接使用threshold,而是用较低的threshold (约 threshold 的 3/4)提前触发resize
    if (size >= threshold - threshold / 4)
        resize();
}

private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

rehash() 里首先调用 expungeStaleEntries(),然后循环调用 expungeStaleEntry(j) ,此方法会清除所有过期的slot。

继续看 resize():

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}

resize() 方法里也会过滤掉一些 过期的 entry。

PS :ThreadLocalMap 没有 影响因子 的字段,是采用直接设置 threshold 的方式,threshold = len * 2 / 3,相当于不可修改的影响因子为 2/3,比 HashMap 的默认 0.75 要低。这也是减少hash冲突的方式。

5. ThreadLocalMap 之 remove(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;
        }
    }
}

remove 方法是删除特定的 ThreadLocal,建议在 ThreadLocal 使用完后执行此方法。

总结

一、ThreadLocalMap 的 value 清理触发时间:

  1. set(ThreadLocal<?> key, Object value)
    若无hash冲突,则先向后检测log2(N)个位置,发现过期 slot 则清除,如果没有任何 slot 被清除,则判断 size >= threshold,超过阀值会进行 rehash(),rehash()会清除所有过期的value;
  2. getEntry(ThreadLocal<?> key) (ThreadLocal 的 get() 方法调用)
    如果没有直接在hash计算的 slot 中找到entry, 则需要向后继续查找(直到null为止),查找期间发现的过期 slot 会被清除;
  3. remove(ThreadLocal<?> key)
    remove 不仅会清除需要清除的 key,还是清除hash冲突的位置的已过期的 key;

清晰了以上过程,相信对于 ThreadLocal 的 内存溢出问题会有自己的看法。在实际开发中,不应乱用 ThreadLocal ,如果使用 ThreadLocal 发生了内存溢出,那应该考虑是否使用合理。

PS:这里的清除并不代表被回收,只是把 value 置为 null,value 的具体回收时间由 垃圾收集器 决定。

二、ThreadLocalMap 的 hash 算法和 开方地址法

由于 ThreadLocal 在每个 Thread 里面的唯一性和特殊性,为其定制了特殊的 hashCode 生成方式,能够很好的散列在 table 中,有效的减少hash冲突。
基于较少的hash冲突,于是采用了开放地址法,开放地址法在没有hash冲突的时候很好理解,在发生冲突时的代码就有些绕。因此理解 ThreadLocalMap 的新增、删除、查找、清除等操作,需要对开方地址法的hash冲突处理有较清晰的思路,最好在手边画一画开放地址法的hash冲突情况,目前没有在网上找的很好的讲解,争取在后续文章补充。

ThreadLocal存储到ThreadLocalMap的过程

结论:

在ThreadLoalMap中,也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,是不是很神奇,通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。

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

ThreadLocal类的set方法:

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

ThreadLocal类的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类的get方法获取不到map,则执行ThreadLocal类的setInitialValue方法,如果ThreadLocal类的setInitialValue获取不到map,则执行ThreadLocal类的createMap方法,获取到执行ThreadLocalMap的set方法
  • 假如ThreadLocal类的set方法获取不到map,则执行ThreadLocal类的createMap方法,获取到执行ThreadLocalMap的set方法

综上两点则可以得到,如果得到map则会执行下面的set方法

ThreadLocalMap的set方法

private Entry[] table;

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的hashCode计算槽位
  int i = key.threadLocalHashCode & (len-1);
	// hash冲突时,使用开放地址法
  // 因为独特和hash算法,导致hash冲突很少,一般不会走进这个for循环
  for (Entry e = tab[i];
       e != null;
       e = tab[i = nextIndex(i, len)]) {
    ThreadLocal<?> k = e.get();
		// key 相同,则覆盖value
    if (k == key) {
      e.value = value;
      return;
    }
		// key = null,说明 key 已经被回收了,进入替换方法
    if (k == null) {
      replaceStaleEntry(key, value, i);
      return;
    }
  }
	// 新增 Entry
  tab[i] = new Entry(key, value);
  int sz = ++size;
  // 清除一些过期的值,并判断是否需要扩容
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
    // 扩容
    rehash();
}

得不到map,则会执行ThreadLocal类的createMap方法:

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

最后执行的还是ThreadLocalMap的构造方法:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  table = new Entry[INITIAL_CAPACITY];
  int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  table[i] = new Entry(firstKey, firstValue);
  size = 1;
  setThreshold(INITIAL_CAPACITY);
}

所以,无论是否存在ThreadLocalMap,都是会走从ThreadLocalMap的set方法或者ThreadLocalMap的构造方法,从ThreadLocalMap的set方法和构造方法可以看出:

以TreadLocal为key,传入的参数为value组成的entry最终是存在于ThreadLocalMap中的Entry[]数组中的,

而组成的entry在数组的下标则为ThreadLocal类中的final变量threadLocalHashCode(threadLocalHashCode是通过CAS实现增长的,所以不会是重复的值)与Entry[]数组长度&运算(与运算)得到,下标对应的值就是组成的entry

当获取存储在ThreadLocalMap中Entry[]数组的entry的时候

private Entry getEntry(ThreadLocal<?> key) {
  //private Entry[] table;
  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类中的final变量threadLocalHashCode与Entry[]数组长度&运算(与运算)得到下标,然后根据下标从Entry[]数组中取出entry,然后再进一步进行运算.

所以由此得出结论:

一个线程有多个ThreadLocal对象,将各个ThreadLocal存储到ThreadLocalMap的时候,实际上是将各个ThreadLocal存储到了内部类ThreadLocalMap的Entry[]数组中,在数组中的下标依赖各个ThreadLocal中的final变量threadLocalHashCode.

此下的结论待证明和改正:

由此得出ThreadLocalMap使用ThreadLocal的弱引用作为key,因为如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,就无法通过该ThreadLocal的threadLocalHashCode计算出下标以访问存储在ThreadLocalMap中entry,所以此时,ThreadLocalMap中此下标的entry就没办法被访问,且没办法倍删除,时间长了则会产生内存溢出.(ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value(原文的解释,前面的是自己的理解,还要参考一下进一步确认https://www.jianshu.com/p/56f64e3c1b6c))

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它,防止内存泄露。

ThreadLocal的应用场景

最常见的ThreadLocal使用场景为 用来解决数据库连接、Session管理等。如:

​ 数据库连接:

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
    public Connection initialValue() {  
        return DriverManager.getConnection(DB_URL);  
    }  
};  
  
public static Connection getConnection() {  
    return connectionHolder.get();  
}  

Session管理:

private static final ThreadLocal threadSession = new ThreadLocal();  
  
public static Session getSession() throws InfrastructureException {  
    Session s = (Session) threadSession.get();  
    try {  
        if (s == null) {  
            s = getSessionFactory().openSession();  
            threadSession.set(s);  
        }  
    } catch (HibernateException ex) {  
        throw new InfrastructureException(ex);  
    }  
    return s;  
}  

当时做记录的时候忘记记录原文链接了,作者看到之后可以私信我,我补上原文链接.

posted @ 2020-01-28 20:24  未月廿三  阅读(683)  评论(2编辑  收藏  举报