JAVA并发-自问自答学ThreadLocal 前言 问答内

前言

ThreadLocal很多同学都搞不懂是什么东西,可以用来干嘛。但面试时却又经常问到,所以这次我和大家一起学习ThreadLocal这个类。

下面我就以面试问答的形式学习我们的——ThreadLocal类(源码分析基于JDK8)

本文同步发布于简书 :www.jianshu.com/p/807686414…

问答内容

1.

问:ThreadLocal了解吗?您能给我说说他的主要用途吗?

答:

  • 从JAVA官方对ThreadLocal类的说明定义(定义在示例代码中):ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过getset方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。
  • 我们可以得知ThreadLocal的作用是:ThreadLocal的作用是提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量的传递的复杂度。
  • 上述可以概述为:ThreadLocal提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程。

示例代码:

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
复制代码
/**
* 该类提供了线程局部 (thread-local) 变量。 这些变量不同于它们的普通对应物,
* 因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量
* 它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段
* 它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
*
* 例如,以下类生成对每个线程唯一的局部标识符。
*
* 线程 ID 是在第一次调用 UniqueThreadIdGenerator.getCurrentThreadId() 时分配的,
* 在后续调用中不会更改。
* <pre>
* import java.util.concurrent.atomic.AtomicInteger;
*
* public class ThreadId {
* // 原子性整数,包含下一个分配的线程Thread ID
* private static final AtomicInteger nextId = new AtomicInteger(0);
*
* // 每一个线程对应的Thread ID
* private static final ThreadLocal<Integer> threadId =
* new ThreadLocal<Integer>() {
* @Override protected Integer initialValue() {
* return nextId.getAndIncrement();
* }
* };
*
* // 返回当前线程对应的唯一Thread ID, 必要时会进行分配
* public static int get() {
* return threadId.get();
* }
* }
* </pre>
* 每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的
* 在线程消失之后,其线程局部实例的所有副本都会被垃圾回收,(除非存在对这些副本的其他引用)。
*
* @author Josh Bloch and Doug Lea
* @since 1.2
*/
public class ThreadLocal<T> {
·····
/**
* 自定义哈希码(仅在ThreadLocalMaps中有用)
* 可用于降低hash冲突
*/
private final int threadLocalHashCode = nextHashCode();

/**
* 生成下一个哈希码hashCode. 生成操作是原子性的. 从0开始
*
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();


/**
* 表示了连续分配的两个ThreadLocal实例的threadLocalHashCode值的增量
*/
private static final int HASH_INCREMENT = 0x61c88647;


/**
* 返回下一个哈希码hashCode
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
·····

}
  • 其中nextHashCode()方法就是一个原子类不停地去加上0x61c88647,这是一个很特别的数,叫斐波那契散列(Fibonacci Hashing),斐波那契又有一个名称叫黄金分割,也就是说将这个数作为哈希值的增量将会使哈希表的分布更为均匀。

2.

问:ThreadLocal实现原理是什么,它是怎么样做到局部变量不同的线程之间不会相互干扰的?

答:

  • 通常,如果我不去看源代码的话,我猜ThreadLocal是这样子设计的:每个ThreadLocal类都创建一个Map,然后用线程的ID threadID作为Mapkey,要存储的局部变量作为Mapvalue,这样就能达到各个线程的值隔离的效果。这是最简单的设计方法,JDK最早期的ThreadLocal就是这样设计的。
  • 但是,JDK后面优化了设计方案,现时JDK8 ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap哈希表,这个哈希表的keyThreadLocal实例本身,value才是真正要存储的值Object
  • 这个设计与我们一开始说的设计刚好相反,这样设计有如下几点优势:

1) 这样设计之后每个Map存储的Entry数量就会变小,因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。

2) 当Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。

ThreadLocal引用关系图- 图片来自于《简书 - 对ThreadLocal实现原理的一点思考》

ThreadLocal引用关系图- 图片来自于《简书 - 对ThreadLocal实现原理的一点思考》

上述解释主要参考自:ThreadLocal和synchronized的区别?

3.

问:您能说说ThreadLocal常用操作的底层实现原理吗?如存储set(T value),获取get(),删除remove()等操作。

答:

  • 调用get()操作获取ThreadLocal中对应当前线程存储的值时,进行了如下操作:

1 ) 获取当前线程Thread对象,进而获取此线程对象中维护的ThreadLocalMap对象。

2 ) 判断当前的ThreadLocalMap是否存在:

  • 如果存在,则以当前的ThreadLocalkey,调用ThreadLocalMap中的getEntry方法获取对应的存储实体 e。找到对应的存储实体 e,获取存储实体 e 对应的 value值,即为我们想要的当前线程对应此ThreadLocal的值,返回结果值。
  • 如果不存在,则证明此线程没有维护的ThreadLocalMap对象,调用setInitialValue方法进行初始化。返回setInitialValue初始化的值。
  • setInitialValue方法的操作如下:

1 ) 调用initialValue获取初始化的值。

2 ) 获取当前线程Thread对象,进而获取此线程对象中维护的ThreadLocalMap对象。

3 ) 判断当前的ThreadLocalMap是否存在:

  • 如果存在,则调用map.set设置此实体entry
  • 如果不存在,则调用createMap进行ThreadLocalMap对象的初始化,并将此实体entry作为第一个值存放至ThreadLocalMap中。

PS:关于ThreadLocalMap对应的相关操作,放在下一个问题详细说明。

示例代码:

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
复制代码    /**
* 返回当前线程对应的ThreadLocal的初始值
* 此方法的第一次调用发生在,当线程通过{@link #get}方法访问此线程的ThreadLocal值时
* 除非线程先调用了 {@link #set}方法,在这种情况下,
* {@code initialValue} 才不会被这个线程调用。
* 通常情况下,每个线程最多调用一次这个方法,
* 但也可能再次调用,发生在调用{@link #remove}方法后,
* 紧接着调用{@link #get}方法。
*
* <p>这个方法仅仅简单的返回null {@code null};
* 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
* 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
* 通常, 可以通过匿名内部类的方式实现
*
* @return 当前ThreadLocal的初始值
*/
protected T initialValue() {
return null;
}

/**
* 创建一个ThreadLocal
* @see #withInitial(java.util.function.Supplier)
*/
public ThreadLocal() {
}

/**
* 返回当前线程中保存ThreadLocal的值
* 如果当前线程没有此ThreadLocal变量,
* 则它会通过调用{@link #initialValue} 方法进行初始化值
*
* @return 返回当前线程对应此ThreadLocal的值
*/
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
ThreadLocalMap.Entry e = map.getEntry(this);
// 找到对应的存储实体 e
if (e != null) {
@SuppressWarnings("unchecked")
// 获取存储实体 e 对应的 value值
// 即为我们想要的当前线程对应此ThreadLocal的值
T result = (T)e.value;
return result;
}
}
// 如果map不存在,则证明此线程没有维护的ThreadLocalMap对象
// 调用setInitialValue进行初始化
return setInitialValue();
}

/**
* set的变样实现,用于初始化值initialValue,
* 用于代替防止用户重写set()方法
*
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
// 调用initialValue获取初始化的值
T value = initialValue();
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null)
// 存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将此实体entry作为第一个值存放至ThreadLocalMap中
createMap(t, value);
// 返回设置的值value
return value;
}

/**
* 获取当前线程Thread对应维护的ThreadLocalMap
*
* @param t the current thread 当前线程
* @return the map 对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
  • 调用set(T value)操作设置ThreadLocal中对应当前线程要存储的值时,进行了如下操作:

1 ) 获取当前线程Thread对象,进而获取此线程对象中维护的ThreadLocalMap对象。

2 ) 判断当前的ThreadLocalMap是否存在:

  • 如果存在,则调用map.set设置此实体entry
  • 如果不存在,则调用createMap进行ThreadLocalMap对象的初始化,并将此实体entry作为第一个值存放至ThreadLocalMap中。

示例代码:

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
复制代码    /**
* 设置当前线程对应的ThreadLocal的值
* 大多数子类都不需要重写此方法,
* 只需要重写 {@link #initialValue}方法代替设置当前线程对应的ThreadLocal的值
*
* @param value 将要保存在当前线程对应的ThreadLocal的值
*
*/
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null)
// 存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将此实体entry作为第一个值存放至ThreadLocalMap中
createMap(t, value);
}

/**
* 为当前线程Thread 创建对应维护的ThreadLocalMap.
*
* @param t the current thread 当前线程
* @param firstValue 第一个要存放的ThreadLocal变量值
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
  • 调用remove()操作删除ThreadLocal中对应当前线程已存储的值时,进行了如下操作:

1 ) 获取当前线程Thread对象,进而获取此线程对象中维护的ThreadLocalMap对象。

2 ) 判断当前的ThreadLocalMap是否存在, 如果存在,则调用map.remove,以当前ThreadLocalkey删除对应的实体entry

  • 示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    复制代码  /**
    * 删除当前线程中保存的ThreadLocal对应的实体entry
    * 如果此ThreadLocal变量在当前线程中调用 {@linkplain #get read}方法
    * 则会通过调用{@link #initialValue}进行再次初始化,
    * 除非此值value是通过当前线程内置调用 {@linkplain #set set}设置的
    * 这可能会导致在当前线程中多次调用{@code initialValue}方法
    *
    * @since 1.5
    */
    public void remove() {
    // 获取当前线程对象中维护的ThreadLocalMap对象
    ThreadLocalMap m = getMap(Thread.currentThread());
    // 如果此map存在
    if (m != null)
    // 存在则调用map.remove
    // 以当前ThreadLocal为key删除对应的实体entry
    m.remove(this);
    }

4.

问:对ThreadLocal的常用操作实际是对线程Thread中的ThreadLocalMap进行操作,核心是ThreadLocalMap这个哈希表,你能谈谈ThreadLocalMap的内部底层实现吗?

答:

  • ThreadLocalMap的底层实现是一个定制的自定义HashMap哈希表,核心组成元素有:

1 ) Entry[] table;:底层哈希表 table, 必要时需要进行扩容,底层哈希表 table.length 长度必须是2的n次方。

2 ) int size;:实际存储键值对元素个数 entries

3 ) int threshold;:下一次扩容时的阈值,阈值 threshold = 底层哈希表table的长度 len * 2 / 3。当size >= threshold时,遍历table并删除keynull的元素,如果删除后size >= threshold*3/4时,需要对table进行扩容(详情请查看set(ThreadLocal<?> key, Object value)方法说明)。

  • 其中Entry[] table;哈希表存储的核心元素是EntryEntry包含:

1 ) ThreadLocal<?> k;:当前存储的ThreadLocal实例对象

2 ) Object value;:当前 ThreadLocal 对应储存的值value

  • 需要注意的是,此Entry继承了弱引用 WeakReference,所以在使用ThreadLocalMap时,发现key == null,则意味着此key ThreadLocal不在被引用,需要将其从ThreadLocalMap哈希表中移除。(弱引用相关问题解释请查看 问答 5)

示例代码:

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
复制代码    /**
* ThreadLocalMap 是一个定制的自定义 hashMap 哈希表,只适合用于维护
* 线程对应ThreadLocal的值. 此类的方法没有在ThreadLocal 类外部暴露,
* 此类是私有的,允许在 Thread 类中以字段的形式声明 ,
* 以助于处理存储量大,生命周期长的使用用途,
* 此类定制的哈希表实体键值对使用弱引用WeakReferences 作为key,
* 但是, 一旦引用不在被使用,
* 只有当哈希表中的空间被耗尽时,对应不再使用的键值对实体才会确保被 移除回收。
*/
static class ThreadLocalMap {

/**
* 实体entries在此hash map中是继承弱引用 WeakReference,
* 使用ThreadLocal 作为 key 键. 请注意,当key为null(i.e. entry.get()
* == null) 意味着此key不再被引用,此时实体entry 会从哈希表中删除。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** 当前 ThreadLocal 对应储存的值value. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

/**
* 初始容量大小 16 -- 必须是2的n次方.
*/
private static final int INITIAL_CAPACITY = 16;

/**
* 底层哈希表 table, 必要时需要进行扩容.
* 底层哈希表 table.length 长度必须是2的n次方.
*/
private Entry[] table;

/**
* 实际存储键值对元素个数 entries.
*/
private int size = 0;

/**
* 下一次扩容时的阈值
*/
private int threshold; // 默认为 0

/**
* 设置触发扩容时的阈值 threshold
* 阈值 threshold = 底层哈希表table的长度 len * 2 / 3
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}

/**
* 获取该位置i对应的下一个位置index
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}

/**
* 获取该位置i对应的上一个位置index
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}

}
  • ThreadLocalMap的构造方法是延迟加载的,也就是说,只有当线程需要存储对应的ThreadLocal的值时,才初始化创建一次(仅初始化一次)。初始化步骤如下:

1) 初始化底层数组table的初始容量为 16。

2) 获取ThreadLocal中的threadLocalHashCode,通过threadLocalHashCode & (INITIAL_CAPACITY - 1),即ThreadLocal 的 hash 值 threadLocalHashCode % 哈希表的长度 length 的方式计算该实体的存储位置。

3) 存储当前的实体,key 为 : 当前ThreadLocal value:真正要存储的值

4)设置当前实际存储元素个数 size 为 1

5)设置阈值setThreshold(INITIAL_CAPACITY),为初始化容量 16 的 2/3。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码
/**
* 用于创建一个新的hash map包含 (firstKey, firstValue).
* ThreadLocalMaps 构造方法是延迟加载的,所以我们只会在至少有一个
* 实体entry存放时,才初始化创建一次(仅初始化一次)。
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化 table 初始容量为 16
table = new Entry[INITIAL_CAPACITY];
// 计算当前entry的存储位置
// 存储位置计算等价于:
// ThreadLocal 的 hash 值 threadLocalHashCode % 哈希表的长度 length
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 存储当前的实体,key 为 : 当前ThreadLocal value:真正要存储的值
table[i] = new Entry(firstKey, firstValue);
// 设置当前实际存储元素个数 size 为 1
size = 1;
// 设置阈值,为初始化容量 16 的 2/3。
setThreshold(INITIAL_CAPACITY);
}
  • ThreadLocalget()操作实际是调用ThreadLocalMapgetEntry(ThreadLocal<?> key)方法,此方法快速适用于获取某一存在key的实体 entry,否则,应该调用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法获取,这样做是为了最大限制地提高直接命中的性能,该方法进行了如下操作:

1 ) 计算要获取的entry的存储位置,存储位置计算等价于:ThreadLocalhashthreadLocalHashCode % 哈希表的长度 length

2 ) 根据计算的存储位置,获取到对应的实体 Entry。判断对应实体Entry是否存在 并且 key是否相等:

  • 存在对应实体Entry并且对应key相等,即同一ThreadLocal,返回对应的实体Entry
  • 不存在对应实体Entry 或者 key不相等,则通过调用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法继续查找。
  • getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法操作如下:

1 ) 获取底层哈希表数组table,循环遍历对应要查找的实体Entry所关联的位置。

2 ) 获取当前遍历的entrykey ThreadLocal,比较key是否一致,一致则返回。

3 ) 如果key不一致 并且 keynull,则证明引用已经不存在,这是因为Entry继承的是WeakReference,这是弱引用带来的坑。调用expungeStaleEntry(int staleSlot)方法删除过期的实体Entry(此方法不单独解释,请查看示例代码,有详细注释说明)。

4 ) key不一致 ,key也不为空,则遍历下一个位置,继续查找。

5 ) 遍历完毕,仍然找不到则返回null

示例代码:

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
复制代码
/**
* 根据key 获取对应的实体 entry. 此方法快速适用于获取某一存在key的
* 实体 entry,否则,应该调用getEntryAfterMiss方法获取,这样做是为
* 了最大限制地提高直接命中的性能
*
* @param key 当前thread local 对象
* @return the entry 对应key的 实体entry, 如果不存在,则返回null
*/
private Entry getEntry(ThreadLocal<?> key) {
// 计算要获取的entry的存储位置
// 存储位置计算等价于:
// ThreadLocal 的 hash 值 threadLocalHashCode % 哈希表
的长度 length
int i = key.threadLocalHashCode & (table.length - 1);
// 获取到对应的实体 Entry
Entry e = table[i];
// 存在对应实体并且对应key相等,即同一ThreadLocal
if (e != null && e.get() == key)
// 返回对应的实体Entry
return e;
else
// 不存在 或 key不一致,则通过调用getEntryAfterMiss继续查找
return getEntryAfterMiss(key, i, e);
}

/**
* 当根据key找不到对应的实体entry 时,调用此方法。
* 直接定位到对应的哈希表位置
*
* @param key 当前thread local 对象
* @param i 此对象在哈希表 table中的存储位置 index
* @param e the entry 实体对象
* @return the entry 对应key的 实体entry, 如果不存在,则返回null
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 循环遍历当前位置的所有实体entry
while (e != null) {
// 获取当前entry 的 key ThreadLocal
ThreadLocal<?> k = e.get();
// 比较key是否一致,一致则返回
if (k == key)
return e;
// 找到对应的entry ,但其key 为 null,则证明引用已经不存在
// 这是因为Entry继承的是WeakReference,这是弱引用带来的坑
if (k == null)
// 删除过期(stale)的entry
expungeStaleEntry(i);
else
// key不一致 ,key也不为空,则遍历下一个位置,继续查找
i = nextIndex(i, len);
// 获取下一个位置的实体 entry
e = tab[i];
}
// 遍历完毕,找不到则返回null
return null;
}


/**
* 删除对应位置的过期实体,并删除此位置后对应相关联位置key = null的实体
*
* @param staleSlot 已知的key = null 的对应的位置索引
* @return 对应过期实体位置索引的下一个key = null的位置
* (所有的对应位置都会被检查)
*/
private int expungeStaleEntry(int staleSlot) {
// 获取对应的底层哈希表 table
Entry[] tab = table;
// 获取哈希表长度
int len = tab.length;

// 擦除这个位置上的脏数据
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

// 直到我们找到 Entry e = null,才执行rehash操作
// 就是遍历完该位置的所有关联位置的实体
Entry e;
int i;
// 查找该位置对应所有关联位置的过期实体,进行擦除操作
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;

// 我们必须一直遍历直到最后
// 因为还可能存在多个过期的实体
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

/**
* 删除所有过期的实体
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
  • ThreadLocalset(T value)操作实际是调用ThreadLocalMapset(ThreadLocal<?> key, Object value)方法,该方法进行了如下操作:

1 ) 获取对应的底层哈希表table,计算对应threalocal的存储位置。

2 ) 循环遍历table对应该位置的实体,查找对应的threadLocal

3 ) 获取当前位置的threadLocal,如果key threadLocal一致,则证明找到对应的threadLocal,将新值赋值给找到的当前实体Entryvalue中,结束。

4 ) 如果当前位置的key threadLocal不一致,并且key threadLocalnull,则调用replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot)方法(此方法不单独解释,请查看示例代码,有详细注释说明),替换该位置key == null 的实体为当前要设置的实体,结束。

5 ) 如果当前位置的key threadLocal不一致,并且key threadLocal不为null,则创建新的实体,并存放至当前位置 i tab[i] = new Entry(key, value);,实际存储键值对元素个数size + 1,由于弱引用带来了这个问题,所以要调用cleanSomeSlots(int i, int n)方法清除无用数据(此方法不单独解释,请查看示例代码,有详细注释说明),才能判断现在的size有没有达到阀值threshhold,如果没有要清除的数据,存储元素个数仍然 大于 阈值 则调用rehash方法进行扩容(此方法不单独解释,请查看示例代码,有详细注释说明)。

示例代码:

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
复制代码
/**
* 设置对应ThreadLocal的值
*
* @param key 当前thread local 对象
* @param value 要设置的值
*/
private void set(ThreadLocal<?> key, Object value) {

// 我们不会像get()方法那样使用快速设置的方式,
// 因为通常很少使用set()方法去创建新的实体
// 相对于替换一个已经存在的实体, 在这种情况下,
// 快速设置方案会经常失败。

// 获取对应的底层哈希表 table
Entry[] tab = table;
// 获取哈希表长度
int len = tab.length;
// 计算对应threalocal的存储位置
int i = key.threadLocalHashCode & (len-1);

// 循环遍历table对应该位置的实体,查找对应的threadLocal
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
// 获取当前位置的ThreadLocal
ThreadLocal<?> k = e.get();
// 如果key threadLocal一致,则证明找到对应的threadLocal
if (k == key) {
// 赋予新值
e.value = value;
// 结束
return;
}
// 如果当前位置的key threadLocal为null
if (k == null) {
// 替换该位置key == null 的实体为当前要设置的实体
replaceStaleEntry(key, value, i);
// 结束
return;
}
}
// 当前位置的k != key && k != null
// 创建新的实体,并存放至当前位置i
tab[i] = new Entry(key, value);
// 实际存储键值对元素个数 + 1
int sz = ++size;
// 由于弱引用带来了这个问题,所以先要清除无用数据,才能判断现在的size有没有达到阀值threshhold
// 如果没有要清除的数据,存储元素个数仍然 大于 阈值 则扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 扩容
rehash();
}

/**
* 当执行set操作时,获取对应的key threadLocal,并替换过期的实体
* 将这个value值存储在对应key threadLocal的实体中,无论是否已经存在体
* 对应的key threadLocal
*
* 有一个副作用, 此方法会删除该位置下和该位置nextIndex对应的所有过期的实体
*
* @param key 当前thread local 对象
* @param value 当前thread local 对象对应存储的值
* @param staleSlot 第一次找到此过期的实体对应的位置索引index
* .
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
// 获取对应的底层哈希表 table
Entry[] tab = table;
// 获取哈希表长度
int len = tab.length;
Entry e;

// 往前找,找到table中第一个过期的实体的下标
// 清理整个table是为了避免因为垃圾回收带来的连续增长哈希的危险
// 也就是说,哈希表没有清理干净,当GC到来的时候,后果很严重

// 记录要清除的位置的起始首位置
int slotToExpunge = staleSlot;
// 从该位置开始,往前遍历查找第一个过期的实体的下标
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;

// 找到key一致的ThreadLocal或找到一个key为 null的
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();

// 如果我们找到了key,那么我们就需要把它跟新的过期数据交换来保持哈希表的顺序
// 那么剩下的过期Entry呢,就可以交给expungeStaleEntry方法来擦除掉
// 将新设置的实体放置在此过期的实体的位置上
if (k == key) {
// 替换,将要设置的值放在此过期的实体中
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;

// 如果存在,则开始清除之前过期的实体
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 在这里开始清除过期数据
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}

// / 如果我们没有在往后查找中找没有找到过期的实体,
// 那么slotToExpunge就是第一个过期Entry的下标了
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}

// 最后key仍没有找到,则将要设置的新实体放置
// 在原过期的实体对应的位置上。
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);

// 如果该位置对应的其他关联位置存在过期实体,则清除
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}


/**
* 启发式的扫描查找一些过期的实体并清除,
* 此方法会再添加新实体的时候被调用,
* 或者过期的元素被清除时也会被调用.
* 如果实在没有过期数据,那么这个算法的时间复杂度就是O(log n)
* 如果有过期数据,那么这个算法的时间复杂度就是O(n)
*
* @param i 一个确定不是过期的实体的位置,从这个位置i开始扫描
*
* @param n 扫描控制: 有{@code log2(n)} 单元会被扫描,
* 除非找到了过期的实体, 在这种情况下
* 有{@code log2(table.length)-1} 的格外单元会被扫描.
* 当调用插入时, 这个参数的值是存储实体的个数,
* 但如果调用 replaceStaleEntry方法, 这个值是哈希表table的长度
* (注意: 所有的这些都可能或多或少的影响n的权重
* 但是这个版本简单,快速,而且似乎执行效率还可以)
*
* @return true 返回true,如果有任何过期的实体被删除。
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}


/**
* 哈希表扩容方法
* 首先扫描整个哈希表table,删除过期的实体
* 缩小哈希表table大小 或 扩大哈希表table大小,扩大的容量是加倍.
*/
private void rehash() {
// 删除所有过期的实体
expungeStaleEntries();

// 使用较低的阈值threshold加倍以避免滞后
// 存储实体个数 大于等于 阈值的3/4则扩容
if (size >= threshold - threshold / 4)
resize();
}

/**
* 扩容方法,以2倍的大小进行扩容
* 扩容的思想跟HashMap很相似,都是把容量扩大两倍
* 不同之处还是因为WeakReference带来的
*/
private void resize() {
// 记录旧的哈希表
Entry[] oldTab = table;
// 记录旧的哈希表长度
int oldLen = oldTab.length;
// 新的哈希表长度为旧的哈希表长度的2倍
int newLen = oldLen * 2;
// 创建新的哈希表
Entry[] newTab = new Entry[newLen];
int count = 0;
// 逐一遍历旧的哈希表table的每个实体,重新分配至新的哈希表中
for (int j = 0; j < oldLen; ++j) {
// 获取对应位置的实体
Entry e = oldTab[j];
// 如果实体不会null
if (e != null) {
// 获取实体对应的ThreadLocal
ThreadLocal<?> k = e.get();
// 如果该ThreadLocal 为 null
if (k == null) {
// 则对应的值也要清除
// 就算是扩容,也不能忘了为擦除过期数据做准备
e.value = null; // Help the GC
} else {
// 如果不是过期实体,则根据新的长度重新计算存储位置
int h = k.threadLocalHashCode & (newLen - 1);
// 将该实体存储在对应ThreadLocal的最后一个位置
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
// 重新分配位置完毕,则重新计算阈值Threshold
setThreshold(newLen);
// 记录实际存储元素个数
size = count;
// 将新的哈希表赋值至底层table
table = newTab;
}
  • ThreadLocalremove()操作实际是调用ThreadLocalMapremove(ThreadLocal<?> key)方法,该方法进行了如下操作:

1 ) 获取对应的底层哈希表 table,计算对应threalocal的存储位置。

2 ) 循环遍历table对应该位置的实体,查找对应的threadLocal

3 ) 获取当前位置的threadLocal,如果key threadLocal一致,则证明找到对应的threadLocal,执行删除操作,删除此位置的实体,结束。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码
/**
* 移除对应ThreadLocal的实体
*/
private void remove(ThreadLocal<?> key) {
// 获取对应的底层哈希表 table
Entry[] tab = table;
// 获取哈希表长度
int len = tab.length;
// 计算对应threalocal的存储位置
int i = key.threadLocalHashCode & (len-1);
// 循环遍历table对应该位置的实体,查找对应的threadLocal
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
// 如果key threadLocal一致,则证明找到对应的threadLocal
if (e.get() == key) {
// 执行清除操作
e.clear();
// 清除此位置的实体
expungeStaleEntry(i);
// 结束
return;
}
}
}

5.

问:ThreadLocalMap中的存储实体Entry使用ThreadLocal作为key,但这个Entry是继承弱引用WeakReference的,为什么要这样设计,使用了弱引用WeakReference会造成内存泄露问题吗?

答:

  • 首先,回答这个问题之前,我需要解释一下什么是强引用,什么是弱引用。

我们在正常情况下,普遍使用的是强引用:

1
2
3
复制代码A a = new A();

B b = new B();

a = null;b = null;时,一段时间后,JAVA垃圾回收机制GC会将 a 和 b 对应所分配的内存空间给回收。

但考虑这样一种情况:

1
2
复制代码C c = new C(b);
b = null;

当 b 被设置成null时,那么是否意味这一段时间后GC工作可以回收 b 所分配的内存空间呢?答案是否定的,因为即使 b 被设置成null,但 c 仍然持有对 b 的引用,而且还是强引用,所以GC不会回收 b 原先所分配的空间,既不能回收,又不能使用,这就造成了 内存泄露。

那么如何处理呢?

可以通过c = null;,也可以使用弱引用WeakReference w = new WeakReference(b);。因为使用了弱引用WeakReference,GC是可以回收 b 原先所分配的空间的。

上述解释主要参考自:对ThreadLocal实现原理的一点思考

  • 回到ThreadLocal的层面上,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现keynullEntry,就没有办法访问这些keynullEntryvalue,如果当前线程再迟迟不结束的话,这些keynullEntryvalue就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永远无法回收,造成内存泄漏。

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocalget(),set(),remove()的时候都会清除线程ThreadLocalMap里所有keynullvalue

但是这些被动的预防措施并不能保证不会内存泄漏:

  • 使用staticThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏(参考ThreadLocal 内存泄露的实例分析)。
  • 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?

我们先来看看官方文档的说法:

1
2
复制代码To help deal with very large and long-lived usages, 
the hash table entries use WeakReferences for keys.

为了应对非常大和长时间的用途,哈希表使用弱引用的 key

下面我们分两种情况讨论:

  • key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  • key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用get(),set(),remove()的时候会被清除。
  • 比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用get(),set(),remove()的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?

每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

上述解释主要参考自:深入分析 ThreadLocal 内存泄漏问题

6.

问:ThreadLocalsynchronized的区别?

答:ThreadLocalsynchronized关键字都用于处理多线程并发访问变量的问题,只是二者处理问题的角度和思路不同。

  1. ThreadLocal是一个Java类,通过对当前线程中的局部变量的操作来解决不同线程的变量访问的冲突问题。所以,ThreadLocal提供了线程安全的共享对象机制,每个线程都拥有其副本。
  2. Java中的synchronized是一个保留字,它依靠JVM的锁机制来实现临界区的函数或者变量的访问中的原子性。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。此时,被用作“锁机制”的变量时多个线程共享的。
  • 同步机制(synchronized关键字)采用了以“时间换空间”的方式,提供一份变量,让不同的线程排队访问。而ThreadLocal采用了“以空间换时间”的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响。

7.

问:ThreadLocal在现时有什么应用场景?

答:总的来说ThreadLocal主要是解决2种类型的问题:

  • 解决并发问题:使用ThreadLocal代替synchronized来保证线程安全。同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
  • 解决数据存储问题:ThreadLocal为变量在每个线程中都创建了一个副本,所以每个线程可以访问自己内部的副本变量,不同线程之间不会互相干扰。如一个Parameter对象的数据需要在多个模块中使用,如果采用参数传递的方式,显然会增加模块之间的耦合性。此时我们可以使用ThreadLocal解决。

应用场景:

Spring使用ThreadLocal解决线程安全问题

  • 我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolderTransactionSynchronizationManagerLocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。
  • 一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public abstract class RequestContextHolder  {
····

private static final boolean jsfPresent =
ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());

private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<RequestAttributes>("Request attributes");

private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<RequestAttributes>("Request context");

·····
}

总结

  1. ThreadLocal提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程。
  2. ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap哈希表,这个哈希表的keyThreadLocal实例本身,value才是真正要存储的值Object
  3. ThreadLocal的常用操作实际是对线程Thread中的ThreadLocalMap进行操作。
  4. ThreadLocalMap的底层实现是一个定制的自定义HashMap哈希表,ThreadLocalMap的阈值threshold = 底层哈希表table的长度 len * 2 / 3,当实际存储元素个数size 大于或等于 阈值threshold3/4size >= threshold*3/4,则对底层哈希表数组table进行扩容操作。
  5. ThreadLocalMap中的哈希表Entry[] table存储的核心元素是Entry,存储的keyThreadLocal实例对象,valueThreadLocal 对应储存的值value。需要注意的是,此Entry继承了弱引用 WeakReference,所以在使用ThreadLocalMap时,发现key == null,则意味着此key ThreadLocal不在被引用,需要将其从ThreadLocalMap哈希表中移除。
  6. ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收。所以,在ThreadLocalget(),set(),remove()的时候都会清除线程ThreadLocalMap里所有keynullvalue。如果我们不主动调用上述操作,则会导致内存泄露。
  7. 为了安全地使用ThreadLocal,必须要像每次使用完锁就解锁一样,在每次使用完ThreadLocal后都要调用remove()来清理无用的Entry。这在操作在使用线程池时尤为重要。
  8. ThreadLocalsynchronized的区别:同步机制(synchronized关键字)采用了以“时间换空间”的方式,提供一份变量,让不同的线程排队访问。而ThreadLocal采用了“以空间换时间”的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响。
  9. ThreadLocal主要是解决2种类型的问题:A. 解决并发问题:使用ThreadLocal代替同步机制解决并发问题。B. 解决数据存储问题:如一个Parameter对象的数据需要在多个模块中使用,如果采用参数传递的方式,显然会增加模块之间的耦合性。此时我们可以使用ThreadLocal解决。

参考文章

深入浅出ThreadLocal
ThreadLocal和synchronized的区别?
深入剖析ThreadLocal
ThreadLocal内部机制
聊一聊Spring中的线程安全性
对ThreadLocal实现原理的一点思考
深入分析 ThreadLocal 内存泄漏问题
学习Spring必学的Java基础知识(6)—-ThreadLocal
ThreadLocal设计模式
ThreadLocal案例分析
Spring单例模式与线程安全ThreadLocal

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%