likes
comments
collection
share

并发利器之ThreadLocal原理剖析

作者站长头像
站长
· 阅读数 14

今天来聊一下ThreadLocal,废话不多说直奔主题。

1、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).

ThreadLocal是线程本地变量,每个线程都有一个自己的、独立初始化的变量副本。

2、为什么要用到ThreadLocal?

并发场景下,会遇到多个线程操作同一个共享变量的情况,就可能会发生线程安全问题。这种场景通常可使用加锁来解决。

ThreadLocal解决这个问题换了个思路,既然多线程操作共享变量,那我将共享变量改成线程本地变量,每个线程都有一个变量副本,自己操作自己的副本。。。 采用了空间换时间的方式,省掉了加锁产生的时间开销,实现了线程隔离

3、ThreadLocal原理

我们借助源码来看一下ThreadLocal的执行原理。

3.1 源码分析

首先,来看一下从ThreadLocal的set()方法开始说起。

/**
 * 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
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 设置到ThreadLocalMap中
        map.set(this, value);
    else
        // 创建一个ThreadLocalMap,并添加
        createMap(t, value);
}

可以看到操作的是当前线程的ThreadLocalMap,这个ThreadLocalMap又是什么呢?

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

    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */
    // ThreadLocalMap维护的是一个Entry数组,可以回想一下Map
    private Entry[] table;
    
    /**
     * 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.
     */
    // 将ThreadLocal做为key存入Entry[]中
    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);
    }
}

回到刚才的set()方法,set方法就是将ThreadLocal设置到当前线程的实例变量ThreadLocalMap中(java.lang.Thread对象的实例变量threadLocals),ThreadLocal作为key,ThreadLocal是支持泛型的,泛型值作为value。

然后看一下ThreadLocal的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
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 根据key(即ThreadLocal)获取value(泛性值)
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 初始化ThreadLocalMap
    return setInitialValue();
}

get方法就是用key(即ThreadLocal)获取value(泛性值)泛性质的过程。

3.2 原理小结

  • ThreadLocal的set()get()操作的是Thread类的实例变量ThreadLocalMap
  • ThreadLocalMap内部维护着一个Entry数组,Entry的key是ThreadLocalvalue是ThreadLocal的值
  • 每个线程都有自己的一个变量副本,采用了空间换时间的方式,实现了线程隔离

4、ThreadLocal注意事项

不怕最牛的设计,就怕使用的不当。ThreadLocal有些地方需要特别了解一下。

4.1 ThreadLocalMap中的key是ThreadLocal对象

我们来看一下源码。上文源码分析提到的ThreadLocal的set()方法中map.set(this, value)调用的是这个私有方法。

public class ThreadLocal<T> {
    /**
     * 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;
        int i = key.threadLocalHashCode & (len-1);
        // 如果发生哈希冲突,则找下一个值为null的下标(开放地址法)
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
    
            if (k == key) {
                e.value = value;
                return;
            }
    
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        // key是传入的ThreadLocal<?>对象
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
}

4.2 ThreadLocalMap中的key是弱引用

首先我们先了解一下JAVA中的引用。

4.2.1 JAVA中的引用

JAVA中的引用类型有四种:

  • 强引用:当我们new一个对象时,Object object = new Object(),被创建的对象就是强引用,是最常见的一种引用。垃圾回收器不会回收还存在强引用关系的对象,即使抛出OOM也不会。
  • 软引用:SoftReference是JAVA中实现软引用的类,SoftReference<Object> objectSoftReference = new SoftReference(new Object()),GC是如果内存空间足够,软引用对象不会被回收,否则软引用对象回收。
  • 弱引用:WeakReference是JAVA中实现弱引用的类,WeakReference<Object> objectWeakReference = new WeakReference<>(new Object()), 弱引用对象被GC检测到了,会立即回收。
  • 虚引用:PhantomReference是JAVA中实现虚引用的类,虚引用和几乎没有引用一样,并且get()方法返回不了对象实例,可以用来当对象被finalize之后做一些事情。
// 与其他引用不同的是,PhantomReference必须传入ReferenceQueue
PhantomReference phantomReference = new PhantomReference(new Object(),null);
// get方法总是返回null
System.out.println("phantomReference:"+phantomReference.get());

4.2.2 为什么是WeakReference

防止内存泄漏提供一层保障

1、一种情况,如果key是强引用,当ThreadLocal没有外部引用时,还保持着强引用,GC不会回收,如果线程结束前也没有删除,会导致内存泄漏

2、一种情况,如果key是弱引用,当ThreadLocal没有外部引用时,GC会回收弱引用,当然如果线程结束前也没有GC也没有调用删除,还是会导致内存泄漏,所以说提供了一层保障。

4.3 ThreadLocal使用不当会导致内存泄漏!

4.3.1 什么是内存泄漏?

内存泄漏:程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

4.3.1 ThreadLocal如何导致内存泄漏?

上边介绍为什么ThreadLocalMap中的key是弱引用时其实已经说了,总结一下就是如果ThreadLocal线程结束前没有GC也没有调用删除,导致内存没有及时释放。

4.4 ThreadLocal的remove()

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

删除此线程本地变量的值。线程结束前一定要调用此方法。

4.5 ThreadLocal要声明为static?

阿里巴巴开发规约中提到,ThreadLocal建议使用static修饰。

ThreadLocal是针对一个线程内所有操作共有的,所以设置为静态变量,所以此类实例共享此静态变量,也就是说在类第一次使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操纵这个变量。

总结一下就是设置为静态,可以省频繁创建对象的内存开销。

5、一个ThreadLocal导致内存泄漏的例子

创建一个线程为1的线程池。

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 60l, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10));
ThreadLocal<Integer> threadLocal = new ThreadLocal();
for (int i = 0; i < 3; i++) {
    int finalI = i;
    threadPoolExecutor.execute(() -> {
        System.out.println("获取到的值为:" + threadLocal.get());
        if(null == threadLocal.get()){
            threadLocal.set(finalI);
            System.out.println("设置的值为:" + finalI);
        }
    });
}

查看执行结果。

获取到的值为:null
设置的值为:0
获取到的值为:0
获取到的值为:0

可以看到只有第一次设置了值,第二次开始用的都是上一个线程设置的值,第一个的内存没有释放掉还在继续使用。

6、ThreadLocal使用场景

6.1、常见使用场景

  • 日志MDC类。
  • 事务注解@Transational
  • 。。。

6.2、项目中使用

项目中可以根据不同场景灵活使用ThreadLocal。

在这里简单实现一个通过添加一个注解就能统计接口执行时间的例子。

首先声明注解。

@Target({ElementType.METHOD})       
@Retention(RetentionPolicy.RUNTIME) 
@Documented
public @interface TimeConsumingLog {
}

编写AOP。

@Aspect        
@Component 
public class TimeConsumingLogAop {

    @Pointcut(value = "@annotation(com.shuaijie.config.annotation.TimeConsumingLog)")
    public void pointcut() {
    }

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

    @Before("pointcut()")
    public void beforeAdvice() {
        StopWatch stopWatch = new StopWatch("统计接口耗时");
        stopWatch.start();
        threadLocal.set(stopWatch);
    }

    @After("pointcut()")
    public void afterAdvice() {
        StopWatch stopWatch = threadLocal.get();
        if (null != stopWatch) {
            if (stopWatch.isRunning()) {
                stopWatch.stop();
            }
            System.out.println(stopWatch.prettyPrint());
        }
        threadLocal.remove();
    }

}

StopWatch是Spring提供的一个工具类,可以统计耗时。

使用注解。

@TimeConsumingLog
@PostMapping("/user/add")
public Boolean addUser(@RequestBody User user) {
    return Boolean.TRUE;
}

查看执行结果。

StopWatch '统计接口耗时': running time = 13842700 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
013842700  100%  

自定义注解详细使用可以看下我的另一篇优雅编码技巧之Spring AOP

7、总结

  • ThreadLocal采用了空间换时间的方式,实现了共享变量的线程隔离
  • ThreadLocal建议使用static修饰。
  • 线程结束前一定要调用remove()方法。