Java ThreadLocal 笔记

  Java多线程一般都会问到 ThreadLocal,既能考数据结构,又能考线程,还能考JVM。用起来很简单,但是理解需要花费时间。

ThreadLocal是什么

 ThreadLocal 线程本地变量/线程本地存储

此类提供线程局部变量。这些变量不同于普通的对应变量,因为每个访问一个(通过其get或set方法)的线程都有自己独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,它们希望将状态与线程相关联(例如,用户ID或事务ID)。
只要线程是活动的并且ThreadLocal实例是可访问的,则每个线程都对其线程局部变量的副本持有隐式引用。 线程消失后,其线程本地实例的所有副本都将进行垃圾回收(除非存在对这些副本的其他引用)。

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

Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

为什么用ThreadLocal

  简单地说,想买个房子自己住,不想和别合租住在一个房子里。
  每个线程都有自己的房子(线程本地变量),不和别的线程共用,没有资源争抢。 ThreadLocal是为每个线程创建一个单独的变量副本,每个线程都可以改变自己的变量副本而不影响其它线程所对应的副本。

什么时候用ThreadLocal

当想给每个线程设置一个私有变量的时候。

使用样例

例如,下面的类生成每个线程本地的唯一标识符。 线程的ID是在第一次调用ThreadId.get()时分配的,并且在以后的调用中保持不变。

For example, the class below generates unique identifiers local to each thread. A thread’s id is assigned the first time it invokes ThreadId.get() and remains unchanged on subsequent calls.

import java.util.concurrent.atomic.AtomicInteger;

 public class ThreadId {
     // Atomic integer containing the next thread ID to be assigned
     private static final AtomicInteger nextId = new AtomicInteger(0);

     // Thread local variable containing each thread's ID
     private static final ThreadLocal<Integer> threadId =
         new ThreadLocal<Integer>() {
             @Override protected Integer initialValue() {
                 return nextId.getAndIncrement();
         }
     };

     // Returns the current thread's unique ID, assigning it if necessary
     public static int get() {
         return threadId.get();
     }
 }

ThreadLocal实现细节

初始化方法

/**
 * Creates a thread local variable. The initial value of the variable is
 * determined by invoking the {@code get} method on the {@code Supplier}.
 *
 * @param <S> the type of the thread local's value
 * @param supplier the supplier to be used to determine the initial value
 * @return a new thread local variable
 * @throws NullPointerException if the specified supplier is null
 * @since 1.8
 */
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

/**
 * Creates a thread local variable.
 * @see #withInitial(java.util.function.Supplier)
 */
public ThreadLocal() {
}

新增 修改

/**
 * Sets the current thread's copy of this thread-local variable
 * to the specified value.  Most subclasses will have no need to
 * override this method, relying solely on the {@link #initialValue}
 * method to set the values of thread-locals.
 *
 * @param value the value to be stored in the current thread's copy of
 *        this thread-local.
 */
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
/**
 * 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;
}
/**
 * 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);
}

删除

/**
 * Removes the current thread's value for this thread-local
 * variable.  If this thread-local variable is subsequently
 * {@linkplain #get read} by the current thread, its value will be
 * reinitialized by invoking its {@link #initialValue} method,
 * unless its value is {@linkplain #set set} by the current thread
 * in the interim.  This may result in multiple invocations of the
 * {@code initialValue} method in the current thread.
 *
 * @since 1.5
 */
 public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }

ThreadLocal里重要的数据结构ThreadLocalMap

为什么要用 ThreadLocalMap

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

ThreadLocalMap构造方法

/**
 * Construct a new map initially containing (firstKey, firstValue).
 * ThreadLocalMaps are constructed lazily, so we only create
 * one when we have at least one entry to put in it.
 */
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 Entry

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

该哈希表中的节点继承WeakReference,使用其主要参考字段作为键(始终为ThreadLocal对象)。 请注意,空键(即entry.get() == null)表示不再引用该键,因此条目可以从表中删除。 此类条目被引用在以下代码中为“陈旧条目”。

ThreadLocal内存溢出

给一个溢出的样例。

Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
package cn.wkq.java.juc;

import java.util.*;

/**
 * @author weikeqin
 * @date 2020-06-06 18:50
 */
public class ThreadLocalDemo {

    private static final ThreadLocal threadLocal = new ThreadLocal();

    /**
     * -Xmx10m
     *
     * @param args
     */
    public static void main(String[] args) {

        ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
        String traceId = UUID.randomUUID().toString();
        try {
            threadLocal.set(traceId);
            threadLocalDemo.method1();

            threadLocalDemo.method2();

            //
            for (int i = 0; i < 10000000; i++) {
                threadLocal.set(i); // 注释掉这行不会内存溢出
                if (i % 10000 == 0) {
                    System.out.println(threadLocal.get());
                }

            }


            // for gc
            for (int i = 0; i < 10; i++) {
                System.gc();
            }

            threadLocalDemo.method3();

        } catch (Exception e) {
            threadLocal.remove();
        }

    }


    /**
     *
     */
    private void method3() {

        // 使用到 traceId
        System.out.println("method3:" + threadLocal.get());
    }

    /**
     *
     */
    private void method2() {

        // 使用到 traceId
        System.out.println("method2:" + threadLocal.get());
    }

    /**
     *
     */
    private void method1() {

        // 使用到 traceId
        System.out.println("method1:" + threadLocal.get());
    }


}

如果在平时写代码很难遇到ThreadLocal内存溢出问题
但是在web服务里由于使用线程池,会出现一个线程一直 threadLocal.set(i) 的情况,而由于ThreadLocalMap和线程的生命周期一样长,并且ThreadLocalMap使用WeakReference,WeakReference只是针对key,但是value是强引用,导致set的value一直有引用(由于线程池复用线程,线程没有死亡),最终导致内存溢出。

内存泄漏的根本原因
所有Entry对象都被ThreadLocalMap类的实例化对象threadLocals持有,当ThreadLocal对象不再使用时,ThreadLocal对象在栈中的引用就会被回收,一旦没有任何引用指向ThreadLocal对象,Entry只持有弱引用的key就会自动在下一次YGC时被回收,而此时持有强引用的Entry对象并不会被回收。

简而言之: threadLocals对象中的entry对象不在使用后,没有及时remove该entry对象 ,然而程序自身也无法通过垃圾回收机制自动清除,从而导致内存泄漏。

References

[1] java-8-api-ThreadLocal
[2] ThreadLocal内存泄漏问题
[3] 深入分析 ThreadLocal 内存泄漏问题
[4] ThreadLocal 内存泄漏问题深入分析
[5] ThreadLocal为什么会导致内存泄漏?