ThreadLocal之深度解读


原文:https://mp.weixin.qq.com/s/K-8aNF3gqg3ekrRbTsjo9w

在展开深入分析之前,咱们先来看一个官方示例:

出处来源于ThreadLocal类上的注释,其中main方法是笔者加上的。

 1 import java.util.concurrent.atomic.AtomicInteger;
 2
 3public class ThreadId {
 4    // Atomic integer containing the next thread ID to be assigned
 5    private static final AtomicInteger nextId = new AtomicInteger(0);
 6
 7    // Thread local variable containing each thread's ID
 8    private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
 9        @Override
10        protected Integer initialValue() {
11            return nextId.getAndIncrement();
12        }
13    };
14
15    // Returns the current thread's unique ID, assigning it if necessary
16    public static int get() {
17        return threadId.get();
18    }
19
20    public static void main(String[] args) {
21        for (int i = 0; i < 5; i++) {
22            new Thread(new Runnable() {
23                @Override
24                public void run() {
25                    System.out.println("threadName=" + Thread.currentThread().getName() + ",threadId=" + ThreadId.get());
26                }
27            }).start();
28        }
29    }
30}

运行结果如下:

1 threadName=Thread-0,threadId=0
2 threadName=Thread-1,threadId=1
3 threadName=Thread-2,threadId=2
4 threadName=Thread-3,threadId=3
5 threadName=Thread-4,threadId=4

我问:看完这个例子,您知道ThreadLocal是干什么的了吗? 您答:不知道,没感觉,一个hello world的例子,完全激发不了我的兴趣。 您问:那个谁,你敢不敢举一个生产级的、工作中真实能用的例子? 我答:得,您是"爷",您说啥我就做啥。还记得《Spring Cloud Netflix Zuul源码分析之请求处理篇》中提到的RequestContext吗?这就是一个生产级的运用啊。Zuul核心原理是什么?就是将请求放入过滤器链中经过一个个过滤器的处理,过滤器之间没有直接的调用关系,处理的结果都是存放在RequestContext里传递的,而这个RequestContext就是一个ThreadLocal类型的对象啊!!!

1public class RequestContext extends ConcurrentHashMap<String, Object> {
 2
 3    protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
 4        @Override
 5        protected RequestContext initialValue() {
 6            try {
 7                return contextClass.newInstance();
 8            } catch (Throwable e) {
 9                throw new RuntimeException(e);
10            }
11        }
12    };
13
14    public static RequestContext getCurrentContext() {
15        if (testContext != null) return testContext;
16
17        RequestContext context = threadLocal.get();
18        return context;
19    }
20}

以Zuul中前置过滤器DebugFilter为例:

 1 public class DebugFilter extends ZuulFilter {
 2
 3    @Override
 4    public Object run() {
 5        // 获取ThreadLocal对象RequestContext
 6        RequestContext ctx = RequestContext.getCurrentContext();
 7        // 它是一个map,可以放入数据,给后面的过滤器使用
 8        ctx.setDebugRouting(true);
 9        ctx.setDebugRequest(true);
10        return null;
11    }
12}

What is this

它是啥?它是一个支持泛型的java类啊,抛开里面的静态内部类ThreadLocalMap不说,其实它没几行代码,不信,您自己去看看。它用来干啥?类上注释说的很明白:

它能让线程拥有了自己内部独享的变量

每一个线程可以通过get、set方法去进行操作

可以覆盖initialValue方法指定线程独享的值

通常会用来修饰类里private static final的属性,为线程设置一些状态信息,例如user ID或者Transaction ID

每一个线程都有一个指向threadLocal实例的弱引用,只要线程一直存活或者该threadLocal实例能被访问到,都不会被垃圾回收清理掉

爱提问的您,一定会有疑惑,demo里只是调用了ThreadLocal.get()方法,它如何实现这伟大的一切呢?这就是笔者下面要讲的内容,走着~~~

我有我的map

话不多说,我们来看get方法内部实现:

get()源码

 1 public T get() {
 2    Thread t = Thread.currentThread();
 3    ThreadLocalMap map = getMap(t);
 4    if (map != null) {
 5        ThreadLocalMap.Entry e = map.getEntry(this);
 6        if (e != null) {
 7            @SuppressWarnings("unchecked")
 8            T result = (T)e.value;
 9            return result;
10        }
11    }
12    return setInitialValue();
13}

逻辑很简单:

  • 获取当前线程内部的ThreadLocalMap
  • map存在则获取当前ThreadLocal对应的value值
  • map不存在或者找不到value值,则调用setInitialValue,进行初始化

setInitialValue()源码

 1 private T setInitialValue() {
 2    T value = initialValue();
 3    Thread t = Thread.currentThread();
 4    ThreadLocalMap map = getMap(t);
 5    if (map != null)
 6        map.set(this, value);
 7    else
 8        createMap(t, value);
 9    return value;
10 }

逻辑也很简单:

调用initialValue方法,获取初始化值【调用者通过覆盖该方法,设置自己的初始化值】

  • 获取当前线程内部的ThreadLocalMap
  • map存在则把当前ThreadLocal和value添加到map中
  • map不存在则创建一个ThreadLocalMap,保存到当前线程内部

时序图 image.png

小结 至此,您能回答ThreadLocal的实现原理了吗?没错,map,一个叫做ThreadLocalMap的map,这是关键。每一个线程都有一个私有变量,是ThreadLocalMap类型。当为线程添加ThreadLocal对象时,就是保存到这个map中,所以线程与线程间不会互相干扰。总结起来,一句话:我有我的young,哦,不对,是我有我的map。弄清楚了这些,是不是使用的时候就自信了很多。但是,这是不是就意味着可以大胆的去使用了呢?其实,不尽然,有一个“大坑”在等着你。

神奇的remove

那个“大坑”指的就是因为ThreadLocal使用不当,会引发内存泄露的问题。笔者给出两段示例代码,来说明这个问题。

代码出处来源于Stack Overflow:https://stackoverflow.com/questions/17968803/threadlocal-memory-leak

示例一:

 1 public class MemoryLeak {
 2
 3    public static void main(String[] args) {
 4        new Thread(new Runnable() {
 5            @Override
 6            public void run() {
 7                for (int i = 0; i < 1000; i++) {
 8                    TestClass t = new TestClass(i);
 9                    t.printId();
10                    t = null;
11                }
12            }
13        }).start();
14    }
15
16    static class TestClass{
17        private int id;
18        private int[] arr;
19        private ThreadLocal<TestClass> threadLocal;
20        TestClass(int id){
21            this.id = id;
22            arr = new int[1000000];
23            threadLocal = new ThreadLocal<>();
24            threadLocal.set(this);
25        }
26
27        public void printId(){
28            System.out.println(threadLocal.get().id);
29        }
30    }
31}

运行结果:

 10
 21
 32
 43
 5...省略...
 6440
 7441
 8442
 9443
10444
11Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
12    at com.gentlemanqc.MemoryLeak$TestClass.<init>(MemoryLeak.java:33)
13    at com.gentlemanqc.MemoryLeak$1.run(MemoryLeak.java:16)
14    at java.lang.Thread.run(Thread.java:745)

对上述代码稍作修改,请看:

 1public class MemoryLeak {
 2
 3    public static void main(String[] args) {
 4        new Thread(new Runnable() {
 5            @Override
 6            public void run() {
 7                for (int i = 0; i < 1000; i++) {
 8                    TestClass t = new TestClass(i);
 9                    t.printId();
10                    t.threadLocal.remove();
11                }
12            }
13        }).start();
14    }
15
16    static class TestClass{
17        private int id;
18        private int[] arr;
19        private ThreadLocal<TestClass> threadLocal;
20        TestClass(int id){
21            this.id = id;
22            arr = new int[1000000];
23            threadLocal = new ThreadLocal<>();
24            threadLocal.set(this);
25        }
26
27        public void printId(){
28            System.out.println(threadLocal.get().id);
29        }
30    }
31}

运行结果:

10
21
32
43
5...省略...
6996
7997
8998
9999

一个内存泄漏,一个正常完成,对比代码只有一处不同:t = null改为了t.threadLocal.remove(); 哇,神奇的remove!!!笔者先留个悬念,暂且不去分析原因。我们先来看看上述示例中涉及到的两个方法:set()和remove()。

set(T value)源码

1public void set(T value) {
2    Thread t = Thread.currentThread();
3    ThreadLocalMap map = getMap(t);
4    if (map != null)
5        map.set(this, value);
6    else
7        createMap(t, value);
8}

逻辑很简单:

  • 获取当前线程内部的ThreadLocalMap
  • map存在则把当前ThreadLocal和value添加到map中
  • map不存在则创建一个ThreadLocalMap,保存到当前线程内部

remove源码

1public void remove() {
2    ThreadLocalMap m = getMap(Thread.currentThread());
3    if (m != null)
4     m.remove(this);
5}

就一句话,获取当前线程内部的ThreadLocalMap,存在则从map中删除这个ThreadLocal对象。

小结

讲到这里,ThreadLocal最常用的四种方法都已经说完了,细心的您是不是已经发现,每一个方法都离不开一个类,那就是ThreadLocalMap。所以,要更好的理解ThreadLocal,就有必要深入的去学习这个map。

无处不在的ThreadLocalMap

还是老规矩,先来看看类上的注释,翻译过来就是这么几点:

  • ThreadLocalMap是一个自定义的hash map,专门用来保存线程的thread local变量
  • 它的操作仅限于ThreadLocal类中,不对外暴露
  • 这个类被用在Thread类的私有变量threadLocals和inheritableThreadLocals上
  • 为了能够保存大量且存活时间较长的threadLocal实例,hash table entries采用了WeakReferences作为key的类型
  • 一旦hash table运行空间不足时,key为null的entry就会被清理掉

我们来看下类的声明信息:

 1 static class ThreadLocalMap {
 2
 3    // hash map中的entry继承自弱引用WeakReference,指向threadLocal对象
 4    // 对于key为null的entry,说明不再需要访问,会从table表中清理掉
 5    // 这种entry被成为“stale entries”
 6    static class Entry extends WeakReference<ThreadLocal<?>> {
 7        /** The value associated with this ThreadLocal. */
 8        Object value;
 9
10        Entry(ThreadLocal<?> k, Object v) {
11            super(k);
12            value = v;
13        }
14    }
15
16    private static final int INITIAL_CAPACITY = 16;
17
18    private Entry[] table;
19
20    private int size = 0;
21
22    private int threshold; // Default to 0
23
24    private void setThreshold(int len) {
25        threshold = len * 2 / 3;
26    }
27
28    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
29        table = new Entry[INITIAL_CAPACITY];
30        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
31        table[i] = new Entry(firstKey, firstValue);
32        size = 1;
33        setThreshold(INITIAL_CAPACITY);
34    }
35}

当创建一个ThreadLocalMap时,实际上内部是构建了一个Entry类型的数组,初始化大小为16,阈值threshold为数组长度的2/3,Entry类型为WeakReference,有一个弱引用指向ThreadLocal对象。

为什么Entry采用WeakReference类型? Java垃圾回收时,看一个对象需不需要回收,就是看这个对象是否可达。什么是可达,就是能不能通过引用去访问到这个对象。(当然,垃圾回收的策略远比这个复杂,这里为了便于理解,简单给大家说一下)。

jdk1.2以后,引用就被分为四种类型:强引用、弱引用、软引用和虚引用。强引用就是我们常用的Object obj = new Object(),obj就是一个强引用,指向了对象内存空间。当内存空间不足时,Java垃圾回收程序发现对象有一个强引用,宁愿抛出OutofMemory错误,也不会去回收一个强引用的内存空间。而弱引用,即WeakReference,意思就是当一个对象只有弱引用指向它时,垃圾回收器不管当前内存是否足够,都会进行回收。反过来说,这个对象是否要被垃圾回收掉,取决于是否有强引用指向。ThreadLocalMap这么做,是不想因为自己存储了ThreadLocal对象,而影响到它的垃圾回收,而是把这个主动权完全交给了调用方,一旦调用方不想使用,设置ThreadLocal对象为null,内存就可以被回收掉。

内存溢出问题解答 至此,该做的铺垫都已经完成了,此时,我们可以来看看上面那个内存泄漏的例子。示例中执行一次for循环里的代码后,对应的内存状态:

循环一次内存状态

  • t为创建TestClass对象返回的引用,临时变量,在一次for循环后就执行出栈了
  • thread为创建Thread对象返回的引用,run方法在执行过程中,暂时不会执行出栈

调用t=null后,虽然无法再通过t访问内存地址,但是当前线程依旧存活,可以通过thread指向的内存地址,访问到Thread对象,从而访问到ThreadLocalMap对象,访问到value指向的内存空间,访问到arr指向的内存空间,从而导致Java垃圾回收并不会回收int[1000000]@541这一片空间。那么随着循环多次之后,不被回收的堆空间越来越大,最后抛出java.lang.OutOfMemoryError: Java heap space。 您问:那为什么调用t.threadLocal.remove()就可以呢? 我答:这就得看remove方法里究竟做了什么了,请看:

image.png

是不是恍然大悟?来看下调用remove方法之后的内存状态: image.png

因为remove方法将referent和value都被设置为null,所以ThreadLocal@540和Memory$TestClass@538对应的内存地址都变成不可达,Java垃圾回收自然就会回收这片内存,从而不会出现内存泄漏的错误。

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×