ThreadLocal

ThreadLocal

参考网址

以数据库连接为例,如果多个线程共享一个连接,有可能一个线程在对数据库进行操作,另一个线程调用了closeConnection操作;如果在每一个线程都new一个连接对象,如果开启关闭数据库操作频繁,会影响到服务器压力,并且影响程序执行性能。
=> ThreadLocal 内部维护一个 ThreadLocalMap 类,保存的是 Entry<Thread K, Object V> 数组,K是线程,V是值,这样每一个线程无论在哪里调用,都会拿到自己线程的值

每个Thread对象中都持有一个ThreadLocalMap的成员变量。每个ThreadLocalMap内部又维护了N个Entry节点,也就是Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值

ThreadLocalMap的引用是在Thread里的,所以它里面的Entry数组存放的是一个线程里new出来的多个ThreadLocal对象

Thread维护了ThreadLocalMap,而ThreadLocalMap里维护了Entry,而Entry里存的是以ThreadLocal为key,传入的值为value的键值对。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// java.lang.Thread类里持有ThreadLocalMap的引用
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}

// java.lang.ThreadLocal有内部静态类ThreadLocalMap
public class ThreadLocal<T> {
static class ThreadLocalMap {
private Entry[] table;

// ThreadLocalMap内部有Entry类,Entry的key是ThreadLocal本身,value是泛型值
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}

ThreadLocal内存结构图

主要方法:

  • initialValue:初始化。在get方法里懒加载的。

    • 通常,每个线程最多调用一次此方法。但是如果已经调用了remove(),然后再次调用get()的话,则可以再次触发initialValue。
    • 如果要重写的话一般建议采取匿名内部类的方式重写此方法,否则默认返回的是null。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal() {
      @Override
      protected SimpleDateFormat initialValue() {
      return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
      }
      };
      // Java8的高逼格写法
      public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
      ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
  • get:得到这个线程对应的value。如果调用get之前没set过,则get内部会执行initialValue方法进行初始化。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    /**
    * 获取当前线程下的entry里的value值。
    * 先获取当前线程下的ThreadLocalMap,
    * 然后以当前ThreadLocal为key取出map中的value
    */
    public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程对应的ThreadLocalMap对象。
    ThreadLocalMap map = getMap(t);
    // 若获取到了。则获取此ThreadLocalMap下的entry对象,若entry也获取到了,那么直接获取entry对应的value返回即可。
    if (map != null) {
    // 获取此ThreadLocalMap下的entry对象
    ThreadLocalMap.Entry e = map.getEntry(this);
    // 若entry也获取到了
    if (e != null) {
    @SuppressWarnings("unchecked")
    // 直接获取entry对应的value返回。
    T result = (T)e.value;
    return result;
    }
    }
    // 若没获取到ThreadLocalMap或没获取到Entry,则设置初始值。 懒加载方式
    return setInitialValue();
    }

    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
    // 如果e=null,说明出现碰撞问题,通过开放寻址方法来继续寻找
    return getEntryAfterMiss(key, i, e);
    }

    // 通过布长+1或-1来寻找下一个相邻的位置
    private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
    }
    private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
    }
  • set:为这个线程设置一个新值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * 设置当前线程的线程局部变量的值
    * 实际上ThreadLocal的值是放入了当前线程的一个ThreadLocalMap实例中,所以只能在本线程中访问。
    */
    public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程对应的ThreadLocalMap实例,注意这里是将t传进去了,t是当前线程,就是说ThreadLocalMap是在线程里持有的引用。
    ThreadLocalMap map = getMap(t);
    // 若当前线程有对应的ThreadLocalMap实例,则将当前ThreadLocal对象作为key,value做为值存到ThreadLocalMap的entry里。
    if (map != null)
    map.set(this, value);
    else
    // 若当前线程没有对应的ThreadLocalMap实例,则创建ThreadLocalMap,并将此线程与之绑定
    createMap(t, value);
    }
  • remove:ThreadLocalMap键为弱引用,删除这个线程对应的值,防止内存泄露的最佳手段。

碰撞解决与神奇的 0x61c88647(十进制:1640531527)

两种碰撞类型

- 只有一个ThreadLocal实例的时候(上面推荐的做法),当向thread-local变量中设置多个值的时产生的碰撞,碰撞解决是通过开放定址法, 且是线性探测(linear-probe)
- 多个ThreadLocal实例的时候,最极端的是每个线程都new一个ThreadLocal实例,此时利用特殊的哈希码0x61c88647大大降低碰撞的几率, 同时利用开放定址法处理碰撞

显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。

所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。

key.threadLocalHashCode ==> AtomicInteger.getAndAdd(HASH_INCREMENT) 其中 HASH_INCREMENT = 0x61c88647,这个值是 为了让哈希码能均匀的分布在2的N次方的数组里

This number represents the golden ratio (sqrt(5)-1) times two to the power of 31 ((sqrt(5)-1) * (2^31)). The result is then a golden number, either 2654435769 or -1640531527.

魔数0x61c88647的与斐波那契散列有关,0x61c88647对应的十进制为1640531527。斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。换句话说(1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是1640531527也就是0x61c88647。通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。