Java Threadlocal

介绍Java中Threadlocal的使用方法。

Threadlocal

Threadlocal主要解决的是每个线程绑定自己的值,也就是说保证相同变量在不同线程的隔离性

Threadlocal保证每一个使用该变量的线程都提供一个变量值得副本,每一个副本的改动不会影响其他副本的值。

默认值

默认情况下,初始化的threadlocal值为null,通过继承threadlocal并重写initialValue实现覆盖初始值。

1
2
3
4
5
6
7
8
9
10
11
public class MyThreadLocal {

public static class ThreadLocal1 extends ThreadLocal {
@Override
protected Object initialValue() {
return "default value";
}
}

public static ThreadLocal1 t1 = new ThreadLocal1();
}

SimpleDateFormat安全格式化

SimpleDateFormat类是用来对日期字符串进行解析和格式化输出。

DateFormatSimpleDateFormat类不都是线程安全的,在多线程环境下调用formatparse方法应该使用同步代码来避免问题。

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 static class DateUtil {
// 使用静态变量来存储SimpleDateFormat
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public static String formatDate(Date date) throws ParseException {
return sdf.format(date);
}

public static Date parse(String strDate) throws ParseException {
return sdf.parse(strDate);
}
}

public static class TestSimpleDateFormatThreadSafe extends Thread {
@Override
public void run() {
while(true) {
try {
this.join(2000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
try {
System.out.println(this.getName()+":"+DateUtil.parse("2013-05-24 06:02:20"));
} catch (ParseException e) {
e.printStackTrace();
}
}
}
}

public static void main(String[] args) {
for(int i = 0; i < 3; i++){
new TestSimpleDateFormatThreadSafe().start();
}

}

结果输出:

1
2
3
4
5
6
7
8
9
10
11
12
Exception in thread "Thread-1" Exception in thread "Thread-2" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
Thread-0:Fri May 24 06:02:20 CST 2013
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at MyThreadLocal$DateUtil.parse(MyThreadLocal.java:28)
at MyThreadLocal$TestSimpleDateFormatThreadSafe.run(MyThreadLocal.java:42)

这正是由于非线程安全的SimpleDateFormat造成的(详情)。

推荐方法:使用threadlocal对不同线程使用不同的SimpleDateFormat对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ThreadLocalDateUtil {
private static final String date_format = "yyyy-MM-dd HH:mm:ss";
// 使用threadlocal来为不同线程生成相同参数的不同副本
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>();

public static DateFormat getDateFormat()
{
DateFormat df = threadLocal.get();
// 默认值为null, 可以继承threadlocal重写initialValue来实现默认值
if(df==null){
df = new SimpleDateFormat(date_format);
threadLocal.set(df);
}
return df;
}

public static String formatDate(Date date) throws ParseException {
return getDateFormat().format(date);
}

public static Date parse(String strDate) throws ParseException {
return getDateFormat().parse(strDate);
}
}

实现原理

我们使用的ThreadLocal是由每个线程中的ThreadLocalMap组成。

ThreadLocalMap是用来存储线程独有KV数据,KEY为WeakReference弱引用

GET

当线程调用ThreadLocal.get()方法获取变量时,

  • 获取当前的Thread
  • 获取线程的ThreadLocalMap
  • 从MAP中获取Entry
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();
}

SET

当线程调用ThreadLocal.set(T object)方法设置变量时,

  • 获取当前的Thread
  • 获取线程的ThreadLocalMap
  • 设置MAP的KV(KEY为弱引用)
1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

使用弱引用作为MAP的KEY,该KEY会在 GC 时被回收。

对于一个正常的Map来说,调用Map.clear方法来清空map,所有对象就会释放。

调用map.remove(KEY)方法,会移除KEY对应的对象整个entry,这样key和value 就不会任何对象引用,被java虚拟机回收。

内存泄露

如何产生

线程中ThreadLocalMap中的KEY弱引用

当KEY中引用的对象没有其他强引用时会被垃圾回收器清理掉,此时KEY就会变为null

此时,由于VALUE还存在KEY为null的强引用,VALUE,VALUE对象就无法被回收,从而可能导致内存泄露

实际上,产生内存泄露的原因是存在使用了ThreadLocal并长时间存活的线程

如果线程结束,无论是ThreadLocalMap中否存在KEY为NULL,ThreadLocalMap的生命周期都会结束。

如何避免

ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施,在set/get/remove方法中都会去清理KEY为NULL的Entry。


2019/11/23 Update

ThreadLocal存在的意义

首先,ThreadLocal可以解决哪些问题?

  • 线程内部上下文的传递(替代参数传递)
  • 多线程并发的资源隔离(临界资源)

上下文的传递看起来是没有太多的问题,但是在实际的应用场景下又很难确定变量的作用域,变量是何时赋值的、何时移除的,都是不能确定的,所以在使用过程中会带来很多不确定性。

ThreadLocal为线程提供了一套管理线程状态机制。

ThreadLocal拥有与线程同样的生命周期,从而拥有提供线程状态的能力。

常见的项目应用都是采用贫血模型贫血模型无状态的,实际上是一种过程化的设计。

为什么要使用WeakReference

为了避免ThreadLocal对于对象生命周期的影响,在GC时不会由于ThreadLocal中的引用而无法回收。


2020/08/05 Update

ThreadLocalMap & HashMap

ThreadLocalMap采用了开放地址法,而HashMap采用了链地址法

开放地址法:在线程数量有限的场景下,通过寻找下一个空的散列地址来解决冲突问题。

链地址法:通过冲突链来解决冲突问题,对于少量线程的场景下,会造成大量的hash槽浪费。

参考

http://www.cnblogs.com/peida/archive/2013/05/31/3070790.html
http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/?utm_source=tuicool&utm_medium=referral