ThreadLocal随手记

ThreadLocal,也是面试中被问到的非常多的一个东西。可以简单理解为线程本地变量,什么意思呢?就是每个线程单独在本地维护的变量,或者说,多个线程间被隔离的开的、不共享的变量。
这个东西有什么用呢?我们在日常的工程代码中,经常会出现一些工具类是线程不安全的,例如我们常用的SimpleDateFormat。sdf为什么线程不安全在这里不多做表述,有兴趣的自己看一下源码,很容易发现它线程不安全的原因。
实际工作中,我们经常需要在接口代码对返回给前端或者其他地方的日期做格式化,sdf在jdk1.7之前几乎是一个绕不开的话题。如何解决这个线程不安全的问题呢?根据我们之前的学习,非常的简单,我们有两种思路:
1.加sync关键词,把格式化日期的代码用sync关键词做互斥同步,所有的线程在这里都要排队,就避免了多线程的问题。
2.每次在方法里new一个新的sdf。
这两种方法都可以解决,但是都有各自的缺陷。sync的关键词,无疑会增加性能消耗,而每次new一个新的对象,似乎也有点浪费。
线程不安全的原因是多个线程操作/使用一个共享的没有做额外并发控制的对象,那,如果每个线程的这个对象都自己维护,自己使用,多个线程之间隔开,是不是就不存在并发问题了呢?
ThreadLocal就是解决这个问题的工具了,再看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
38
39
40

public class MyThreadLocal<T> {
private ConcurrentHashMap<Thread, T> map = new ConcurrentHashMap();

private static MyThreadLocal one;

public static MyThreadLocal getInstance() {
if (one == null) {
synchronized (MyThreadLocal.class) {
if (one == null)
one = new MyThreadLocal();
}
}
return one;
}

public void put(T obj) {
Thread curr = Thread.currentThread();
map.put(curr, obj);
}

public T get() {
Thread curr = Thread.currentThread();
return map.get(curr);
}

public static void main(String[] args) {
MyThreadLocal<String> myThreadLocal = MyThreadLocal.getInstance();
myThreadLocal.put("aaaa");
Thread thread = new Thread() {
@Override
public void run() {
myThreadLocal.put("bbbb");
System.out.println(Thread.currentThread().getName() + ":" + myThreadLocal.get());
}
};
thread.start();
System.out.println(Thread.currentThread().getName() + ":" + myThreadLocal.get());
}
}

这里做一个简单的demo模拟一下这个思路,当然,实际上源码和这个有比较大的区别。

我们再看一个简单的controller代码来学习一下如何使用ThreadLocal来解决线程之间隔离sdf的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ThreadLocal<SimpleDateFormat> sdfMap = new ThreadLocal() {
@Override
protected SimpleDateFormat initialValue() {
Thread curr = Thread.currentThread();
log.info("-------------");
return new SimpleDateFormat("yyyyMMdd hh:mm:ss");
}
};

@IgnoreSecurity
@RequestMapping(value = "/test", method = RequestMethod.POST)
@ApiOperation(value = "test")
public RetData collection() {
return RetData.getInstance(sdfMap.get().format(new Date()));
}

通过测试工具,多次访问这个接口,我们会发现,当线程复用时,并不会重复实例化sdf对象。实际上,ThreadLocal的get代码非常简单,如下:

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

getmap的代码非常简单,如下:

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


所以,我们可以理解为,实际上get方法是通过Thread.currentThread获取到当前线程,然后获取到线程自己的局部变量。这样,多个线程之间就被隔离开了。