Java并发编程之ThreadLocal原理
ThreadLocal是什么
早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
Thread-local,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
下面来看一个简单的示例:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ParseDate implements Runnable{
int i = 0;
public ParseDate(int i) {
this.i = i;
}
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public void run() {
try {
Date date = sdf.parse("2018-05-20 12:00:"+i%60);
System.out.println(i+":1"+date);
} catch (ParseException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//用线程池创建线程,
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
es.execute(new ParseDate(i));
}
}
}
运行代码后会出现这种错误:
image.png
造成这样错误的原因是在多线程中使用simpleDateFormat.parse()方法并不是线程安全的,因此,正在线程池中共享这个对象必然会导致报错。
一种可行的解决方案是在simpleDateFormat.parse()前后加锁,这个也是我们一般的处理思路。但这里我们不这么做 ,我们使用ThreadLocal为每一个线程都产生simpleDateFormat对象实例:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ParseDate2 implements Runnable{
int i = 0;
public ParseDate2(int i) {
this.i = i;
}
private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>();
@Override
public void run() {
try {
if (threadLocal.get() == null) {
threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
Date date = threadLocal.get().parse("2018-05-20 12:00:"+i%60);
System.out.println(i+":1"+date);
} catch (ParseException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//用线程池创建线程,
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
es.execute(new ParseDate2(i));
}
}
}
注意这一段 if (threadLocal.get() == null),如果当前线程不持有SimpleDateFormat对象实例。那么就新建一个并把它设置到当前线程中,如果已经持有,则 直接使用。
从这里可以看到,为每一个线程都分配一个对象的工作并不是由ThreadLocal来完成的,而是需要在应用层面保证的,ThreadLoacl只是起到简单容器的作用。如果在应用上为每一个线程分配了相同的对象实例,那么ThreadLocal也不能保证线程安全。
ThreadLoacl的实现原理
ThreadLocal是如何保证这些对象只能被当前线程所访问呢?那我们下面来看一下具体ThreadLocal是如何实现的。
我们需要关注的,自然是ThreadLocal的set()方法和get()方法。先看一下set()方法:
image.png
在set时,首先通过Thread.currentThread()获得当前线程对象,然后通过getMap()拿到线程的ThreadLocalMap,并将值设置ThreadLocalMap中。ThreadLocalMap是Thread的内部成员。
image.png
而设置到ThreadLocal中的数据,也正是写入threadLocals这个Map中。其中,key为ThreadLocal当前对象,value就是我们需要的值。而threadLocals本身保存了当前自己所在线程的所有“局部变量”,也就是ThreadLocal变量的集合。
在进行变更get()操作时,自然是将这个map中的数据拿出来:
image.png
首先,get()方法也是先获取当前线程的ThreadLocalMap对象。然后通过将自己作为key获取vaule。
ThreadLocal的问题
在了解ThreadLocal的内部后,我们自然会引出一个问题,那就是这些变量是维护在Thread类的内部,这也意味着只要线程不退出,对象的引用就会一直存在。
当线程退出时,Thread类会进行一些清理工作,其中包括清理ThreadLocalMap。我们看一下Thread类的exit()方法。
image.png
exit()方法在线程退出前,有系统回调,进行资源清理。
因此如果我们使用线程池,那就意味着当前线程未必退出(比如固定大小的线程池,线程总是存在)。如果这样,将一些比较大的对象设置到ThreadLocal中(它实际保存在线程持有的threadLocals这个Map中,可能会使系统出现内存泄漏(你设置了对象到ThreadLocal中,但是不清理它,在你是用几次后,这个对象不再有用了,但是它却无法被回收)。
此时,如果你希望及时回收对象,最好使用ThreadLocal.remove()方法将这个变量移除。就像我们习惯性的关闭数据库连接一样。如果你确实不需要这个对象了,那么就应该告诉虚拟机,请把它回收掉,防止内存泄漏。
image.png
另外一种有趣的情况是JDK有可能允许你像释放普通变量一样释放ThreadLocal。比如,我们有时候为了加速垃圾回收,会特意写object =null之类的代码,如果这么做,obj所指向的对象就更容易被垃圾回收器发现,从而加速垃圾回收。
同理,对于ThreadLocal的变量,我们也可以手动将其设置为null,比如threadLocal =null。那么这个ThreadLocal对应的所有线程的局部变量都有可能被回收。