前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ThreadLocal能解决线程安全问题?胡扯!本文教你正确的使用姿势【享学Java】

ThreadLocal能解决线程安全问题?胡扯!本文教你正确的使用姿势【享学Java】

作者头像
YourBatman
发布2020-03-18 20:01:46
1.8K1
发布2020-03-18 20:01:46
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦

跟对领导很重要:愿意教你的,并且放手让你做的领导要珍惜。

前言

ThreadLocal:线程 + 本地 -> 线程本地变量(所以说我觉得它取名叫ThreadLocalVariable获取还更能让人易懂些),它的出镜率可不低。虽然写业务代码一般用不着,但它是开源工具的常客:用于在线程生命周期内传递数据。

有的人说,每看一遍ThreadLocal都会有新的感受,这其实是比较诡异的现象,因为我认为“真理”是不应该经常变的(或者说是不可能变化的)。我自己百度了一波,关于ThreadLocal的文章满天飞,有讲使用的亦有讲原理的,鱼龙混杂。其中有一派文章主旨讲述:使用ThreadLocal解决多线程程序的并发问题,使用该工具写出简洁、优美的多线程程序

这类水文不在少数,大有占据主流的意思,它对初学者的误导性非常大,从而造成了每看一遍都会有新感受的错觉。本文为社区贡献一份微薄之力,在这里教你完全正确的使用ThreadLocal的姿势,避免你以后再犯迷糊。


正文

本文的内容并不讲述ThreadLocal/InheritableThreadLocal的源码、原理,一方面确实不难,另一方面关于它的源码、原理讲解的相关文章确实不在少数。


ThreadLocal是什么?

我们从字面上的意思来理解ThreadLocal,Thread:线程;Local:本地的,局部的。也就是说,ThreadLocal是线程本地的变量,只要是本线程内都可以使用,线程结束了,那么相应的线程本地变量也就跟随着线程消失了。

ThreadLocalInheritableThreadLocal均是JDK1.2新增的API,在JDK5后支持到了泛型。它表示线程局部变量:为当前线程绑定一个变量,这样在线程的声明周期内的任何地方均可取出。

说明:InheritableThreadLocal继承自ThreadLocal,在其基础上扩展了能力:不仅可在本线程内获取绑定的变量,在子线程内亦可获取到。(请注意:必须是子线程,若你是线程池就可能不行喽,因为线程池里的线程是实现初始化好的,并不一定是你的子线程~)

它仅有如下三个public方法:

代码语言:javascript
复制
public void set(T value) { ... }
public T get() { ... }
public void remove() { ... }

分别代表:

  • 设置值:把value和当前线程绑定
  • 获取值:获取和当前线程绑定的变量值
  • 删除值:移除绑定关系

说明:虽然每个绑定关系都是使用的WeakReference,但是还是建议你显示的做好remove()移除动作,否则容易造成内存泄漏。当然关于ThreadLocal内存泄漏并不是本文的内容,有兴趣可以自行去了解。

另外对于解释ThreadLocal是什么,建议可参考下它的Javadoc:

代码语言:javascript
复制
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).

大致意思是:

代码语言:javascript
复制
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量
(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。

ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程
(例如,用户 ID 或事务 ID)相关联。

更准确的说,一般我们使用ThreadLocal是作为private static final字段来使用的~


ThreadLocal怎么用?

知道了ThreadLocal是什么后,怎么用其实就非常简单了。看如下这个简单示例:

本例模拟使用Person对象和当前线程绑定:

代码语言:javascript
复制
@Setter
@ToString
private static class Person {
    private Integer age = 18;
}

书写测试代码:

代码语言:javascript
复制
private static final ThreadLocal<Person> THREAD_LOCAL = new ThreadLocal<>();

@Test
public void fun1() {
    // 方法入口处,设置一个变量和当前线程绑定
    setData(new Person());
    // 调用其它方法,其它方法内部也能获取到刚放进去的变量
    getAndPrintData();

    System.out.println("======== Finish =========");
}

private void setData(Person person){
    System.out.println("set数据,线程名:" + Thread.currentThread().getName());
    THREAD_LOCAL.set(person);
}
private void getAndPrintData() {
    // 拿到当前线程绑定的一个变量,然后做逻辑(本处只打印)
    Person person = THREAD_LOCAL.get();
    System.out.println("get数据,线程名:" + Thread.currentThread().getName() + ",数据为:" + person);
}

运行程序打印输出:

代码语言:javascript
复制
set数据,线程名:main
get数据,线程名:main,数据为:Test2.Person(age=18)
======== Finish =========

这便是ThreadLocal的典型应用场景:方法调用间传参,并不一定必须得从方法入参处传入进来,还可以通过ThreadLocal来传递,进而在该线程生命周期内任何地方均可获取到,非常的方便有木有

小细节:set和get数据时的线程是同一个线程:均未main线程


局限性

上例是ThreadLocal的典型应用场景,大部分情况下均能正常work。但是,在当下互联网环境下,经常会用到了异步方式来提高程序运行效率,比如如上方法调用getAndPrintData()因比较耗时所以我希望异步去进行,改造如下:

代码语言:javascript
复制
@Test
public void fun1() throws InterruptedException {
    // 方法入口处,设置一个变量和当前线程绑定
    setData(new Person());

    // getAndPrintData();
    // 异步获取数据
    Thread subThread = new Thread(() -> getAndPrintData());
    subThread.start();
    subThread.join();
	
	// 非异步方式获:在主线程里获取
	getAndPrintData();
    System.out.println("======== Finish =========");
}

运行程序,打印输出:

代码语言:javascript
复制
set数据,线程名:main
get数据,线程名:Thread-0,数据为:null
get数据,线程名:main,数据为:Test2.Person(age=18)
======== Finish =========

线程名为Thread-0的子线程里并没有获取到数据,只因为它并不是当前线程,貌似合情合理,这便是ThreadLocal的局限性。

那既然这是一个常见需求,除了把变量作为方法入参传进去,有没有什么更为便捷的方案呢?有的,JDK扩展了ThreadLocal提供了一个子类:InheritableThreadLocal,它能够向子线程传递数据。


InheritableThreadLocal向子线程传递数据

它继承自ThreadLocal,所以它能力更强:通过它set进去的数据,不仅本线程内任意地方可以获取,子线程(包括子线程的子线程…)内的任意地方也都可以获取到值。

重说三:必须是子线程,必须是子线程,必须是子线程(当然子线程的子线程…也算作这个范畴)

因此对于上例,只需做个微小的变化:

代码语言:javascript
复制
// 使用InheritableThreadLocal作为实现
private static final ThreadLocal<Person> THREAD_LOCAL = new InheritableThreadLocal<>();

再次运行测试程序,打印:

代码语言:javascript
复制
set数据,线程名:main
get数据,线程名:Thread-0,数据为:Test2.Person(age=18)
get数据,线程名:main,数据为:Test2.Person(age=18)
======== Finish =========

完美。

强调说明:其实这还不完美。还有非父子线程、垮线程池之间的数据产地它解决不了,对于这种场景较为复杂,源生JDK并没有“特效类”,一般需要借助阿里巴巴的开源库:TTL(transmittable-thread-local)来搞定,这个后面文章还会继续补充。


开源框架使用示例

优秀的开源框架中有非常多的对ThreadLocal的使用示例,这里以Spring的为例:

RequestContextHolder

代码语言:javascript
复制
public abstract class RequestContextHolder  {
	...
	private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal<>("Request attributes");
	private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal<>("Request context");
	...
	@Nullable
	public static RequestAttributes getRequestAttributes() {
		RequestAttributes attributes = requestAttributesHolder.get();
		if (attributes == null) {
			attributes = inheritableRequestAttributesHolder.get();
		}
		return attributes;
	}
	...
}

TransactionSynchronizationManager

代码语言:javascript
复制
public abstract class TransactionSynchronizationManager {
	...
	private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
	private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal<>("Transaction synchronizations");
	private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal<>("Current transaction name");
	private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal<>("Current transaction read-only status");
	private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal<>("Current transaction isolation level");
	private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal<>("Actual transaction active");
	...
}

如果你们公司做过全链路追踪、全链路压测,那么ThreadLocal将是其中使用最为频繁的基础组件之一。


ThreadLocal不能解决共享变量的线程安全问题

标题即是结论,请务必烂熟于胸,使用时请勿滥用。网上太多的文章说:ThreadLocal使得每个线程均持有这个变量的副本,所以对多线程是安全的。言外之意便是:

  • 只要这个变量是共享变量,把它用ThreadLocal包起来便可
  • 别的线程修改其线程绑定的变量,并不影响其它线程里的变量值

以上结果,如果你的ThreadLocal绑定的是Immutable不可变变量,如字符串等,那结论尚能成立,但若绑定的是引用类型的变量,结论可就大错特错喽,如下示例:

代码语言:javascript
复制
private static final ThreadLocal<Person> THREAD_LOCAL = new InheritableThreadLocal<>();


@Test
public void fun2() throws InterruptedException {
    setData(new Person());

    Thread subThread1 = new Thread(() -> {
        Person data = getAndPrintData();
        if (data != null)
            data.setAge(100);
        getAndPrintData(); // 再打印一次
    });
    subThread1.start();
    subThread1.join();


    Thread subThread2 = new Thread(() -> getAndPrintData());
    subThread2.start();
    subThread2.join();

    // 主线程获取线程绑定内容
    getAndPrintData();
    System.out.println("======== Finish =========");
}


private void setData(Person person) {
    System.out.println("set数据,线程名:" + Thread.currentThread().getName());
    THREAD_LOCAL.set(person);
}

private Person getAndPrintData() {
    // 拿到当前线程绑定的一个变量,然后做逻辑(本处只打印)
    Person person = THREAD_LOCAL.get();
    System.out.println("get数据,线程名:" + Thread.currentThread().getName() + ",数据为:" + person);
    return person;
}

对本实例模拟的场景做如下文字解释:

  1. 主线程设置一个共享变量Person(age=18),希望子线程得以共享
  2. 创建两个子线程subThread1/subThread2用于模拟多个线程,共享访问Person这个共享变量
  3. 线程subThread1在其执行过程中,把共享变量Person的age值改为了100
  4. 线程subThread2以及主线程此时也去获取共享变量Person,情况如何呢?

运行测试程序,打印如下:

代码语言:javascript
复制
set数据,线程名:main
get数据,线程名:Thread-0,数据为:Test2.Person(age=18)
get数据,线程名:Thread-0,数据为:Test2.Person(age=100)
get数据,线程名:Thread-1,数据为:Test2.Person(age=100)
get数据,线程名:main,数据为:Test2.Person(age=100)
======== Finish =========

看到这个结果,你或许会傻眼。不是拷贝了一个副本吗,为何最终值也变了呢?可以明确的告诉你,这不是ThreadLocal有错,而是你没有理解它。

结论:线程subThread1把共享变量Person的值改过之后,其它线程再去获取得到的均是改变后的值,因此此处使用ThreadLocal并没有达到决绝共享变量线程安全问题的效果。

这是最为典型的一种错误认知,希望通过此例能帮你纠正你以前对ThreadLocal的理解和看法(有错则改之嘛~)。 为何会出现此现象,是因为这里面所谓的变量副本都是“引用传递”来着,可以用如下程序证明:

代码语言:javascript
复制
@Test
public void fun3() throws InterruptedException {
    setData(new Person());

    new Thread(() -> System.identityHashCode(THREAD_LOCAL.get())).start();
    new Thread(() -> System.identityHashCode(THREAD_LOCAL.get())).start();

    TimeUnit.SECONDS.sleep(2);
    System.out.println(System.identityHashCode(THREAD_LOCAL.get()));
    System.out.println("======== Finish =========");
}

运行程序,控制台输出:

代码语言:javascript
复制
set数据,线程名:main
434455603
434455603
434455603
======== Finish =========

可以看到,不管是主线程还是子线程,绑定的变量的HashCode一模一样。这样就更能解释了:为何一处修改,其它均被修改了吧,因为指向是同一位置。

因此:ThreadLocal包装根本就不能解决共享变量的多线程安全问题


ThreadLocal使用的正确姿势

说了这么多,那使用它的正确姿势是什么呢?正确姿势用文字无法表达,请以如下使用示例为参照。

众所周知,SimpleDateFomat是线程不安全的,所以若我们这样定义一个全局模版:

代码语言:javascript
复制
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

在多线程访问的情况下,那必然会有多线程安全问题。

而通过如上表述,这么做也依旧是不靠谱的,依旧解决不了多线程安全问题。

代码语言:javascript
复制
public static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new InheritableThreadLocal<>();
static {
    DATE_FORMAT_THREAD_LOCAL.set(new SimpleDateFormat("yyyy-MM-dd"));
}

其实关于它的使用,阿里巴巴已经在它的规范手册里给出了使用示范:

代码语言:javascript
复制
public static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new InheritableThreadLocal<DateFormat>() {
    @Override
    protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd");
    }
};

这么处理后再使用DateFormat这个实例,就是绝对安全的。理由是每次调用set方法进行和线程绑定的时候,都是new一个新的SimpleDateFormat实例,而并非全局共享一个,不存在了数据共享那必然就线程安全喽。

当然你可能会说,这和自己在线程里面每次你自己new一个出来用有什么区别呢?答案是:效果上没任何区别,但是这样方便。比如:可以保持线程里面只有唯一一个SimpleDateFormat对象,你要手动new的话,一不小心就new多个了,消耗内存不说还不好管理。

可能你还会说,那只new一个实例,然后哪个方法要用就通过参数传给它就行。答案还是一样的:不嫌麻烦的话,这样做也是能达到效果的。

然而,对于这种全局通用的变量,使用ThreadLocal管理和维护一份即可,大大的降低了维护成本和他人的使用成本。so,只要你使用它的姿势正确了,它能让你事半功倍,特别是如果你是写中间件的小伙伴的话,跟它打交道会更为频繁。


总结

本文总体上算是一篇纠错文章,希望更多人能够看到,多多转发,为社区献上微薄之力。

ThreadLocal并不是为了解决线程安全问题,而是提供了一种将变量绑定到当前线程的机制,类似于隔离的效果。ThreadLocal跟线程安全基本不搭边:线程安全or不安全取决于绑上去的实例是怎样的:

  • 每个线程独享一份new出来的实例 -> 线程安全
  • 多个线程共享一份“引用类型”实例 -> 线程不安全

ThreadLocal最大的用处就是用来把实例变量共享成全局变量,在程序的任何方法中都可以访问到该实例变量而已。网上很多人说ThreadLocal是解决了线程安全问题,大都是望文生义,二者完全非同类问题,读者需要有自己的思考呀。

分隔线
分隔线

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 正文
    • ThreadLocal是什么?
      • ThreadLocal怎么用?
        • 局限性
        • InheritableThreadLocal向子线程传递数据
        • 开源框架使用示例
      • ThreadLocal不能解决共享变量的线程安全问题
        • ThreadLocal使用的正确姿势
        • 总结
        相关产品与服务
        消息队列 TDMQ
        消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档