死磕Java之聊聊ThreadLocal源码(基于JDK1.8)

代码星冰乐

专注成就未来

首页 归档 关于

死磕Java之聊聊ThreadLocal源码(基于JDK1.8)

May 9, 2018 | haifeiWu | Java | 阅读
文章目录
  1. 1. 聊聊JDK源码中ThreadLocal的实现
  2. 2. ThreadLocal主要的代码实现
  3. 3. ThreadLocal 使用demo
  4. 4. 小结
  5. 5. 参考文章

作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2018/1766/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。


由于版权原因,请阅读原文 --> 死磕Java之聊聊ThreadLocal源码(基于JDK1.8)

关注我们

作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2018/1766/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。

作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2018/1766/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。

记得在一次面试中被问到ThreadLocal,答得马马虎虎,所以打算研究一下ThreadLocal的源码

面试官 : 用过ThreadLocal吗?

楼主答 : 用过,当时使用ThreadLocal的时候,使用Spring实现横切整个Controller层,使用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

public class ProfilerAdvice {

Logger logger = Logger.getLogger(ProfilerAdvice.class);

private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>(){
@Override
protected Long initialValue() {
return System.currentTimeMillis();
}
};

public static final void begin() {
TIME_THREADLOCAL.set(System.currentTimeMillis());
}

public static final Long end() {
return System.currentTimeMillis() - TIME_THREADLOCAL.get();
}

public void requestStart() {
ProfilerAdvice.begin();
}

public void requestEnd(JoinPoint joinPoint) {
/**
* 获取被拦截的方法.
*/
String methodName = joinPoint.getSignature().getName();
logger.info(methodName + "方法请求耗时 "+ProfilerAdvice.end()/1000.0+"s");

}
}

面试官 : 那ThreadLocal对应的数据结构是咋样的啊?

楼主答 :应该是使用哈希表实现的吧,楼主这个时候心里开始没底了,然后就没有然后……

聊聊JDK源码中ThreadLocal的实现

主要方法:

  • ThreadLocal的get方法
    ThreadLocal之get流程:
    1、获取当前线程t;
    2、返回当前线程t的成员变量ThreadLocalMap(以下简写map);
    3、map不为null,则获取以当前线程为key的ThreadLocalMap的Entry(以下简写e),如果e不为null,则直接返回该Entry的value;
    4、如果map为null或者e为null,返回setInitialValue()的值。setInitialValue()调用重写的initialValue()返回新值(如果没有重写initialValue将返回默认值null),并将新值存入当前线程的ThreadLocalMap(如果当前线程没有ThreadLocalMap,会先创建一个)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
  • ThreadLocal的set方法
    ThreadLocal之set流程:
    1、获取当前线程t;
    2、返回当前线程t的成员变量ThreadLocalMap(以下简写map);
    3、map不为null,则更新以当前线程为key的ThreadLocalMap,否则创建一个ThreadLocalMap,其中当前线程t为key;
1
2
3
4
5
6
7
8
9

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

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

public class ThreadLocal<T> {
/**
* ThreadLocals依赖于附加的每线程线性探测哈希映射到每个线程(Thread.threadLocals和
* inheritableThreadLocals)。 ThreadLocal对象充当键,通过threadLocalHashCode搜索。 这是一个自定义哈希码
* (仅在ThreadLocalMaps内有用),可以消除哈希冲突
* 在连续构造ThreadLocals的常见情况下
* 由相同的线程使用,同时保持良好的行为和异常情况的发生。
*/
private final int threadLocalHashCode = nextHashCode();

/**
* 下一个哈希码将被发出,原子更新,从零开始。
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();

/**
* 连续生成的散列码之间的区别 , 将隐式顺序线程本地ID转换为近乎最佳的散布
* 两倍大小的表的乘法散列值。
*/
private static final int HASH_INCREMENT = 0x61c88647;

/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}

/**
* 返回此线程局部变量的当前线程的“初始值”。 在线程首次访问带有{@link #get}方法的变量时,将调用此方法,
* 除非线程先前调用了{@link #set}方法,在这种情况下,initialValue方法不会 为该线程调用。
* 通常,每个线程最多调用一次此方法,但在后续调用{@link #remove}后跟{@link #get}时可能会再次调用此方法。
* <p>这个实现只是返回{@code null}; 如果程序员希望线程局部变量的初始值不是{@code null},
* 则必须对子代码{@CodeLocal}进行子类化,并重写此方法。 通常,将使用匿名内部类。
*/
protected T initialValue() {
return null;
}

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

/**
* SuppliedThreadLocal是JDK8新增的内部类,只是扩展了ThreadLocal的初始化值的方法而已
* ,允许使用JDK8新增的Lambda表达式赋值。需要注意的是,函数式接口Supplier不允许为null。
*/
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

private final Supplier<? extends T> supplier;

SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}

@Override
protected T initialValue() {
return supplier.get();
}
}

/**
* ThreadLocalMap是定制的hashMap,关于HashMap的更详细的问题请参看《聊聊HashMap源码》,
* 仅用于维护当前线程的本地变量值。仅ThreadLocal类对其有操作权限,
* 是Thread的私有属性。为避免占用空间较大或生命周期较长的数据常驻于内存引发一系列问题,
* hash table的key是弱引用WeakReferences。当空间不足时,会清理未被引用的entry。
*/
static class ThreadLocalMap {

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

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

/**
* 构造一个新的包含初始映射,ThreadLocal映射的新映射,因此我们只在创建至少一个条目时创建一个。
*/
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);
}

/**
* 从给定的parentMap构造一个包含所有map的新ThreadLocal。仅由createInheritedMap调用。
*
* @param parentMap the map associated with parent thread.
*/
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];

for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
}
}

ThreadLocal 使用demo

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

public class ThreadLocalExample {


public static class MyRunnable implements Runnable {

private ThreadLocal<Integer> threadLocal =
new ThreadLocal<Integer>();

@Override
public void run() {
threadLocal.set( (int) (Math.random() * 100D) );

try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}

System.out.println(threadLocal.get());
}
}


public static void main(String[] args) {
MyRunnable sharedRunnableInstance = new MyRunnable();

Thread thread1 = new Thread(sharedRunnableInstance);
Thread thread2 = new Thread(sharedRunnableInstance);

thread1.start();
thread2.start();

thread1.join(); //wait for thread 1 to terminate
thread2.join(); //wait for thread 2 to terminate
}

}

本示例创建一个传递给两个不同线程的MyRunnable实例。 两个线程都执行run()方法,从而在ThreadLocal实例上设置不同的值。 如果对set()调用的访问已经同步,并且它不是ThreadLocal对象,则第二个线程将覆盖第一个线程设置的值。
但是,由于它是一个ThreadLocal对象,因此两个线程无法看到对方的值。 因此,他们设定并获得不同的价值观。

小结

  • ThreadLocal特性及使用场景

1、实现单个线程单例以及单个线程上下文信息存储,比如交易id等

2、实现线程安全,非线程安全的对象使用ThreadLocal之后就会变得线程安全,因为每个线程都会有一个对应的实例

3、承载一些线程相关的数据,避免在方法中来回传递参数

  • ThreadLocal使用过程中出现的问题

1、ThreadLocal并未解决多线程访问共享对象的问题,如果ThreadLocal.set()的对象是多线程共享的,那么还是涉及并发问题;

2、会导致内存泄露么?
有人认为ThreadLocal会导致内存泄露,原因如下

首先ThreadLocal实例被线程的ThreadLocalMap实例持有,也可以看成被线程持有。
如果应用使用了线程池,那么之前的线程实例处理完之后出于复用的目的依然存活
所以,ThreadLocal设定的值被持有,导致内存泄露。

上面的逻辑是清晰的,然而Java的设计者早已经想到了这个问题,ThreadLocal并不会产生内存泄露,因为ThreadLocalMap在选择key的时候,
并不是直接选择ThreadLocal实例,而是ThreadLocal实例的弱引用。所以实际上从ThreadLocal设计角度来说是不会导致内存泄露的,具体代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
static class ThreadLocalMap { 

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

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

3、然而ThreadLocal真的不会导致内存泄漏吗?上面2说到的不会导致内存泄漏,是说java的设计者也想到了这一点,也不一定保证ThreadLocal一定不会出现内存泄漏的问题,如下图所示

ThreadLocal内存泄漏

每个thread中都存在一个map,map的类型是ThreadLocal.ThreadLocalMap,Map中的key为一个threadlocal实例。这个Map的确使用了弱引用,不过弱引用只是针对key。每个key都弱引用指向threadlocal。当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用。只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current Thread,Map,value将全部被GC回收。
所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为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
public class ThreadPoolProblem {
static ThreadLocal<AtomicInteger> sequencer = new ThreadLocal<AtomicInteger>() {

@Override
protected AtomicInteger initialValue() {
return new AtomicInteger(0);
}
};

static class Task implements Runnable {

@Override
public void run() {
AtomicInteger s = sequencer.get();
int initial = s.getAndIncrement();
// 期望初始为0
System.out.println(initial);
}
}

public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new Task());
executor.execute(new Task());
executor.execute(new Task());
executor.shutdown();
}
}

对于异步任务Task而言,它期望的初始值应该总是0,但运行程序,结果却为:

1
2
3
0
0
1

第三次执行异步任务,结果就不对了,为什么呢?因为线程池中的线程在执行完一个任务,执行下一个任务时,其中的ThreadLocal对象并不会被清空,修改后的值带到了下一个异步任务。那怎么办呢?有几种思路:

  1. 第一次使用ThreadLocal对象时,总是先调用set设置初始值,或者如果ThreaLocal重写了initialValue方法,先调用remove
  2. 使用完ThreadLocal对象后,总是调用其remove方法
  3. 使用自定义的线程池

参考文章

  • Java ThreadLocal
  • 理解Java中的ThreadLocal
  • Java编程的逻辑 (82) - 理解ThreadLocal
  • ThreadLocal可能引起的内存泄露

关注我们

作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2018/1766/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。

分享
Java源码解析
Kotlin:Android开发技巧死磕Java之聊聊LinkedList源码(基于JDK1.8)
微信关注我们
分类
  • Android8
  • Go4
  • Java59
  • Kafka,Java1
  • Kotlin2
  • Linux1
  • MapReduce1
  • Python2
  • Raft1
  • Redis1
  • ThreadPoolExecutor1
  • go1
  • 工具1
  • 总结8
  • 旅游日记1
标签
Nginx ChanghuiN haifeiWu Android Java 设计模式 hexo Kotlin 算法 MySQL 源码解析 Python Redis golang web Kafka 配置中心 总结 性能优化 旅游日记 Shell Go 问题排查 译文 Docker Spring Boot 工具 学习笔记 WebFlux 性能测试 go 散列表 源码 netty Raft
最近文章
  • Kafka的日志复制机制
  • 从20到21
  • go 并发编程
  • 【译】了解Linux CPU负载-您何时应该担心?
  • Zookeeper 与分布式锁
  • 基于Redis的分布式锁到底安全吗?
  • 【译】Raft 学生指南
  • ThreadPoolExecutor 的简单梳理
  • MapReduce 的简单实现
  • 使用 Map 实现策略模式
福利专区
    免费SSL证书
      阿里云红包
        腾讯云专属福利
        Copyright © 2021 代码星冰乐. Powered by ChanghuiN. 版权所有 晋ICP备15001365号
        特别感谢: 云服务器服务商 、 CDN 服务商