ThreadLocal实现导图及其原理解析

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值