让我们谈谈你对 ThreadLocal 的理解

介绍 ThreadLocal

从 JDK1.2 开始,ThreadLocal 是一个被用来存储线程本地变量的类。在 ThreadLocal 中的变量在线程之间是独立的。当多个线程访问 ThreadLocal 中的变量,它们事实上访问的是自己当前线程在内存中的变量,这能确保这些变量是线程安全的。

我们通常使用 ThreadLocal 解决线程中的变量冲突问题。事实上,解决这类问题,我们通常考虑使用 synchronized。

例如,当解决 SimpleDateFormat 的线程安全问题时,SimpleDateFormat 不是线程安全的。不管 format() 方法或 parse() 方法,它使用了自己的内部 Calendar 对象。format() 方法设置时间,parse() 方法调用 Calendar.first.clear() 方法,再调用 Calendar 类的 set() 方法。如果一个线程刚刚调用 set(),期间有另一个线程直接调用了 clear() 方法,会导致 parse() 方法出现问题。

第一种解决办法,对使用了 SimpleDateformat 的方法加 synchronized,尽管线程安全有了保障,但是效率(吞吐量)变低了。一段时间内只有一个线程能使用这个方法。

 private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 public static synchronized String formatDate(Date date){
     return simpleDateFormat.format(date);
 }

第二种解决办法,将 SimpleDateFormat 对象放入 ThreadLocal,所以,每一个线程都有了自己的 SimpleDateFormat 对象。它们不会互相干扰,保证了线程安全。

 private static final ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal.withInitial(
     () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
 );public static String formatDate(Date date)
 {
    return simpleDateFormatThreadLocal.get().format(date);
 }

ThreadLocal 的基本用法

下面看看如何使用 ThreadLocal:

 ThreadLocal<Integer> threadLocal99 = new ThreadLocal<Integer>();
 threadLocal99.set(3);
 int num = threadLocal99.get();
 System.out.println("Number:"+num);
 threadLocal99.remove();
 System.out.println("Digital Empty:"+threadLocal99.get());

运行结果:

 Number: 3
 Number Empty: null

ThreadLocal 非常容易使用。main 线程向 ThreadLocal 中放入了多个变量,它们在线程执行期间都能访问。当线程执行结束,变量都将被销毁。只要没有调用 remove() 方法,当前线程都能从 ThreadLocal 获取到数据。

因为 ThreadLocal 存放在当前执行线程中,ThreadLocal 中的变量值仅能在当前线程(当前线程的子线程也可以获取到)中使用,这保证了线程安全。

让我们看一下 ThreadLocal 类的 set() 方法的源码:

 public void set(T value) {
     // Get the current thread
     Thread t = Thread.currentThread();
     // Get ThreadLocalMap
     ThreadLocal.ThreadLocalMap map = getMap(t);
     // Whether the ThreadLocalMap object is empty, if it is not empty, put the data directly into the ThreadLocalMap
     if (map != null)
         map.set(this, value);
     else
         createMap(t, value); // If the ThreadLocalMap object is empty, create the object first, and then assign the value.
 }

我们看到变量都存储在 ThreadLocalMap 变量中。所以,ThreadLocalMap 从哪里来的?

 ThreadLocalMap getMap(Thread t) {
     return t.threadLocals;
 }
 public class Thread implements Runnable {
     ......
     /* ThreadLocal values pertaining to this thread.This map is maintained
      * by the ThreadLocal class.*/
     ThreadLocal.ThreadLocalMap threadLocals = null;
     ......
 }

通过上面的源码,我们可以发现 ThreadLocalMap 变量是当前执行线程的一个变量,所以,存储在ThreadLocal 中的数据其实都是存放在当前线程的 threadLocals 变量中。threadLocals 变量存放在当前线程对象中。对于另一个线程,就是另一个线程对象。另一个线程对象中的数据是不能获取到的,所以 ThreadLocal 是隔离的。

ThreadLocalMap 如何存储数据呢?

ThreadLocalMap 是 ThreadLocal 的内部类。尽管名字中包含 Map,它并没有实现 Map 接口,但是结构和 Map 类似。

让我们谈谈你对 ThreadLocal 的理解_第1张图片

ThreadLocalMap 事实上是一个由 Entry 组成的 array,Entry 是 ThreadLocalMap 的一个内部类,继承了 WeakReference,同时将 Entry 的 key 设置成了 ThreadLocal 对象,并且还将 key 设置成了弱引用。ThreadLocalMap 的内部结构大概就是一个 Entry 的集合,其中 Entry 中的 key 和 value 的类型是固定的。

让我们谈谈你对 ThreadLocal 的理解_第2张图片

它和真正的 Map 还是有一些区别的,没有链表,这就意味着解决 hash 冲突的方法和 HashMap 不同(HashMap 采用的是拉链法,ThreadLocalMap 采用的是线性探测)。

一个线程中可以创建多个 ThreadLocal 对象,多个 ThreadLocal 对象可以存储多个数据,这些数据将会存储在 ThreadLocalMap 的 array 中。

接下来看看 ThreadLocalMap 中比较特别的方法 set():

 /**
  * Set the value associated with key.
  * @param key the thread local object
  * @param value the value to be set
  */
 private void set(ThreadLocal<?> key, Object value) {// We don't use a fast path as with get() because it is at
     // least as common to use set() to create new entries as
     // it is to replace existing ones, in which case, a fast
     // path would fail more often than not.Entry[] tab = table;
     int len = tab.length;
     // Position in the array
     int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];
          e != null;
          e = tab[i = nextIndex(i, len)]) {
         ThreadLocal<?> k = e.get();
         // If the current position is not empty, and the key at the current position is equal to the passed key, then the data at the current position will be overwritten
         if (k == key) {
             e.value = value;
             return;
         }
         // If the current position is empty, initialize an Entry object and place it at the current position.
         if (k == null) {
             replaceStaleEntry(key, value, i);
             return;
         }
     }
     // If the current position is not empty, and the key at the current position is not equal to the key to be assigned, then the next empty position will be found and the data will be placed directly in the next empty position.
     tab[i] = new Entry(key, value);
     int sz = ++size;
     if (!cleanSomeSlots(i, sz) && sz >= threshold)
         rehash();
 }

我们可以从 set() 方法中看出,在代码逻辑层面分为了 4 步:

第一步,通过对 ThreadLocal 对象的 hash 值和 array 的数组长度求 AND,得到数据放入当前 array 的那个位置

第二步,判断当前位置是否为空,如果为空,直接初始化一个 Entry 对象并将其放入这个位置

第三步,如果当前位置不为空,并且当前位置的 Entry 的 key 和传入的 key 相等,将当前位置的数据覆盖掉

第四步,如果当前位置不为空,并且当前位置的 Entry 的 key 和传入的 key 不相等,于是向后查找下一个不为空的位置,将数据放入空位置中(当遍历到 array 的末尾,会执行扩容操作)

get() 方法采用的是相同的逻辑。首先通过传入的 ThreadLocal 的 hash 值获取到元素在 Entry Array 中的位置,然后比较当前位置的 Entry 的 key 是否和 传入的 ThreadLocal 对象相同,如果相同,直接返回当前位置的数据,如果不相同,就需要判断 key 是否和 array 中的下一个元素相同。

 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);
 }
 /**
  * Version of getEntry method for use when key is not found in
  * its direct hash slot.
  *
  * @param  key the thread local object
  * @param  i the table index for key's hash code
  * @param  e the entry at table[i]
  * @return the entry associated with key, or null if no such
  */
 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;
 }

我们已经说过 ThreadLocal 是存放在单个线程中的,每一个线程都会有自己的数据,但是 ThreadLocal 中真正的对象数据是存放在堆中的,线程对象仅仅保存了对象的引用。

当我们使用 ThreadLocal 的时候,我们通常需要在执行的方法的上下文中共享线程局部的变量。例如,我的 main 线程执行方法中的代码,但是,方法中有一段代码创建了一个新的线程,这个方法中定义的 ThreadLocal 中的变量也被这个新创建的线程使用了…这种情况下,有必要从新线程中调用外部线程的数据,这些数据需要在线程之间共享。ThreadLocal 支持父子线程共享数据这种情况。比如:

 ThreadLocal threadLocalMain = new InheritableThreadLocal();
 threadLocalMain.set("Main thread variable");
 Thread t = new Thread() {
     @Override
     public void run() {
         super.run();
         System.out.println( "The variable obtained now is = "+ threadLocalMain.get());
     }
 };
 t.start();

执行结果:

 The variable obtained now is = main thread variable

上面的代码可以实现在父线程和子线程中共享数据,关键是通过 InheritableThreadLocal 去实现共享数据。

所以,它是如何做到的呢?

下面是在 Thread 类的 init() 方法中的一小段代码:

 if (inheritThreadLocals && parent.inheritableThreadLocals != null)
     this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

这段代码意味着,当创建一个线程时,如果当前线程的 inheritThreadLocals 变量存在,并且,父线程中的 inheritThreadLocals 变量不为 null,父线程的 inheritThreadLocals 变量中的数据会赋值给当前线程中的 inheritThreadLocals 变量。

ThreadLocal 的内存泄漏问题

我们前面介绍过,Entry 类是继承了 WeakReference 类,表明了 Entry 的 key 是弱引用。

让我们谈谈你对 ThreadLocal 的理解_第3张图片

弱引用类型用于描述非必须的对象。弱引用类型对象仅能存活到下一次垃圾收集之前。当垃圾收集器开始工作,不管当前内存是否充足,都会回收那些只有弱引用的对象。

弱引用依然是 ThreadLocal 对象本身,所以,通常来所,当线程执行完毕,ThreadLocal 对象会变成 null,并且弱引用对象如果为 null 就会在下一次 GC 时被清理,所以,Entry key 所占用的空间会被释放,但是 Entry 的 value 依然会占用内存,如果当前线程被重新使用(例如线程在线程池中),并且之后 ThreadLocal 不再被使用(用来从 ThreadLocalMap 获取数据),所以,ThreadLocalMap 中对应的 Entry 中的 value 会一直存在,这将会导致内存泄漏。 为了避免内存泄漏,在 ThreadLocal 使用后需要立即执行 remove() 方法去释放 key 和 value 占用的内存。

内存泄漏还是会发生,那为什么 ThreadLocal 需要设置为弱引用?

在一般情况下,对象是强引用的,但是,只要引用一直存在,强引用对象就不会被回收,所以,如果 Thread 被重用,在 Entry 中的 Key 和 Value 都不会被回收,这回导致 Key 和 value 都会占用内存。

但是,如果将 ThreadLocal 设置为弱引用,当 ThreadLocal 没有被强制引用时,它可以被回收。回收后,Entry 中的 key 变成 null。如果线程被重复使用,只要 ThreadLocal 被用来从 ThreadLocalMap 中获取数据,ThreadLocal 的 set、get 和其他方法就会被调用。当 set、get 和其他方法被调用,那些 Entry 中 key 是 null 的数据可能(get 方法:如果目标 Entry 的 key 为 null(回收了)或着发生了 Hash 冲突时,set 方法:启发式扫描。看源码)被扫描到。当发现 Entry 中有 Entry 的 key 是 null,这个 Entry 的 value 也会被设置为 null,所以,这个 Entry 的 value 也会被回收,这样可以进一步的避免内存溢出,当 ThreadLocalMap 重新 hash 的时候,也可以首先将那些 key 是 null 的 Entry 清理掉,如果 ThreadLocalMap 的空间不足,在执行扩容操作。

尽管 Entry 的 key 设置为弱引用,如果线程被重用并且 ThreadLocal 在后来的任务执行过程中不被使用,Entry 中的 value 还是占用内存,最终还是会导致内存溢出,所以,当使用 ThreadLocal 的时候,最终一定要调用 remove() 方法。

你可能感兴趣的:(java,java,jvm,开发语言)