本文介绍下Java多线程方面高频出现的ThreadLocal类
基本实践
在传统的多线程开发场景中,为了避免多个线程同时操作一个共享变量而引起并发的问题。通常会通过加锁的形式进行处理。特别是在这个共享变量,并不是一个所谓的共享资源而只是用于线程内部各方法传递、使用的参数时,这种加锁的并发控制显然会降低系统的吞吐量。而ThreadLocal类则给我们提供一个新的思路——线程本地私有存储数据。简单来说就是,ThreadLocal为共享变量在每个线程内部提供了一个副本,用于进行线程内部自身的访问、存储等操作。避免了该共享变量在多线程环境下的操作冲突。该类典型方法如下所示
1 | // 设置Value |
这里通过一个Demo来了解下该如何使用
1 | package com.aaron.ThreadLocalTest; |
通过上述示例及其测试结果,可以看出对于普通的共享变量num而言,在多线程操作过程中会发生冲突。具体表现为Thread-0线程下该变量本来为53,却又被Thread-1线程修改为74;而对于threadLocal变量而言,从测试结果直观上我们就可看出并未在多线程环境下发生冲突,各线程的threadLocal变量数据被线程隔离了
实现原理
前面我们提到ThreadLocal是通过线程本地私有存储数据实现线程安全的。这里结合ThreadLocal、Thread类的源码做进一步阐述。以set方法为例,其首先通过currentThread获取当前线程的Thread实例。并通过getMap方法获取该线程的threadLocals属性,即一个ThreadLocal.ThreadLocalMap实例。而在ThreadLocalMap则通过Entry实现对ThreadLocal及其值的存储。进一步地,为了支持存储多个ThreadLocal变量及其值,ThreadLocalMap类中提供一个Entry数组类型的table字段。get方法同理,不再赘述。简而言之,ThreadLocal之所以可以实现对数据的线程隔离,是因为其将数据存储到Thread实例中
1 | public class ThreadLocal<T> { |
应用场景
线程内资源的复用
众所周知,时间格式化类SimpleDateFormat在多线程环境下是非线程安全的。为此传统的解决方案,要么通过加锁的方式实现,此举会产生阻塞显著降低效率;要么则定义为局部变量,每次使用需自行new该实例,如果任务的数量较多,显然会严重浪费内存、CPU等资源。而ThreadLocal则可以很好的解决该问题。将SimpleDateFormat实例与线程实例进行绑定。一方面,各线程使用不同的SimpleDateFormat实例,避免了SimpleDateFormat线程不安全问题;另一方面,在基于线程池的方式利用线程的场景下,该线程所绑定的SimpleDateFormat实例在本次任务完成后,可以在该线程下一次的任务中继续复用。避免根据任务频繁地创建SimpleDateFormat实例。示例Demo如下所示
1 | public class Demo2 { |
测试结果如下所示,两个线程str1输出的结果证明了SimpleDateFormat的非线程安全
传递上下文
鉴于ThreadLocal的线程隔离特性,可以很方便我们在一个线程内的多个方法进行参数传递。即所谓的Context上下文。典型地,包括用户身份信息、数据库连接信息等。以避免通过添加入参的形式进行传递
内存泄露
Entry的Key
前面我们提到在ThreadLocalMap内部是通过Entry实现对ThreadLocal及其值的存储。而Entry的key字段则是一个指向ThreadLocal实例的弱引用。这里对弱引用 WeakReference 作必要的补充说明:GC进行回收时,对于只具有弱引用的对象,不管当前内存空间是否充足,均会回收该对象。故当一个ThreadLocal实例没有外部强引用时,其必然可以被GC回收,显然利用static修饰的ThreadLocal变量除外。试想如果Entry的key字段是一个指向ThreadLocal实例的强引用,那么如果该线程永远不结束退出,则会导致ThreadLocal实例无法被回收
Entry的Value
需要注意的是,Entry存储value则是通过强引用进行关联的。结合前面通过对Entry的key进行分析可知,一旦ThreadLocal实例不存在外部的强引用而被GC回收后,则相应的Entry实例就会变为key为null而value依然存在强引用。除非该线程退出结束,否则该value对象将会一直被Entry实例强引用而无法进行回收。这也是大家通常所说的ThreadLocal存在内存泄露的根源所在。其实,在ThreadLocal的set、get方法内部实现中也会对Entry数组进行检查,对key为null的Entry实例进行清除。但显然这种清除的触发是有一定的条件。故推荐大家在使用完ThreadLocal后,通过remove方法显式地清除该value值。或者将ThreadLocal变量修饰为static属性,以保持对ThreadLocal实例的强引用。这样就能保证任何时候均可以通过ThreadLocal实例对value进行更新、清除等操作