Java并发编程之ThreadLocal的使用

ThreadLocal 介绍

java.lang.ThreadLocal 是Java提供用来保存线程变量的机制,是对象为键,任意类型为值的存储结构。那么何为线程变量呢,当你使用ThreadLocalset函数将一个值与这个线程绑定的时候,在当前线程下使用get方法可以获取到之前绑定的值。其他线程的set操作也只对其他当前线程‘有效’。

ThreadLocal 使用

与synchronized等诸多锁实现变量在多线程的正确性不同,锁机制讲究将共享变量在多线程中进行正确的同步,而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
//初始化ThreadLocal的值
protected T initialValue() {
return null;
}
//创建一个ThreadLocal对象并初始化值
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
//设置一个线程变量
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前的threadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//如果map已经初始化,则设置值
map.set(this, value);
else
//初始化map并设置值
createMap(t, value);
}
//获取线程变量
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//如果map不为空,则直接获取值
if (map != null) {
//获取ThreadLocalMap 的Entry 存储。
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果map为空,则直接设置初始化值并返回这个初始化值。
return setInitialValue();
}
//将线程变量从当前线程移除
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//将线程变量从当前变线程移除
m.remove(this);
}

由于篇幅原因,我们就不看所有代码实现,只看核心方法的大致内容,有兴趣的读者可以去阅读源码。我们一般讲ThreadLocal对象声明为静态变量。initialValue方法可以在初始化的时候指定初始化值,而静态方法withInitial可以初始化值并返回一个已经初始化的ThreadLocal对象。set,get方法很明显是为当前线程绑定线程变量,和获取当前线程绑定的线程变量。而remove方法是将线程变量从当前线程中移除。这些方法无论看起来还是使用起来都相当简单。

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
 /**
* ThreadLocal测试类
* <p>@Author Kam</p>
* <p>@Date 2018/4/26</p>
* <p>@Version</p>
*/
public class ThreadLocalTest {
private static ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return "init";
}
};
@Test
public void testThreadLocal() throws InterruptedException {
String value = threadLocal.get();
//第一次获取的是初始值
System.out.println("value:"+value);// ==>"init"
threadLocal.set("main value");
value = threadLocal.get();
System.out.println("value:"+value);// ==>"main value"

ValueThread valueThread = new ValueThread("valueThread");
valueThread.start();
valueThread.join();
}
class ValueThread extends Thread{
public ValueThread(String name){
super(name);
}
@Override
public void run() {
System.out.println("我的线程名称是:"+this.getName());
System.out.println("我的初始值是:"+threadLocal.get());//==>"init"
threadLocal.set(this.getName());
System.out.println("我现在的值是:"+threadLocal.get());//==>valueThread
}
}
}

从以上示例代码我们可以看出,当我们利用构建函数初始化一个ThreadLocal对象,并赋予初始化值。那么无论是哪个线程在没有set新值得情况下,第一次获取到的都是初始化值。这个应该很好理解。而当Test主线程set了新的值,对于示例中的ValueThread来说仍然是不可见的。这就是ThreadLocal的奥妙之处。同一个ThreadLocal对象对于每个线程来说都拥有自己的线程变量。使得当前线程对这个变量的使用不会影响其他线程。

ThreadLocal 的原理。

ThreadLocal 究竟是怎么实现这种线程变量机制的呢,其实关键在于ThreadLocal的存储:ThreadLocalMap。相信有翻过源码的朋友都可以看见,ThreadLocal内部实现了静态类 ThreadLocalMap。而ThreadLocalMap 的存储结构Entry继承了弱引用类 WeakReference ,同时Entry的 Object 类型的value存储的真正的值。

1
2
3
4
5
6
7
8
9
10
11
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;
}
}
}

而每个Thread 都有一个ThreadLocalMap 对象。

1
2
3
class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}

现在大家明白了吧。当set一个值得时候,其实就是获取当前线程的ThreadLocalMap 引用,然后赋值。当然get操作也是如此。


ThreadLocal 存在的问题

因为Entry 是继承弱引用类型,而弱引用类型会在GC的时候递归引用进行回收。而如果线程在使用完之后不可达。Entry 的弱引用key将被回收,而value因为是强引用类型所以会导致很多 key为 null的value,没办法访问这些key为null的ThrealLocal。虽然,ThreadLocal在每次进行get,set,remove方法的时候会进行nullkey的清除,但这还是无法避免内存泄漏的根本问题。所以在此提供良好使用ThreadLocal的两条建议。
1)尽量将ThreadLocal 声明为static.延迟生命周期。
2)每次在线程使用完之后,进行remove操作。

如果看的爽,不如请我吃根辣条?