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静态内部类,这个Map为每个线程复制一个变量的拷贝,每一个内部线程都有一个ThreadLocalMap对象。

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

  • 获取当前线程引用
  • 获取线程内部的ThreadLocalMap对象
  • 设置mapkey值为threadLocal对象,value为参数中的object
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);
}

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

  • 获取当前线程引用
  • threadLocal对象为key去获取响应的ThreadLocalMap
  • 如果此Map不存在则初始化一个,否则返回其中的变量
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();
}

每个线程内部的ThreadLocalMap对象中的key保存的threadLocal对象的引用,但对threadLocal的对象的引用是WeakReference弱引用

ThreadLocalMap是使用ThreadLocal的弱引用作为Key的,弱引用的对象在 GC 时会被回收。

对于一个正常的Map来说,调用Map.clear方法来清空map,所有对象就会释放。调用map.remove(key)方法,会移除key对应的对象整个entry,这样key和value 就不会任何对象引用,被java虚拟机回收。

内存泄露

如何产生

Thread对象里面的ThreadLocalMap中的keyThreadLocal的对象的弱引用,如果ThreadLocal对象会回收,那个这个对象对应的key就会变为null,那么ThreadLocalMap就无法移除其对应的value,那么value对象就无法被回收,导致内存泄露

但是如果Thread运行结束,整个线程对象被回收,那么value所引用的对象也就会被垃圾回收。

对于没有使用线程池的方法来说,因为每次线程运行完就退出了,Map里面引用的所有对象都会被垃圾回收,所以没有关系。

如何避免?

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


2019/11/23 Update

ThreadLocal存在的意义

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

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

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

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

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

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


参考

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