深入理解ThreadLocal

释放双眼,带上耳机,听听看~!

1 定义

ThreadLocal是存储线程局部变量的容器。

它为每一个使用该变量的线程都提供了一个变量值的副本,是Java中一种较为特殊的线程绑定机制。

每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本发生冲突。

2 原理分析

在Java中,Thread类代表线程。

查看Thread源码,如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1public class Thread implements Runnable {
2    ......
3
4    /**
5     * 与此线程相关的ThreadLocal值。
6     * 这个map由ThreadLocal类维护。
7     */
8    ThreadLocal.ThreadLocalMap threadLocals = null;
9
10    /*
11     * 与此线程相关的那些从父线程继承而来的ThreadLocal值。
12     * 这个map由InheritableThreadLocal类维护。
13     */
14    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
15
16

可以看出,在Thread中是通过ThreadLocal.ThreadLocalMap来发挥ThreadLocal的功能的。

下面来看一下ThreadLocal的工作原理。

ThreadLocal提供了set(T value)和get()方法,用来存取线程局部变量。

2.1 ThreadLocal的set(T value)方法

查看ThreadLocal的set(T value)方法的源码:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1    /**
2     * 设置当前线程中线程局部变量的值
3     */
4    public void set(T value) {
5        // 获取当前线程对象
6        Thread t = Thread.currentThread();
7        // 获取ThreadLocalMap
8        ThreadLocalMap map = getMap(t);
9        // 如果map存在,就将设置的value存入map中;如果map不存在,就创建一个map并写入值
10        if (map != null)
11            map.set(this, value);
12        else
13            createMap(t, value);
14    }
15
16

getMap(t)源码如下:


1
2
3
4
5
1    ThreadLocalMap getMap(Thread t) {
2        return t.threadLocals;
3    }
4
5

createMap(t, value)的源码如下:


1
2
3
4
5
1    void createMap(Thread t, T firstValue) {
2        t.threadLocals = new ThreadLocalMap(this, firstValue);
3    }
4
5

可以看出,getMap是将当前线程对象t传入,然后获取当前线程对象t中threadLocals的引用。因为每个线程Thread都有自己的threadLocals,所以getMap(t)返回的ThreadLocalMap是每个线程自己的。

每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。

ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己的,完全不会有并发错误。

ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是所设置的对象。

2.2 ThreadLocal的get()方法

查看ThreadLocal的get()方法源码:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
1    /**
2     * 设置当前线程中线程局部变量的值
3     */
4    public T get() {
5        // 获取当前线程对象
6        Thread t = Thread.currentThread();
7        // 获取ThreadLocalMap
8        ThreadLocalMap map = getMap(t);
9        // 1.如果map存在
10        if (map != null) {
11            // 从map中获取键值对,键为当前ThreadLocal对象
12            ThreadLocalMap.Entry e = map.getEntry(this);
13            // 如果键值对存在
14            if (e != null) {
15                // 获取键值对中的值,并返回
16                @SuppressWarnings("unchecked")
17                T result = (T)e.value;
18                return result;
19            }
20        }
21        // 2.如果map不存在,设置初始化值并返回它
22        return setInitialValue();
23    }
24
25

getMap(t)源码如下:


1
2
3
4
5
1    ThreadLocalMap getMap(Thread t) {
2        return t.threadLocals;
3    }
4
5

setInitialValue()源码如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1    /**
2     * 设置初始化值
3     */
4    private T setInitialValue() {
5        T value = initialValue();
6        Thread t = Thread.currentThread();
7        ThreadLocalMap map = getMap(t);
8        if (map != null)
9            map.set(this, value);
10        else
11            createMap(t, value);
12        return value;
13    }
14
15    /**
16     * 初始化
17     */
18    protected T initialValue() {
19        return null;
20    }
21
22

可以看出,和set(T value)同理,也是通过ThreadLocalMap来获取线程的局部变量的。这样就能保证获取到的值都是每个线程自己的副本,线程之间不会相互影响。

2.3 总结

实际使用的时候,ThreadLocal变量作为类中的实例域,会被所有的线程共享。

但是,每个线程获取ThreadLocal对象之后,通过set(T value)方法设置值的时候,首先是获取线程自己的ThreadLocalMap对象,然后将设置的值存入ThreadLocalMap中,键为这个线程获取的ThreadLocal对象,值为设置的value。

所以,对于同一个ThreadLocal来说,在每个线程中的ThreadLocalMap中的键都是同一个对象;每个线程中的ThreadLocalMap可以有多个键值对,那么不同的键对应的就是不同的ThreadLocal实例域对象。

get()方法的原理和set(T value)一样的,也是通过通过ThreadLocalMap来实现线程隔离的。

3 需要注意的点

3.1 ThreadLocalMap不是Map接口的实现

ThreadLocalMap不是Map接口的实现,内部使用的是Entry[] table来保存键值对的。


1
2
3
4
5
6
7
1   /**
2    * The table, resized as necessary.
3    * table.length MUST always be a power of two.
4    */
5    private Entry[] table;
6
7

并且,通过hash算法来做散列:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1    // 计算数组索引
2    int i = key.threadLocalHashCode & (len-1);
3
4    ......
5
6    // 生成threadLocal对象的hash值
7    private final int threadLocalHashCode = nextHashCode();
8
9    ......
10    
11    /**
12     * 返回下一个hash值
13     */
14    private static int nextHashCode() {
15        return nextHashCode.getAndAdd(HASH_INCREMENT);
16    }
17
18
19

3.2 ThreadLocal使用不当引发的内存泄漏问题

ThreadLocal可能存在内存泄漏问题的根源在于:

ThreadLocal中的key是弱引用的。

源码:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1    /**
2     * Entry继承WeakReference,
3     * map中的键(ThreadLocal对象)是弱引用。
4     * 注意,null键(entry.get() == null)表示这个键不再被引用,因此这个entry可以从数组中移除。
5     */
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

3.2.1 为什么使用弱引用

要理解为什么ThreadLocalMap中需要使用WeakReference作为key类型,那么首先需要理解WeakReference的意义。

WeakReference是Java语言规范中为了区别直接的对象引用(程序中通过构造函数声明出来的对象引用)而定义的另外一种引用关系。WeakReference标志性的特点是:不会影响到被引用对象的GC回收行为(即,只要对象被除了WeakReference对象之外所有的对象解除引用后,该对象便可以被GC回收),只不过在被引用对象回收之后,通过WeakReference获得被引用对象时程序会返回null。

理解了WeakReference之后,ThreadLocalMap使用它的目的也相对清晰了:

当ThreadLocal实例可以被GC回收时(该实例没有任何强引用了),系统可以通过弱引用检测到该ThreadLocal对应的Entry是否已经过期(根据reference.get() == null来判断,如果为true则表示过期,程序内部称为stale slots)来做一些自动清除工作,否则如果不清除的话容易产生内存无法释放的问题——value对应的对象即使不再使用,但由于被ThreadLocalMap所引用导致无法被GC回收。

3.2.2 内存泄漏问题

下面的图展示了ThreadLocal、ThreadLocalMap、Entry之间的关系:

深入理解ThreadLocal

上图中,实线代表强引用,虚线代表弱引用。

  • 如果ThreadLocal实例对象的外部强引用ThreadLocalRef被置为null(threadLocalRef == null)的话,ThreadLocal实例对象就没有一条引用链路可达,很显然在GC的时候势必会被回收。

  • 因此这个ThreadLocal实例对应的Entry就存在key为null的情况,程序是无法通过一个key为null去访问到该Entry的value。

  • 如果当前线程未被销毁。那么,就存在这样一条引用链:currentThreadRef -> currentThread -> threadLocalMap -> entry -> valueRef -> valueMemory,导致在垃圾回收的时进行可达性分析的时候,value可达从而不会被回收掉,但是该value永远不能被访问到了,这样导致了 **内存泄漏 ** 的问题。

  • 当然,如果线程执行结束后,栈被销毁,那么threadLocalRef、currentThreadRef就会断掉。因此ThreadLocal、ThreadLocalMap、Entry都会被回收掉,对应的value也会被回收,不会出现**内存泄漏 **。

  • 可是,在实际使用中我们大多数情况都会用线程池去维护我们的线程,线程在使用完之后并不会被销毁,而是返回到线程池中,这时候很可能出现ThreadLocal内存泄漏的问题,需要我们多加关注。

3.2.3 已经做出了哪些改进?

实际上,为了解决ThreadLocal潜在的内存泄漏的问题,Josh Bloch and Doug Lea大师已经做了一些改进。

在ThreadLocal的set和get方法中都有相应的处理。

下文为了叙述,针对key == null的entry,源码注释为stale entry,直译为“不新鲜的entry”,这里我就称之为“脏entry”。

查看ThreadLocalMap的set方法:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
1    private void set(ThreadLocal<?> key, Object value) {
2
3        // We don't use a fast path as with get() because it is at
4        // least as common to use set() to create new entries as
5        // it is to replace existing ones, in which case, a fast
6        // path would fail more often than not.
7
8        Entry[] tab = table;
9        int len = tab.length;
10        int i = key.threadLocalHashCode & (len-1);
11
12        for (Entry e = tab[i];
13             e != null;
14             e = tab[i = nextIndex(i, len)]) {
15            ThreadLocal<?> k = e.get();
16
17            if (k == key) {
18                e.value = value;
19                return;
20            }
21
22            // 脏entry
23            if (k == null) {
24                // 替换这个脏entry
25                replaceStaleEntry(key, value, i);
26                return;
27            }
28        }
29
30        // 插入新的entry
31        tab[i] = new Entry(key, value);
32        int sz = ++size;
33        if (!cleanSomeSlots(i, sz) && sz >= threshold)
34            rehash();
35    }
36
37

在该方法中针对 脏entry做了这样的处理:

  1. 如果当前table[i]!= null的话,说明hash冲突,就需要向后环形查找,若在查找过程中遇到脏entry就通过replaceStaleEntry(key, value, i)进行处理;
  2. 如果当前table[i] == null的话,说明这是新的entry,可以直接插入,但是插入后会调用cleanSomeSlots(i, sz)方法检测并清除脏entry。

具体的源码分析,参见https://www.jianshu.com/p/dde92ec37bd1

3.3 ThreadLocal最佳实践

因为在线程池中使用ThreadLocal的时候,很可能引发内存泄漏的问题,所以:

在确定不再使用ThreadLocal的时候,请调用remove()方法删除数据。

下面是remove的源码:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
1    /**
2     * 移除当前ThreadLocal对应的线程局部变量
3     */
4    public void remove() {
5        // 拿到当前线程的ThreadLocalMap
6        ThreadLocalMap m = getMap(Thread.currentThread());
7        // 如果map不是null,调用map的remove方法删除当前这个ThreadLocal对应的Entry
8        if (m != null)
9            m.remove(this);
10    }
11
12
13    /**
14     * 移除key对应的Entry
15     */
16    private void remove(ThreadLocal<?> key) {
17        // 根据key计算数组中的索引位置i
18        Entry[] tab = table;
19        int len = tab.length;
20        int i = key.threadLocalHashCode & (len-1);
21            
22        for (Entry e = tab[i];
23            e != null;
24            e = tab[i = nextIndex(i, len)]) {
25            // 查找到与key对应的Entry
26            if (e.get() == key) {
27                // 通过clear方法将key的引用置为null,这个entry就变成了一个“脏Entry”
28                e.clear();
29                // 通过
30                expungeStaleEntry(i);
31                return;
32            }
33        }
34    }
35
36
37    /**
38     * 删除“脏Entry”
39     */
40    private int expungeStaleEntry(int staleSlot) {
41            Entry[] tab = table;
42            int len = tab.length;
43
44            // 删除entry
45            tab[staleSlot].value = null;
46            tab[staleSlot] = null;
47            size--;
48
49            // Rehash until we encounter null
50            Entry e;
51            int i;
52            for (i = nextIndex(staleSlot, len);
53                 (e = tab[i]) != null;
54                 i = nextIndex(i, len)) {
55                ThreadLocal<?> k = e.get();
56                if (k == null) {
57                    e.value = null;
58                    tab[i] = null;
59                    size--;
60                } else {
61                    int h = k.threadLocalHashCode & (len - 1);
62                    if (h != i) {
63                        tab[i] = null;
64
65                        // Unlike Knuth 6.4 Algorithm R, we must scan until
66                        // null because multiple entries could have been stale.
67                        while (tab[h] != null)
68                            h = nextIndex(h, len);
69                        tab[h] = e;
70                    }
71                }
72            }
73            return i;
74        }
75
76

4 应用场景

最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。

例如:


1
2
3
4
5
6
7
8
9
10
11
1    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
2        public Connection initialValue() {
3            return DriverManager.getConnection(DB_URL);
4        }
5    };
6
7    public static Connection getConnection() {
8        return connectionHolder.get();
9    }
10
11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1    private static final ThreadLocal<Session> threadSession = new ThreadLocal<>();
2
3    public static Session getSession() throws InfrastructureException {
4        Session s = (Session) threadSession.get();
5        try {
6            if (s == null) {
7                s = getSessionFactory().openSession();
8                threadSession.set(s);
9            }
10        } catch (HibernateException ex) {
11            throw new InfrastructureException(ex);
12        }
13        return s;
14    }
15
16

给TA打赏
共{{data.count}}人
人已打赏
安全经验

海外中文博客与Adsense

2021-10-11 16:36:11

安全经验

安全咨询服务

2022-1-12 14:11:49

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索