ThreadLocal(史上最全)

文章很长,建议收藏起来,慢慢读! 疯狂创客圈为小伙伴奉上如下珍贵的学习资源:html


推荐2:史上最全 Java 面试题 21 个专题

史上最全 Java 面试题 21 个专题 阿里、京东、美团、头条.... 随意挑、横着走!!!
1: JVM面试题(史上最强、持续更新、吐血推荐) http://www.javashuo.com/article/p-vhnpdnhb-vd.html
2:Java基础面试题(史上最全、持续更新、吐血推荐) http://www.javashuo.com/article/p-otujhkjp-vd.html
3:死锁面试题(史上最强、持续更新) http://www.javashuo.com/article/p-uyudvdol-vd.html
4:设计模式面试题 (史上最全、持续更新、吐血推荐) http://www.javashuo.com/article/p-qnkzhtsu-vd.html
5:架构设计面试题 (史上最全、持续更新、吐血推荐) http://www.javashuo.com/article/p-dlpjqbmg-vd.html
还有 10 +必刷、必刷 的面试题 更多 ....., 请参见【 疯狂创客圈 高并发 总目录

推荐3: 疯狂创客圈 高质量 博文

springCloud 高质量 博文
nacos 实战(史上最全) sentinel (史上最全+入门教程)
springcloud + webflux 高并发实战 Webflux(史上最全)
SpringCloud gateway (史上最全) spring security (史上最全)
还有 10 +必刷、必刷 的高质量 博文 更多 ....., 请参见【 疯狂创客圈 高并发 总目录

1、ThreadLocal 介绍:

正如 JDK 注释中所说的那样: ThreadLocal 类提供线程局部变量,它一般是私有类中但愿将状态与线程关联的静态字段。java

简而言之,就是 ThreadLocal 提供了线程间数据隔离的功能,从它的命名上也能知道这是属于一个线程的本地变量。也就是说,每一个线程都会在 ThreadLocal 中保存一份该线程独有的数据,因此它是线程安全的。程序员

熟悉 Spring 的同窗可能知道 Bean 的做用域(Scope),而 ThreadLocal 的做用域就是线程。web

下面经过一个简单示例来展现一下 ThreadLocal 的特性:面试

public static void main(String[] args) {
  ThreadLocal<String> threadLocal = new ThreadLocal<>();
  // 建立一个有2个核心线程数的线程池
  ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 1, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(10));
  // 线程池提交一个任务,将任务序号及执行该任务的子线程的线程名放到 ThreadLocal 中
  threadPool.execute(() -> threadLocal.set("任务1: " + Thread.currentThread().getName()));
  threadPool.execute(() -> threadLocal.set("任务2: " + Thread.currentThread().getName()));
  threadPool.execute(() -> threadLocal.set("任务3: " + Thread.currentThread().getName()));
  // 输出 ThreadLocal 中的内容
  for (int i = 0; i < 10; i++) {
    threadPool.execute(() -> System.out.println("ThreadLocal value of " + Thread.currentThread().getName() + " = " + threadLocal.get()));
  }
  // 线程池记得关闭
  threadPool.shutdown();
}

上面代码首先建立了一个有2个核心线程数的普通线程池,随后提交一个任务,将任务序号及执行该任务的子线程的线程名放到 ThreadLocal 中,最后在一个 for 循环中输出线程池中各个线程存储在 ThreadLocal 中的值。算法

这个程序的输出结果是:spring

ThreadLocal value of pool-1-thread-1 = 任务3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任务2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任务3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任务2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任务3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任务2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任务3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任务2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任务3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任务2: pool-1-thread-2

因而可知,线程池中执行提交的任务的是名为 pool-1-thread-1 的线程,随后屡次输出线程池核心线程在 ThreadLocal 变量中存储的的内容也代表:每一个线程在 ThreadLocal 中存储的内容是当前线程独有的,在多线程环境下,可以有效防止本身的变量被其余线程修改(存储的内容是同一个引用类型对象的状况除外)。编程

2、ThreadLocal 实现原理:

在 JDK1.8 版本中 ThreadLocal 类的源码总共723行,去掉注释大概有350行,应该算是 JDK 核心类库中代码量比较少的一个类了,相对来讲它的源码仍是挺容易理解的。设计模式

下面,就从 ThreadLocal 的数据结构开始聊聊它的实现原理吧。数组

底层数据结构:

ThreadLocal 底层是经过 ThreadLocalMap 这个静态内部类来存储数据的,ThreadLocalMap 就是一个键值对的 Map,它的底层是 Entry 对象数组,Entry 对象中存放的键是 ThreadLocal 对象,值是 Object 类型的具体存储内容。

除此以外,ThreadLocalMap 也是 Thread 类一个属性。

img

如何证实上面给出的 ThreadLocal 类底层数据结构的正确性?

咱们能够从 ThreadLocal#get() 方法开始追踪代码,看看线程局部变量究竟是从哪里被取出来的。

public T get() {
  // 获取当前线程
  Thread t = Thread.currentThread();
  // 获取 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 变量
  ThreadLocalMap map = getMap(t);
  // 若 threadLocals 变量不为空,根据 ThreadLocal 对象来获取 key 对应的 value
  if (map != null) {
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
      @SuppressWarnings("unchecked")
      T result = (T)e.value;
      return result;
    }
  }
  // 若 threadLocals 变量是 NULL,初始化一个新的 ThreadLocalMap 对象
  return setInitialValue();
}

// ThreadLocal#setInitialValue
// 初始化一个新的 ThreadLocalMap 对象
private T setInitialValue() {
  // 初始化一个 NULL 值
  T value = initialValue();
  // 获取当前线程
  Thread t = Thread.currentThread();
  // 获取 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 变量
  ThreadLocalMap map = getMap(t);
  if (map != null)
    map.set(this, value);
  else
    createMap(t, value);
  return value;
}

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

经过 ThreadLocal#get() 方法能够很清晰的看到,咱们根据 ThreadLocal 对象从 ThreadLocal 中读取数据时,首先会获取当前线程对象,而后获得当前线程对象中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 属性;

若是 threadLocals 属性不为空,会根据 ThreadLocal 对象做为 key 来获取 key 对应的 value;若是 threadLocals 变量是 NULL,就初始化一个新的ThreadLocalMap 对象。

再看 ThreadLocalMap 的构造方法,也就是 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 属性不为空时的执行逻辑。

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

这个构造方法实际上是将 ThreadLocal 对象做为 key,存储的具体内容 Object 对象做为 value,包装成一个 Entry 对象,放到 ThreadLocalMap 类中类型为 Entry 数组的 table 属性中,这样就完成了线程局部变量的存储。

因此说, ThreadLocal 中的数据最终是存放在 ThreadLocalMap 这个类中的

散列方式:

ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法中我写了一行注释:

// 获取当前 ThreadLocal 对象的散列值
int i = key.threadLocalHashCode & (len-1);

这行代码获得的值实际上是一个 ThreadLocal 对象的散列值,这就是 ThreadLocal 的散列方式,咱们称之为 斐波那契散列

// ThreadLocal#threadLocalHashCode
private final int threadLocalHashCode = nextHashCode();

// ThreadLocal#nextHashCode
private static int nextHashCode() {
	  return nextHashCode.getAndAdd(HASH_INCREMENT);
}

// ThreadLocal#nextHashCode
private static AtomicInteger nextHashCode = new AtomicInteger();

// AtomicInteger#getAndAdd
public final int getAndAdd(int delta) {
  	return unsafe.getAndAddInt(this, valueOffset, delta);
}

// 魔数 ThreadLocal#HASH_INCREMENT
private static final int HASH_INCREMENT = 0x61c88647;

key.threadLocalHashCode 所涉及的函数及属性如上所示,每个 ThreadLocal 的 threadLocalHashCode 属性都是基于魔数 0x61c88647 来生成的。

这里就不讨论选择这个魔数的缘由了(实际上是我看不太懂),总之大量的实践证实: 使用 0x61c88647 做为魔数生成的 threadLocalHashCode 再与2的幂取余,获得的结果分布很均匀。

注: 对 A 进行2的幂取余操做 A % 2^N 能够经过 A & (2^n-1) 来代替,位运算的效率比取模效率高不少。

如何解决哈希冲突:

咱们已经知道 ThreadLocalMap 类的底层数据结构是一个 Entry 类型的数组,但与 HashMap 中的 Node 类数组+链表形式不一样的是,Entry 类没有 next 属性来构成链表,因此它是一个单纯的数组。

就算上面所说的 斐波那契散列法 真的可以充分散列,但不免仍是可能会发生哈希碰撞,那么问题来了,Entry 数组是如何解决哈希冲突的?

这就须要拿出 ThreadLocal#set(T value) 方法了,而具体处理哈希冲突的逻辑是在 ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法中的:

public void set(T value) {
  // 获取当前线程
  Thread t = Thread.currentThread();
  // 获取 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 变量
  ThreadLocalMap map = getMap(t);
  // 若 threadLocals 变量不为空,进行赋值;不然新建一个 ThreadLocalMap 对象来存储
  if (map != null)
    map.set(this, value);
  else
    createMap(t, value);
}

// ThreadLocalMap#set
private void set(ThreadLocal<?> key, Object value) {
  // 获取 ThreadLocalMap 的 Entry 数组对象
  Entry[] tab = table;
  int len = tab.length;
  // 基于斐波那契散列法获取当前 ThreadLocal 对象的散列值
  int i = key.threadLocalHashCode & (len-1);
  // 解决哈希冲突,线性探测法
  for (Entry e = tab[i];
       e != null;
       e = tab[i = nextIndex(i, len)]) {
    ThreadLocal<?> k = e.get();
		// 代码(1)
    if (k == key) {
      e.value = value;
      return;
    }
		// 代码(2)
    if (k == null) {
      replaceStaleEntry(key, value, i);
      return;
    }
  }
  // 代码(3)将 key-value 包装成 Entry 对象放在数组退出循环时的位置中
  tab[i] = new Entry(key, value);
  int sz = ++size;
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
}

// ThreadLocalMap#nextIndex
// Entry 数组的下一个索引,若超过数组大小则从0开始,至关于环形数组
private static int nextIndex(int i, int len) {
  return ((i + 1 < len) ? i + 1 : 0);
}

具体分析处理哈希冲突的 ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法,能够看到,在拿到 ThreadLocal 对象的散列值以后进入了一个 for 循环,循环的条件也很清楚:从 Entry 数组的 ThreadLocal 对象散列值处开始,每次向后挪一位,若是超过数组大小则从0开始继续遍历,直到 Entry 对象为 NULL 为止。

在循环过程当中:

  • 如代码(1),若是当前 ThreadLocal 对象正好等于 Entry 对象中的 key 属性,直接更新 ThreadLocal 中 value 的值;
  • 如代码(2),若是当前 ThreadLocal 对象不等于 Entry 对象中的 key 属性,而且 Entry 对象的 key 是空的,这里进行的逻辑实际上是 设置键值对,同时清理无效的 Entry (必定程序防止内存泄漏,下文会有详细介绍);
  • 如代码(3),若是在遍历中没有发现当前 TheadLocal 对象的散列值,也没有发现 Entry 对象的 key 为空的状况,而是知足了退出循环的条件,即 Entry 对象为空时,那么就会建立一个 新的 Entry 对象进行存储 ,同时作一次 启发式清理 ,将 Entry 数组中 key 为空,value 不为空的对象的 value 值释放;

至此,咱们分析完了在向 ThreadLocal 中存储数据时,拿到 ThreadLocal 对象散列值以后的逻辑,回到本小节的主题—— ThreadLocal 是如何解决哈希冲突的?

由上面的代码能够知道,在基于斐波那契散列法获取当前 ThreadLocal 对象的散列值以后进入了一个循环,在循环中是处理具体处理哈希冲突的方法:

  • 若是散列值已存在且 key 为同一个对象,直接更新 value
  • 若是散列值已存在但 key 不是同一个对象,尝试在下一个空的位置进行存储

因此,来总结一下 ThreadLocal 处理哈希冲突的方式就是:若是在 set 时遇到哈希冲突,ThreadLocal 会经过线性探测法尝试在数组下一个索引位置进行存储,同时在 set 过程当中 ThreadLocal 会释放 key 为 NULL,value 不为 NULL 的脏 Entry对象的 value 属性来防止内存泄漏

初始容量及扩容机制:

在上文中有提到过 ThreadLocalMap 的构造方法,这里详细说明一下。

// ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  // 初始化 Entry 数组
  table = new Entry[INITIAL_CAPACITY];
  int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  table[i] = new Entry(firstKey, firstValue);
  size = 1;
  // 设置扩容条件
  setThreshold(INITIAL_CAPACITY);
}

ThreadLocalMap 的初始容量是 16:

// 初始化容量
private static final int INITIAL_CAPACITY = 16;

下面聊一下 ThreadLocalMap 的扩容机制 ,它在扩容前有两个判断的步骤,都知足后才会进行最终扩容:

  • ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法中可能会触发启发式清理,在清理无效 Entry 对象后,若是数组长度大于等于数组定义长度的 2/3,则首先进行 rehash;
// rehash 条件
private void setThreshold(int len) {
  threshold = len * 2 / 3;
}
  • rehash 会触发一次全量清理,若是数组长度大于等于数组定义长度的 1/2,则进行 resize(扩容);
// 扩容条件
private void rehash() {
  expungeStaleEntries();

  // Use lower threshold for doubling to avoid hysteresis
  if (size >= threshold - threshold / 4)
    resize();
}
  • 进行扩容时,Entry 数组为扩容为 原来的2倍 ,从新计算 key 的散列值,若是遇到 key 为 NULL 的状况,会将其 value 也置为 NULL,帮助虚拟机进行GC。
// 具体的扩容函数
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;
}

父子线程间局部变量如何传递:

咱们已经知道 ThreadLocal 中存储的是线程的局部变量,那若是如今有个需求,想要实现线程间局部变量传递,这该如何实现呢?

大佬们早已料到会有这样的需求,因而设计出了 InheritableThreadLocal 类。

InheritableThreadLocal 类的源码除去注释以外一共不超过10行,由于它是继承于 ThreadLocal 类,不少东西在 ThreadLocal 类中已经实现了,InheritableThreadLocal 类只重写了其中三个方法:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

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

咱们先用一个简单的示例来实践一下父子线程间局部变量的传递功能。

public static void main(String[] args) {
  ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
  threadLocal.set("这是父线程设置的值");

  new Thread(() -> System.out.println("子线程输出:" + threadLocal.get())).start();
}

// 输出内容
子线程输出:这是父线程设置的值

能够看到,在子线程中经过调用 InheritableThreadLocal#get() 方法,拿到了在父线程中设置的值。

那么,这是如何实现的呢?

实现父子线程间的局部变量共享须要追溯到 Thread 对象的构造方法:

public Thread(Runnable target) {
  init(null, target, "Thread-" + nextThreadNum(), 0);
}

private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
  init(g, target, name, stackSize, null, true);
}

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  // 该参数通常默认是 true
                  boolean inheritThreadLocals) {
  // 省略大部分代码
  Thread parent = currentThread();
  
  // 复制父线程的 inheritableThreadLocals 属性,实现父子线程局部变量共享
  if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
   	this.inheritableThreadLocals =
    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); 
  }
  
	// 省略部分代码
}

在最终执行的构造方法中,有这样一个判断:若是当前父线程(建立子线程的线程)的 inheritableThreadLocals 属性不为 NULL,就会将当下父线程的 inheritableThreadLocals 属性复制给子线程的 inheritableThreadLocals 属性。具体的复制方法以下:

// ThreadLocal#createInheritedMap
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
  return new ThreadLocalMap(parentMap);
}

private ThreadLocalMap(ThreadLocalMap parentMap) {
  Entry[] parentTable = parentMap.table;
  int len = parentTable.length;
  setThreshold(len);
  table = new Entry[len];
	// 一个个复制父线程 ThreadLocalMap 中的数据
  for (int j = 0; j < len; j++) {
    Entry e = parentTable[j];
    if (e != null) {
      @SuppressWarnings("unchecked")
      ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
      if (key != null) {
        // childValue 方法调用的是 InheritableThreadLocal#childValue(T parentValue)
        Object value = key.childValue(e.value);
        Entry c = new Entry(key, value);
        int h = key.threadLocalHashCode & (len - 1);
        while (table[h] != null)
          h = nextIndex(h, len);
        table[h] = c;
        size++;
      }
    }
  }
}

须要注意的是,复制父线程共享变量的时机是在建立子线程时,若是在建立子线程后父线程再往 InheritableThreadLocal 类型的对象中设置内容,将再也不对子线程可见。

ThreadLocal 内存泄漏分析:

最后再来讲说 ThreadLocal 的内存泄漏问题,众所周知,若是使用不当,ThreadLocal 会致使内存泄漏。

内存泄漏 是指程序中已动态分配的堆内存因为某种缘由程序未释放或没法释放,形成系统内存的浪费,致使程序运行速度减慢甚至系统崩溃等严重后果。

发生内存泄漏的缘由:

而 ThreadLocal 发生内存泄漏的缘由须要从 Entry 对象提及。

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

Entry 对象的 key 即 ThreadLocal 类是继承于 WeakReference 弱引用类。具备弱引用的对象有更短暂的生命周期,在发生 GC 活动时,不管内存空间是否足够,垃圾回收器都会回收具备弱引用的对象。

因为 Entry 对象的 key 是继承于 WeakReference 弱引用类的,若 ThreadLocal 类没有外部强引用,当发生 GC 活动时就会将 ThreadLocal 对象回收。

而此时若是建立 ThreadLocal 类的线程依然活动,那么 Entry 对象中 ThreadLocal 对象对应的 value 就依旧具备强引用而不会被回收,从而致使内存泄漏。

如何解决内存泄漏问题:

要想解决内存泄漏问题其实很简单,只须要记得在使用完 ThreadLocal 中存储的内容后将它 remove 掉就能够了。

这是主动防止发生内存泄漏问题的手段,但其实设计 ThreadLocal 的大神固然也发现了 ThreadLocal 可能引起内存泄漏的问题,因此他们也设计了相应的手段来防止内存泄漏。

ThreadLocal 内部如何防止内存泄漏:

在上文中描述 ThreadLocalMap#set(ThreadLocal key, Object value) 其实已经有涉及 ThreadLocal 内部清理无效 Entry 的逻辑了,在经过线性检测法处理哈希冲突时,若 Entry 数组的 key 与当前 ThreadLocal 不是同一个对象,同时 key 为空的时候,会进行 清理无效 Entry 的处理,即 ThreadLOcalMap#replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) 方法:

  • 这个方法中也是一个循环,循环的逻辑与 ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法一致;
  • 在循环过程当中若是找到了将要存储的 ThreadLocal 对象,则会将它与进入 replaceStaleEntry 方法时知足条件的 k 值作交换,同时将 value 更新;
  • 若是没有找到将要存储的 ThreadLocal 对象,则会在此 k 值处新建一个 Entry 对象存储;
  • 同时,在循环过程当中若是发现其余无效的 Entry( key 为 NULL,value还在的状况,可能致使内存泄漏,下文会有详细描述),会顺势找到 Entry 数组中全部的无效 Entry,释放这些无效 Entry(经过将 key 和 value 都设置为NULL),在必定程度上避免了内存泄漏;

若是知足线性检测循环结束条件了,即遇到了 Entry==NULL 的状况,就新建一个 Entry 对象来存储数据。而后会进行一次启发式清理,若是启发式清理没有成功释放知足条件的对象,同时知足扩容条件时,会执行 ThreadLocalMap#rehash() 方法。

private void rehash() {
  // 全量清理
  expungeStaleEntries();
  // 知足条件则扩容
  if (size >= threshold - threshold / 4)
    resize();
}

ThreadLocalMap#rehash() 方法中会对 ThreadLocalMap 进行一次全量清理,全量清理会遍历整个 Entry 数组,删除全部 key 为 NULL,value 不为 NULL 的脏 Entry对象。

// 全量清理
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);
  }
}

进行全量清理以后,若是 Entry 数组的大小大于等于 threshold - threshold / 4 ,则会进行2倍扩容。

总结一下:在ThreadLocal 内部是经过在 get、set、remove 方法中主动进行清理 key 为 NULL 且 value 不为 NULL 的无效 Entry 来避免内存泄漏问题。

可是基于 get、set 方法让 ThreadLocal 自行清理无效 Entry 对象并不能彻底避免内存泄漏问题,要完全解决内存泄漏问题还得养成使用完就主动调用 remove 方法释放资源的好习惯。

3、ThreadLocal的常见面试题目

什么是ThreadLocal

ThreadLocal 是 JDK java.lang 包下的一个类,是自然的线程安全的类,

1.ThreadLoca 是线程局部变量,这个变量与普通变量的区别,在于每一个访问该变量的线程,在线程内部都会
初始化一个独立的变量副本,只有该线程能够访问【get() or set()】该变量,ThreadLocal实例一般声明
为 private static。

2.线程在存活而且ThreadLocal实例可被访问时,每一个线程隐含持有一个线程局部变量副本,当线程生命周期
结束时,ThreadLocal的实例的副本跟着线程一块儿消失,被GC垃圾回收(除非存在对这些副本的其余引用)

JDK 源码中解析:

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code 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).
 * /

稍微翻译一下:ThreadLocal提供线程局部变量。这些变量与正常的变量不一样,由于每个线程在访问ThreadLocal实例的时候(经过其get或set方法)都有本身的、独立初始化的变量副本。ThreadLocal实例一般是类中的私有静态字段,使用它的目的是但愿将状态(例如,用户ID或事务ID)与线程关联起来。

ThreadLocalMap 和HashMap区别

HashMap 的数据结构是数组+链表

ThreadLocalMap的数据结构仅仅是数组

HashMap 是经过链地址法解决hash 冲突的问题

ThreadLocalMap 是经过开放地址法来解决hash 冲突的问题

HashMap 里面的Entry 内部类的引用都是强引用

ThreadLocalMap里面的Entry 内部类中的key 是弱引用,value 是强引用

ThreadLocal怎么用

讨论ThreadLocal用在什么地方前,咱们先明确下,若是仅仅就一个线程,那么都不用谈ThreadLocal的,ThreadLocal是用在多线程的场景的!!!

ThreadLocal概括下来就3类用途:

  1. 保存线程上下文信息,在任意须要的地方能够获取!!!
  2. 线程安全的,避免某些状况须要考虑线程安全必须同步带来的性能损失!!!
  3. 线程间数据隔离

1.保存线程上下文信息,在任意须要的地方能够获取!!!
因为ThreadLocal的特性,同一线程在某地方进行设置,在随后的任意地方均可以获取到。从而能够用来保存线程上下文信息。

经常使用的好比每一个请求怎么把一串后续关联起来,就能够用ThreadLocal进行set,在后续的任意须要记录日志的方法里面进行get获取到请求id,从而把整个请求串起来。

还有好比Spring的事务管理,用ThreadLocal存储Connection,从而各个DAO能够获取同一Connection,能够进行事务回滚,提交等操做。

2.线程安全的,避免某些状况须要考虑线程安全必须同步带来的性能损失!!!
因为不须要共享信息,天然就不存在竞争问题了,从而保证了某些状况下线程的安全,以及避免了某些状况须要考虑线程安全必须同步带来的性能损失!!!

ThreadLocal局限性
ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。可是ThreadLocal也有局限性,咱们来看看阿里规范:
在这里插入图片描述
这类场景阿里规范里面也提到了:
在这里插入图片描述
ThreadLocal用法

public class MyThreadLocalDemo {

	private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    public static void main(String[] args) throws InterruptedException {
        int threads = 9;
        MyThreadLocalDemo demo = new MyThreadLocalDemo();
        CountDownLatch countDownLatch = new CountDownLatch(threads);
        for (int i = 0; i < threads; i++) {
            Thread thread = new Thread(() -> {
                threadLocal.set(Thread.currentThread().getName());
                System.out.println("threadLocal.get()================>" + threadLocal.get());
                countDownLatch.countDown();
            }, "执行线程 - " + i);
            thread.start();
        }
        countDownLatch.await();
    }

}

代码运行结果:

threadLocal.get()================>执行线程 - 1
threadLocal.get()================>执行线程 - 0
threadLocal.get()================>执行线程 - 3
threadLocal.get()================>执行线程 - 4
threadLocal.get()================>执行线程 - 5
threadLocal.get()================>执行线程 - 8
threadLocal.get()================>执行线程 - 7
threadLocal.get()================>执行线程 - 2
threadLocal.get()================>执行线程 - 6

Process finished with exit code 0

ThreadLocal的原理

在这里插入图片描述

以两个线程为例:

ThreadLocal虽然叫线程局部变量,可是实际上它并不存听任何的信息,能够这样理解:它是线程(Thread)操做ThreadLocalMap中存放的变量的桥梁。它主要提供了初始化、set()、get()、remove()几个方法。这样说可能有点抽象,下面画个图说明一下在线程中使用ThreadLocal实例的set()和get()方法的简单流程图。

假设咱们有以下的代码,主线程的线程名字是main(也有可能不是main):

public class Main {

    private static final ThreadLocal<String> LOCAL = new ThreadLocal<>();

    public static void main(String[] args) throws Exception{
        LOCAL.set("doge");
        System.out.println(LOCAL.get());
    }
}

在这里插入图片描述
上面只描述了单线程的状况而且由于是主线程忽略了Thread t = new Thread()这一步,若是有多个线程会稍微复杂一些,可是原理是不变的,ThreadLocal实例老是经过Thread.currentThread()获取到当前操做线程实例,而后去操做线程实例中的ThreadLocalMap类型的成员变量,所以它是一个桥梁,自己不具有存储功能

链地址法

这种方法的基本思想是将全部哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,于是查找、插入和删除主要在同义词链中进行。列如对于关键字集合{12,67,56,16,25,37, 22,29,15,47,48,34},咱们用前面一样的12为除数,进行除留余数法:

在这里插入图片描述

开放地址法

这种方法的基本思想是一旦发生了冲突,就去寻找下一个空的散列地址(这很是重要,源码都是根据这个特性,必须理解这里才能往下走),只要散列表足够大,空的散列地址总能找到,并将记录存入。

好比说,咱们的关键字集合为{12,33,4,5,15,25},表长为10。 咱们用散列函数f(key) = key mod l0。 当计算前S个数{12,33,4,5}时,都是没有冲突的散列地址,直接存入(蓝色表明为空的,能够存放数据):

在这里插入图片描述

计算key = 15时,发现f(15) = 5,此时就与5所在的位置冲突。因而咱们应用上面的公式f(15) = (f(15)+1) mod 10 =6。因而将15存入下标为6的位置。这其实就是房子被人买了因而买下一间的做法:

在这里插入图片描述

链地址法和开放地址法的优缺点

开放地址法:

容易产生堆积问题,不适于大规模的数据存储。

散列函数的设计对冲突会有很大的影响,插入时可能会出现屡次冲突的现象。

删除的元素是多个冲突元素中的一个,须要对后面的元素做处理,实现较复杂。

链地址法:

处理冲突简单,且无堆积现象,平均查找长度短。

链表中的结点是动态申请的,适合构造表不能肯定长度的状况。

删除结点的操做易于实现。只要简单地删去链表上相应的结点便可。

指针须要额外的空间,故当结点规模较小时,开放定址法较为节省空间。

ThreadLocalMap 采用开放地址法缘由

ThreadLocal 中看到一个属性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一个神奇的数字,让哈希码能均匀的分布在2的N次方的数组里, 即 Entry[] table

经过HASH_INCREMENT 能够看到,ThreadLocal 中使用了斐波那契散列法,来保证哈希表的离散度。而它选用的乘数值便是2^32 * 黄金分割比

什么是散列?

散列(Hash)也称为哈希,就是把任意长度的输入,经过散列算法,变换成固定长度的输出,这个输出值就是散列值。

ThreadLocal 每每存放的数据量不会特别大(并且key 是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是很是高,加上第一点的保障,冲突几率也低.

解决哈希冲突

ThreadLocal中的hash code很是简单,就是调用AtomicInteger的getAndAdd方法,参数是个固定值0x61c88647。

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

上面说过ThreadLocalMap的结构很是简单只用一个数组存储,并无链表结构,当出现Hash冲突时采用线性查找的方式,所谓线性查找,就是根据初始key的hashcode值肯定元素在table数组中的位置,若是发现这个位置上已经有其余key值的元素被占用,则利用固定的算法寻找必定步长的下个位置,依次判断,直至找到可以存放的位置。若是产生屡次hash冲突,处理起来就没有HashMap的效率高,为了不哈希冲突,使用尽可能少的threadlocal变量

内存泄漏问题

在JAVA里面,存在强引用、弱引用、软引用、虚引用。这里主要谈一下强引用和弱引用。

强引用,就没必要说了,相似于:

A a = new A();

B b = new B();

考虑这样的状况:

C c = new C(b);

b = null;

考虑下GC的状况。要知道b被置为null,那么是否意味着一段时间后GC工做能够回收b所分配的内存空间呢?答案是否认的,由于即使b被置为null,可是c仍然持有对b的引用,并且仍是强引用,因此GC不会回收b原先所分配的空间!既不能回收利用,又不能使用,这就形成了内存泄露

那么如何处理呢?

能够c = null;也可使用弱引用!(WeakReference w = new WeakReference(b);)

ThreadLocal使用到了弱引用,是否意味着不会存在内存泄露呢?

把ThreadLocal置为null,那么意味着Heap中的ThreadLocal实例不在有强引用指向,只有弱引用存在,所以GC是能够回收这部分空间的,也就是key是能够回收的。可是value却存在一条从Current Thread过来的强引用链。所以只有当Current Thread销毁时,value才能获得释放。

只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间内不会被回收的,就发生了咱们认为的内存泄露。最要命的是线程对象不被回收的状况,好比使用线程池的时候,线程结束是不会销毁的,再次使用的,就可能出现内存泄露。

那么如何有效的避免呢?

在ThreadLocalMap中的set/getEntry方法中,会对key为null(也便是ThreadLocal为null)进行判断,若是为null的话,那么是会对value置为null的。咱们也能够经过调用ThreadLocal的remove方法进行释放!也就是每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

ThreadLocal使用

ThreadLocal使用的通常步骤:

一、在多线程的类(如ThreadDemo类)中。建立一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。
二、在ThreadDemo类中。建立一个获取要隔离访问的数据的方法getXxx(),在方法中推断,若ThreadLocal对象为null时候,应该new()一个隔离訪问类型的对象,并强制转换为要应用的类型。
三、在ThreadDemo类的run()方法中。经过getXxx()方法获取要操做的数据。这样可以保证每个线程相应一个数据对象,在不论什么时刻都操做的是这个对象。

使用示例:

private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {

        new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    threadLocal.set(i);
                    System.out.println(Thread.currentThread().getName() + " = " + threadLocal.get());
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                threadLocal.remove();
            }
        }, "threadLocal test 1").start();


        new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + " = " + threadLocal.get());
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                threadLocal.remove();
            }
        }, "threadLocal test 2").start();
    }

输出

threadLocal test 1 = 0
threadLocal test 2 = null
threadLocal test 2 = null
threadLocal test 1 = 1
threadLocal test 2 = null
threadLocal test 1 = 2
threadLocal test 2 = null
threadLocal test 1 = 3
threadLocal test 2 = null
threadLocal test 1 = 4
threadLocal test 2 = null
threadLocal test 1 = 5
threadLocal test 2 = null
threadLocal test 1 = 6
threadLocal test 2 = null
threadLocal test 1 = 7
threadLocal test 2 = null
threadLocal test 1 = 8
threadLocal test 2 = null
threadLocal test 1 = 9

与Synchonized的对照:

ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的差异。synchronized是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每一个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通讯时能够得到数据共享。

Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

线程隔离特性

线程隔离特性,只有在线程内才能获取到对应的值,线程外不能访问。

(1)Synchronized是经过线程等待,牺牲时间来解决访问冲突

(1)ThreadLocal是经过每一个线程单独一份存储空间,牺牲空间来解决冲突

须要了解ThreadLocal的源码解析: 点此了解

4、ThreadLocal源码分析

从Thread源码入手:

public class Thread implements Runnable {
......
//与此线程有关的ThreadLocal值。该映射由ThreadLocal类维护。
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。该Map由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
......
}

从上面Thread类源代码能够看出Thread类中有一个threadLocals和一个inheritableThreadLocals 变量,它们都是ThreadLocalMap类型的变量,默认状况下这两个变量都是null,只有当前线程调用ThreadLocal类的Iset或get方法时才建立它们,实际上调用这两个方法的时候,咱们调用的是ThreadLocalMap类对应的get()、set()方法。
在这里插入图片描述

在这里插入图片描述

1.ThreadLocal的内部属性

ThreadLocalMap 的 key 是 ThreadLocal,但它不会传统的调用 ThreadLocal 的 hashCode 方法(继承自Object 的 hashCode),而是调用 nextHashCode() ,具体运算以下:

public class ThreadLocal<T> {
	//获取下一个ThreadLocal实例的哈希魔数
	private final int threadLocalHashCode = nextHashCode();
	
	//原子计数器,主要到它被定义为静态
	private static AtomicInteger nextHashCode = new AtomicInteger();
	
	//哈希魔数(增加数),也是带符号的32位整型值黄金分割值的取正
	private static final int HASH_INCREMENT = 0x61c88647;
	
	//生成下一个哈希魔数
	private static int nextHashCode() {
	    return nextHashCode.getAndAdd(HASH_INCREMENT);
	}
	...
}

这里须要注意一点,threadLocalHashCode是一个final的属性,而原子计数器变量nextHashCode和生成下一个哈希魔数的方法nextHashCode()是静态变量和静态方法,静态变量只会初始化一次。换而言之,每新建一个ThreadLocal实例,它内部的threadLocalHashCode就会增长0x61c88647。举个例子:

//t1中的threadLocalHashCode变量为0x61c88647
ThreadLocal t1 = new ThreadLocal();
//t2中的threadLocalHashCode变量为0x61c88647 + 0x61c88647
ThreadLocal t2 = new ThreadLocal();
//t3中的threadLocalHashCode变量为0x61c88647 + 0x61c88647 + 0x61c88647
ThreadLocal t3 = new ThreadLocal();

threadLocalHashCode是下面的ThreadLocalMap结构中使用的哈希算法的核心变量,对于每一个ThreadLocal实例,它的threadLocalHashCode是惟一的。

这里写个demo看一下基于魔数 1640531527 方式产生的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 

Process finished with exit code 0

2. ThreadLocal 之 set() 方法

ThreadLocal中set()方法的源码以下:

protected T initialValue() {
        return null;
    }
    
   /**
    * 将此线程局部变量的当前线程副本设置为指定值。大多数子类将不须要
    * 重写此方法,而仅依靠{@link #initialValue} 
    * 方法来设置线程局部变量的值。
    *
    * @param value 要存储在此线程的thread-local副本中的值
    */
   public void set(T value) {
    //设置值前老是获取当前线程实例
    Thread t = Thread.currentThread();
    //从当前线程实例中获取threadLocals属性
    ThreadLocalMap map = getMap(t);
    if (map != null)
         //threadLocals属性不为null则覆盖key为当前的ThreadLocal实例,值为value
         map.set(this, value);
    else
    //threadLocals属性为null,则建立ThreadLocalMap,第一个项的Key为当前的ThreadLocal实例,值为value
        createMap(t, value);
	}
	
	//这里看到获取ThreadLocalMap实例时候老是从线程实例的成员变量获取
 	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    //建立ThreadLocalMap实例的时候,会把新实例赋值到线程实例的threadLocals成员
 	void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

上面的过程源码很简单,设置值的时候老是先获取当前线程实例而且操做它的变量threadLocals。步骤是:

  1. 获取当前运行线程的实例。
  2. 经过线程实例获取线程实例成员threadLocals(ThreadLocalMap),若是为null,则建立一个新的ThreadLocalMap实例赋值到threadLocals。
  3. 经过threadLocals设置值value,若是原来的哈希槽已经存在值,则进行覆盖。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.ThreadLocal 之 get() 方法

ThreadLocal中get()方法的源码以下:

/**
     * 返回此线程局部变量的当前线程副本中的值。若是该变量没有当前线程的值,
     * 则首先经过调用{@link #initialValue}方法将其初始化为*返回的值。
     *
     * @return 当前线程局部变量中的值
     */
     public T get() {
	    //获取当前线程的实例
	    Thread t = Thread.currentThread();
	    ThreadLocalMap map = getMap(t);
	    if (map != null) {
	    //根据当前的ThreadLocal实例获取ThreadLocalMap中的Entry,使用的是ThreadLocalMap的getEntry方法
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T) e.value;
             return result;
            }
        }
	    //线程实例中的threadLocals为null,则调用initialValue方法,而且建立ThreadLocalMap赋值到threadLocals
	    return setInitialValue();
	}
	
	private T setInitialValue() {
	    // 调用initialValue方法获取值
	    T value = initialValue();
	    Thread t = Thread.currentThread();
	    ThreadLocalMap map = getMap(t);
	    // ThreadLocalMap若是未初始化则进行一次建立,已初始化则直接设置值
	    if (map != null)
	        map.set(this, value);
	    else
	        createMap(t, value);
	    return value;
	}
	
	protected T initialValue() {
       return null;
    }

initialValue()方法默认返回null,若是ThreadLocal实例没有使用过set()方法直接使用get()方法,那么ThreadLocalMap中的此ThreadLocal为Key的项会把值设置为initialValue()方法的返回值。若是想改变这个逻辑能够对initialValue()方法进行覆盖。
在这里插入图片描述

4.TreadLocal的remove方法

ThreadLocal中remove()方法的源码以下:

public void remove() {
    //获取Thread实例中的ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
       //根据当前ThreadLocal做为Key对ThreadLocalMap的元素进行移除
       m.remove(this);
}

在这里插入图片描述

这里罗列了 ThreadLocal 的几个public方法,其实全部工做最终都落到了 ThreadLocalMap 的头上,ThreadLocal 仅仅是从当前线程取到 ThreadLocalMap 而已,具体执行,请看下面对 ThreadLocalMap 的分析。

5.内部类ThreadLocalMap的基本结构和源码分析

ThreadLocalMap 是ThreadLocal 内部的一个Map实现,然而它并无实现任何集合的接口规范,由于它仅供内部使用,数据结构采用 数组 + 开方地址法,Entry 继承 WeakReference,是基于 ThreadLocal 这种特殊场景实现的 Map,它的实现方式很值得研究。

ThreadLocal内部类ThreadLocalMap使用了默认修饰符,也就是包(包私有)可访问的。ThreadLocalMap内部定义了一个静态类Entry。咱们重点看下ThreadLocalMap的源码,

5.1先当作员和结构部分

/**
 * ThreadLocalMap是一个定制的散列映射,仅适用于维护线程本地变量。
 * 它的全部方法都是定义在ThreadLocal类以内。
 * 它是包私有的,因此在Thread类中能够定义ThreadLocalMap做为变量。
 * 为了处理很是大(指的是值)和长时间的用途,哈希表的Key使用了弱引用(WeakReferences)。
 * 引用的队列(弱引用)再也不被使用的时候,对应的过时的条目就能经过主动删除移出哈希表。
 */
static class ThreadLocalMap {

    //注意这里的Entry的Key为WeakReference<ThreadLocal<?>>
    static class Entry extends WeakReference<ThreadLocal<?>> {

        //这个是真正的存放的值
        Object value;
        // Entry的Key就是ThreadLocal实例自己,Value就是输入的值
        Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
        }
    }
    //初始化容量,必须是2的幂次方
    private static final int INITIAL_CAPACITY = 16;

    //哈希(Entry)表,必须时扩容,长度必须为2的幂次方
    private Entry[] table;

    //哈希表中元素(Entry)的个数
    private int size = 0;

    //下一次须要扩容的阈值,默认值为0
    private int threshold;

    //设置下一次须要扩容的阈值,设置值为输入值len的三分之二
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

    // 以len为模增长i
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

    // 以len为模减小i
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }
}
  1. 这里注意到十分重要的一点:ThreadLocalMap$Entry是WeakReference(弱引用),而且键值Key为ThreadLocal<?>实例自己,这里使用了无限定的泛型通配符。
  2. ThreadLocalMap 的 key 是 ThreadLocal,但它不会传统的调用 ThreadLocal 的 hashCode 方法(继承自Object 的 hashCode),而是调用 nextHashCode()

5.2接着看ThreadLocalMap的构造函数

// 构造ThreadLocal时候使用,对应ThreadLocal的实例方法void createMap(Thread t, T firstValue)
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 哈希表默认容量为16
    table = new Entry[INITIAL_CAPACITY];
    // 计算第一个元素的哈希码
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

// 构造InheritableThreadLocal时候使用,基于父线程的ThreadLocalMap里面的内容进行
// 提取放入新的ThreadLocalMap的哈希表中
// 对应ThreadLocal的静态方法static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap)
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
    // 基于父ThreadLocalMap的哈希表进行拷贝
    for (Entry e : parentTable) {
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

这里注意一下,ThreadLocal的set()方法调用的时候会懒初始化一个ThreadLocalMap而且放入第一个元素。而ThreadLocalMap的私有构造是提供给静态方法ThreadLocal#createInheritedMap()使用的。

5.3ThreadLocalMap 之 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. 过时值清理

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

1.replaceStaleEntry()
由于开发地址发的使用,致使 replaceStaleEntry 这个方法有些复杂,它的清理工做会涉及到slot先后的非null的slot。

//这里个方法比较长,做用是替换哈希码为staleSlot的哈希槽中Entry的值
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 才终止循环
    // 遍历staleSlot以后的哈希槽,若是Key匹配则用输入值替换
    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

2.cleanSomeSlots()
cleanSomeSlots 清除一些slot(一些?是否是有点模糊,究竟是哪些?)

//清理第i个哈希槽以后的n个哈希槽,若是遍历的时候发现Entry的Key为null,则n会重置为哈希表的长度,
//expungeStaleEntry有可能会重哈希使得哈希表长度发生变化
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) 里面

3.expungeStaleEntry(int staleSlot)

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

//对当前哈希表中全部的Key为null的Entry调用expungeStaleEntry
// 1.清空staleSlot对应哈希槽的Key和Value
// 2.对staleSlot到下一个空的哈希槽之间的全部可能冲突的哈希表部分槽进行重哈希,置空Key为null的槽
// 3.注意返回值是staleSlot以后的下一个空的哈希槽的哈希码
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 清空staleSlot对应哈希槽的Key和Value
    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) {//空key直接清除
            e.value = null;
            tab[i] = null;
            size--;
        } else {//key非空,则Rehash
            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;
}

5.4ThreadLocalMap 之 getEntry() 方法

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

/**
 * 这个方法主要给`ThreadLocal#get()`调用,经过当前ThreadLocal实例获取哈希表中对应的Entry
 *
 */
private Entry getEntry(ThreadLocal<?> key) {
    // 计算Entry的哈希值
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i]; 
    if (e != null && e.get() == key)//无hash冲突状况
        return e;
    else  // 注意这里,若是e为null或者Key对不上,表示:有hash冲突状况,会调用getEntryAfterMiss
        return getEntryAfterMiss(key, i, e);
}

// 若是Key在哈希表中找不到哈希槽的时候会调用此方法
// 这个方法是在遇到 hash 冲突时日后继续查找,而且会清除查找路上遇到的过时slot。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 这里会经过nextIndex尝试遍历整个哈希表,若是找到匹配的Key则返回Entry
    // 若是哈希表中存在Key == null的状况,调用expungeStaleEntry进行清理
    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;
}

5.5ThreadLocalMap 之 rehash() 方法

// 重哈希,必要时进行扩容
private void rehash() {
    // 清理全部空的哈希槽,而且进行重哈希
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    // 在上面的清除过程当中,size会减少,在此处从新计算是否须要扩容
    // 并无直接使用threshold,而是用较低的threshold (约 threshold 的 3/4)提早触发resize
    if (size >= threshold - threshold / 4)
        resize();
}

// 对当前哈希表中全部的Key为null的Entry调用expungeStaleEntry
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);
    }
}

// 扩容,简单的扩大2倍的容量        
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (Entry e : oldTab) {
        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;
}

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

5.6ThreadLocalMap 之 remove(key) 方法

/**
	 * Remove the entry for 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 使用完后必定要执行此方法。

5、什么状况下ThreadLocal的使用会致使内存泄漏

其实ThreadLocal自己不存听任何的数据,而ThreadLocal中的数据其实是存放在线程实例中,从实际来看是线程内存泄漏,底层来看是Thread对象中的成员变量threadLocals持有大量的K-V结构,而且线程一直处于活跃状态致使变量threadLocals没法释放被回收。threadLocals持有大量的K-V结构这一点的前提是要存在大量的ThreadLocal实例的定义,通常来讲,一个应用不可能定义大量的ThreadLocal,因此通常的泄漏源是线程一直处于活跃状态致使变量threadLocals没法释放被回收。可是咱们知道,·ThreadLocalMap·中的Entry结构的Key用到了弱引用(·WeakReference<ThreadLocal<?>>·),当没有强引用来引用ThreadLocal实例的时候,JVM的GC会回收ThreadLocalMap中的这些Key,此时,ThreadLocalMap中会出现一些Key为null,可是Value不为null的Entry项,这些Entry项若是不主动清理,就会一直驻留在ThreadLocalMap中。也就是为何ThreadLocal中get()、set()、remove()这些方法中都存在清理ThreadLocalMap实例key为null的代码块。总结下来,内存泄漏可能出现的地方是:

大量地(静态)初始化ThreadLocal实例,初始化以后再也不调用get()、set()、remove()方法。

初始化了大量的ThreadLocal,这些ThreadLocal中存放了容量大的Value,而且使用了这些ThreadLocal实例的线程一直处于活跃的状态。
ThreadLocal中一个设计亮点是ThreadLocalMap中的Entry结构的Key用到了弱引用。试想若是使用强引用,等于ThreadLocalMap中的全部数据都是与Thread的生命周期绑定,这样很容易出现由于大量线程持续活跃致使的内存泄漏。使用了弱引用的话,JVM触发GC回收弱引用后,ThreadLocal在下一次调用get()、set()、remove()方法就能够删除那些ThreadLocalMap中Key为null的值,起到了惰性删除释放内存的做用。

其实ThreadLocal在设置内部类ThreadLocal.ThreadLocalMap中构建的Entry哈希表已经考虑到内存泄漏的问题,因此ThreadLocal.ThreadLocalMap$Entry类设计为弱引用,类签名为static class Entry extends WeakReference<ThreadLocal<?>>。以前一篇文章介绍过,若是弱引用关联的对象若是置为null,那么该弱引用会在下一次GC时候回收弱引用关联的对象。举个例子:

public class ThreadLocalMain {

    private static ThreadLocal<Integer> TL_1 = new ThreadLocal<>();

    public static void main(String[] args) throws Exception {
        TL_1.set(1);
        TL_1 = null;
        System.gc();
        Thread.sleep(300);
    }
}

这种状况下,TL_1这个ThreadLocal在主动GC以后,线程绑定的ThreadLocal.ThreadLocalMap实例中的Entry哈希表中原来的TL_1所在的哈希槽Entry的引用持有值referent(继承自WeakReference)会变成null,可是Entry中的value是强引用,还存放着TL_1这个ThreadLocal未回收以前的值。这些被”孤立”的哈希槽Entry就是前面说到的要惰性删除的哈希槽。

6、ThreadLocal的最佳实践

其实ThreadLocal的最佳实践很简单:

  • 每次使用完ThreadLocal实例,都调用它的remove()方法,清除Entry中的数据。

调用remove()方法最佳时机是线程运行结束以前的finally代码块中调用,这样能彻底避免操做不当致使的内存泄漏,这种主动清理的方式比惰性删除有效。

7、黄金分割 - 魔数0x61c88647

在这里插入图片描述

1.黄金分割数与斐波那契数列

首先复习一下斐波那契数列,下面的推导过程来自某搜索引擎的wiki:

斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …
通项公式:假设F(n)为该数列的第n项(n ∈ N*),那么这句话能够写成以下形式:F(n) = F(n-1) + F(n-2)。
有趣的是,这样一个彻底是天然数的数列,通项公式倒是用无理数来表达的。并且当n趋向于无穷大时,前一项与后一项的比值愈来愈逼近0.618(或者说后一项与前一项的比值小数部分愈来愈逼近0.618),而这个值0.618就被称为黄金分割数。证实过程以下:

在这里插入图片描述

黄金分割数的准确值为(根号5 - 1)/2,约等于0.618。

2.黄金分割数的应用

黄金分割数被普遍使用在美术、摄影等艺术领域,由于它具备严格的比例性、艺术性、和谐性,蕴藏着丰富的美学价值,可以激发人的美感。固然,这些不是本文研究的方向,咱们先尝试求出无符号整型和带符号整型的黄金分割数的具体值:

public static void main(String[] args) throws Exception {
    //黄金分割数 * 2的32次方 = 2654435769 - 这个是无符号32位整数的黄金分割数对应的那个值
    long c = (long) ((1L << 32) * (Math.sqrt(5) - 1) / 2);
    System.out.println(c);
    //强制转换为带符号为的32位整型,值为-1640531527
    int i = (int) c;
    System.out.println(i);
}

经过一个线段图理解一下:
在这里插入图片描述
也就是2654435769为32位无符号整数的黄金分割值,而-1640531527就是32位带符号整数的黄金分割值。而ThreadLocal中的哈希魔数正是1640531527(十六进制为0x61c88647)。为何要使用0x61c88647做为哈希魔数?这里提早说一下ThreadLocal在ThreadLocalMap(ThreadLocal在ThreadLocalMap以Key的形式存在)中的哈希求Key下标的规则:

哈希算法:keyIndex = ((i + 1) * HASH_INCREMENT) & (length - 1)

其中,i为ThreadLocal实例的个数,这里的HASH_INCREMENT就是哈希魔数0x61c88647,length为ThreadLocalMap中可容纳的Entry(K-V结构)的个数(或者称为容量)。在ThreadLocal中的内部类ThreadLocalMap的初始化容量为16,扩容后老是2的幂次方,所以咱们能够写个Demo模拟整个哈希的过程:

public class Main {

    private static final int HASH_INCREMENT = 0x61c88647;

    public static void main(String[] args) throws Exception {
        hashCode(4);
        hashCode(16);
        hashCode(32);
    }

    private static void hashCode(int capacity) throws Exception {
        int keyIndex;
        for (int i = 0; i < capacity; i++) {
            keyIndex = ((i + 1) * HASH_INCREMENT) & (capacity - 1);
            System.out.print(keyIndex);
            System.out.print(" ");
        }
        System.out.println();
    }
}

上面的例子中,咱们分别模拟了ThreadLocalMap容量为4,16,32的状况下,不触发扩容,而且分别”放入”4,16,32个元素到容器中,输出结果以下:

3 2 1 0 
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 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

每组的元素通过散列算法后刚好填充满了整个容器,也就是实现了完美散列。实际上,这个并非偶然,其实整个哈希算法能够转换为多项式证实:证实(x - y) HASH_INCREMENT != 2^n (n m),在x != y,n != m,HASH_INCREMENT为奇数的状况下恒成立,具体证实能够自行完成。HASH_INCREMENT赋值为0x61c88647的API文档注释以下:

连续生成的哈希码之间的差别(增量值),将隐式顺序线程本地id转换为几乎最佳分布的乘法哈希值,这些不一样的
哈希值最终生成一个2的幂次方的哈希表。
相关文章
相关标签/搜索