ThreadLocal实现导图及其原理解析

来源:互联网 发布:网络最近流行的舞蹈 编辑:程序博客网 时间:2024/05/21 18:45

首先我们来看生活中的一个情景:我们到银行办卡前必须填表,如果人多而笔只有一支,那么填表需要花费大量的时间。如果是人手一支笔的话,那就能省去排队等笔的时间,从而大大减少了填表时间。在并发编程中,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!

原创粉丝点击