一篇文章带你搞定并发多线程里的 ThreadLocal

除了控制资源的访问外,我们还可以通过增加资源来保证所有对象的线程安全。比如,让100个人填写个人信息表,如果只有一支笔,那么大家就得挨个填写,对于管理人员来说,必须保证大家不会去哄抢这仅存的一支笔,否则,谁也填不完。从另外一个角度出发,我们可以准备100支笔,人手一支,那么所有人很快就能完成表格的填写工作。如果说锁使用的是第一种思路,那么ThreadLocal使用的就是第二种思路。

一、ThreadLocal的简单使用

从ThreadLocal的名字上可以看到,这是一个线程的局部变量。也就是说,只有当前线程可以访问。既然是只有当前线程可以访问的数据,自然是线程安全的。

下面来看一个简单的示例。

在这里插入图片描述
上述代码在多线程中使用SimpleDateFormat对象实例来解析字符串类型的日期。执行上述代码,一般来说,很可能得到一些异常(篇幅有限不再给出堆栈,只给出异常名称):

在这里插入图片描述
一种可行的方案是在sdf.parse()方法前后加锁,这也是我们一般的处理思路。这里不这么做,我们使用ThreadLocal为每一个线程创造一个SimpleDateformat对象实例。

在这里插入图片描述
在上述代码第7~9行中,如果当前线程不持有SimpleDateformat对象实例,那么就新建一个并把它设置到当前线程中,如果已经持有,则直接使用。

从这里也可以看到,为每一个线程分配一个对象的工作并不是由ThreadLocal来完成的,而是需要在应用层面保证的。如果在应用上为每一个线程分配了相同的对象实例,那么ThreadLocal也不能保证线程安全,这点也需要大家注意。

注意:为每一个线程分配不同的对象,需要在应用层面保证ThreadLocal只起到了简单的容器作用。

二、ThreadLocal 的实现原理

我们需要关注的自然是ThreadLocalset()方法和get()方法。先从set()方法说起:

在这里插入图片描述
在set时,首先获得当前线程对象,然后通过getMap()方法拿到线程的ThreadLocalMap,并将值存入ThreadLocalMap中。而ThreadLocalMap可以理解为一个Map(虽然不是,但是你可以把它简单地理解成HashMap),但是它是定义在Thread内部的成员。注意下面的定义是从Thread类中摘出来的:

在这里插入图片描述

而设置到ThreadLocal中的数据,也正是写入了threadLocals的这个Map。其中,key为ThreadLocal当前对象,value就是我们需要的值。而threadLocals本身就保存了当前自己所在线程的所有“局部变量”,也就是一个ThreadLocal变量的集合。

在进行get()方法操作时,自然就是将这个Map中的数据拿出来。

在这里插入图片描述
get()方法先取得当前线程的ThreadLocalMap对象,然后通过将自己作为key取得内部的实际数据。

在了解了ThreadLocal的内部实现后,我们自然会引出一个问题:那就是这些变量是维护在Thread类内部的(ThreadLocalMap定义所在类),这也意味着只要线程不退出,对象的引用将一直存在。

当线程退出时,Thread类会进行一些清理工作,其中就包括清理ThreadLocalMap,注意下述代码的加粗部分:

在这里插入图片描述
因此,使用线程池就意味着当前线程未必会退出(比如固定大小的线程池,线程总是存在)。如果这样,将一些大的对象设置到ThreadLocal中(它实际保存在线程持有的threadLocals Map内),可能会使系统出现内存泄漏的可能(这里我的意思是:你设置了对象到ThreadLocal中,但是不清理它,在你使用几次后,这个对象也不再有用了,但是它却无法被回收)。

此时,如果你希望及时回收对象,最好使用ThreadLocal.remove()方法将这个变量移除。就像我们习惯性地关闭数据库连接一样。如果你确实不需要这个对象了,就应该告诉虚拟机,请把它回收,防止内存泄漏。

另外一种有趣的情况是JDK也可能允许你像释放普通变量一样释放ThreadLocal。比如,我们有时候为了加速垃圾回收,会特意写出类似obj=null的代码。如果这么做,那么obj所指向的对象就会更容易地被垃圾回收器发现,从而加速回收。

同理,如果对于ThreadLocal的变量,我们也手动将其设置为null,比如tl=null,那么这个ThreadLocal对应的所有线程的局部变量都有可能被回收。这里面的奥秘是什么呢?先来看一个简单的例子。

在这里插入图片描述
在这里插入图片描述
上述案例是为了跟踪ThreadLocal对象,以及内部SimpleDateFormat对象的垃圾回收。为此,我们在第3行代码和第17行代码中重载了finalize()方法。这样,我们在对象被回收时,就可以看到它们的踪迹。

在主函数main中,先后进行了两次任务提交,每次10 000个任务。在第一次任务提交后,在代码第39行,我们将tl设置为null,并进行一次GC。接着,我们进行第二次任务提交,完成后,在代码第50行再进行一次GC。

执行上述代码,最有可能的一种输出如下所示。

在这里插入图片描述
在这里插入图片描述
注意这些输出所代表的含义。首先,线程池中10个线程都各自创建了一个SimpleDateFormat对象实例。接着进行第一次GC,可以看到ThreadLocal对象被回收了(这里使用了匿名类,所以类名看起来有点怪,这个类就是第2行创建的tl对象)。提交第2次任务,这次一样也创建了10个SimpleDateFormat对象,然后进行第二次GC。在第二次GC后,第一次创建的10个SimpleDateFormat的子类实例全部被回收。虽然我们没有手工remove()这些对象,但是系统依然有可能回收它们(注意,这段代码是在JDK 7中输出的,在JDK 8中,也许得不到类似的输出,大家可以比较两个JDK版本之间线程持有ThreadLocal变量的不同)。

要了解这里的回收机制,我们需要更进一步了解Thread.ThreadLocalMap的实现。之前我们说过,ThreadLocalMap是一个类似HashMap的东西。更准确地说,它更加类似WeakHashMap。

ThreadLocalMap的实现使用了弱引用。弱引用是比强引用弱得多的引用。Java虚拟机在垃圾回收时,如果发现弱引用,就会立即回收。ThreadLocalMap内部由一系列Entry构成,每一个Entry都是WeakReference<ThreadLocal>

在这里插入图片描述
这里的参数k就是Map的key,v就是Map的value,其中k也是ThreadLocal实例,作为弱引用使用(super(k)就是调用了WeakReference的构造函数)。因此,虽然这里使用ThreadLocal作为Mapkey,但是实际上,它并不真的持有ThreadLocal的引用。而当ThreadLocal的外部强引用被回收时,ThreadLocalMap中的key就会变成null。当系统进行ThreadLocalMap清理时(比如将新的变量加入表中,就会自动进行一次清理,虽然JDK不一定会进行一次彻底的扫描,但显然在这个案例中,它奏效了),就会将这些垃圾数据回收。ThreadLocal的回收机制,如图4.1所示。

在这里插入图片描述

三、对性能有何帮助

为每一个线程分配一个独立的对象对系统性能也许是有帮助的。当然了,这也不一定,这完全取决于共享对象的内部逻辑。如果共享对象对于竞争的处理容易引起性能损失,我们还是应该考虑使用ThreadLocal为每个线程分配单独的对象。一个典型的案例就是在多线程下产生随机数。

这里,让我们简单测试一下在多线程下产生随机数的性能问题。首先,我们定义了一些全局变量。

在这里插入图片描述
代码第1行定义了每个线程要产生的随机数数量;第2行定义了参与工作的线程数量;第3行定义了线程池;第4行定义了被多线程共享的Random实例,用于产生随机数;第6~11行定义了由ThreadLocal封装的Random。

定义一个工作线程的内部逻辑,它可以工作在两种模式下。

第一是多线程共享一个Random(mode=0)。

第二是多个线程各分配一个Random(mode=1)。

在这里插入图片描述
上述代码第19~27行定义了线程的工作内容。每个线程都会产生若干个随机数,完成工作后,记录并返回所消耗的时间。

最后是我们的main()函数,它分别对上述两种情况进行测试,并打印了测试的耗时。

在这里插入图片描述
上述代码的运行结果可能如下:

在这里插入图片描述
很明显,在多线程共享一个Random实例的情况下,总耗时达13秒之多(这里是指4个线程的耗时总和,不是程序执行经历的时间)。而在ThreadLocal模式下,仅耗时1.7秒左右。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值