首先我们来看生活中的一个情景:我们到银行办卡前必须填表,如果人多而笔只有一支,那么填表需要花费大量的时间。如果是人手一支笔的话,那就能省去排队等笔的时间,从而大大减少了填表时间。在并发编程中,ThreadLocal就解决了上述问题。
并发编程 | 生活实例 |
---|---|
线程 | 填表的客户 |
线程间共享变量 | 笔 |
ThreadLocal | 实现“人手一支” |
1. 资源竞争带来的麻烦
我们以一段ThreadLocal的应用代码为契机,来看看ThreadLocal是如何实现“人手一支”,从而减小资源竞争的。
我们先来看一个没用ThreadLocal的demo
public class WithoutThreadLocalDemo {
public static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //线程共享变量
public static class ParseDate implements Runnable{
int i = 0;
public ParseDate(int i) {
this.i = i;
}
@Override
public void run() {
try {
Date date = simpleDateFormat.parse("2017-09-06 19:29:" + i%60); //使用共享变量
System.out.println(i + ":" + date + System.currentTimeMillis());
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++){ //开启10个线程
newFixedThreadPool.execute(new ParseDate(i));
}
newFixedThreadPool.shutdown();
}
}
如图所见,运行后程序抛出NumberFormatException,根据异常来看程序的27行
Date date = simpleDateFormat.parse("2017-09-06 19:29:" + i%60);
因为SimpleDateFormat是线程不安全的,当多个线程使用simpleDateFormat时产生了不符合格式的字符串,如下导图所示。但如果你足够幸运,线程间不发生中断性的竞争就不会出现上述情况。
2. 使用ThreadLocal 解决竞争
接下来我们使用ThreadLocal解决上述问题
public class ThreadLocalDemo {
public static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>(); //创建threadLocal变量
// public static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static class ParseDate implements Runnable{
int i = 0;
public ParseDate(int i) {
this.i = i;
}
@Override
public void run() {
try {
if (threadLocal.get() == null){
threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); //向threadLocal中设值
}
Date date = threadLocal.get().parse("2017-09-06 19:29:" + i%60); //从threadLocal中取值使用
System.out.println(i + ":" + date + System.currentTimeMillis());
} catch (ParseException e) {
e.printStackTrace();
}
finally {
threadLocal.remove();
}
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++){ //创建10个线程
newFixedThreadPool.execute(new ParseDate(i));
}
newFixedThreadPool.shutdown();
}
}
运行结果:
2.1 从ThreadLocal源码入手
从程序中可以发现,ThreadLocal的get()和set()方法在整个程序中起了重要作用,先来看set()的源码。
通过源码可知每个线程自带ThreadLocalMap属性,执行set()方法时以ThreadLocal作为键,变量作为值存入ThreadLocalMap。
get()方法先获得当前线程,以ThreadLocal为键从线程的ThreadLocalMap中获取到相应的值。
2.2 关系导图
这就好比,每个排队的人自带了一个袋子,ThreadLocal是凭袋子发笔的机器,自动向每个排队人的口袋里放笔。
在这里值得注意的一点是:ThreadLocalMap的key对ThreadLocal的引用是弱引用。(详细请看《实战Java高并发程序设计》4.3.2)
并发编程 | 生活实例 |
---|---|
thread | 填表的客户 |
SimpleDateFormat | 笔 |
ThreadLocalMap | 装笔的容器 |
ThreadLocal | 发放笔,实现人手一支 |
3. ThreadLocal的资源回收问题
红框部分的代码用finally进行了包裹,大家都知道用finally包裹的代码是一定会执行的,为什么要这么做呢?
在demo中,我们用到了线程池。执行完后,线程池中的线程依旧存在,线程ThreadLocalMap属性内存放的变量不会被回收。如果ThreadLocalMap中存放很大的变量而一直不回收,很有可能导致内存泄露。因此手动回收变量显得很有必要,remove()会将线程ThreadLocalMap中的变量移除。
本文部分代码参考《实战Java高并发程序设计》,同时也向大家推荐下这本书。有不足或错误的地方,希望大家及时向作者反馈,欢迎大家的吐槽,QQ375035834!