飞道的博客

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

417人阅读  评论(0)

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

前言

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

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

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


正文

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


ThreadLocal是什么?

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

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

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

它仅有如下三个public方法:

public void set(T value) { ... }
public T get() { ... }
public void remove() { ... }

分别代表:

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

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

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

 * 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).

大致意思是:

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

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

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


ThreadLocal怎么用?

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

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

@Setter
@ToString
private static class Person {
    private Integer age = 18;
}

书写测试代码:

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);
}

运行程序打印输出:

set数据,线程名:main
get数据,线程名:main,数据为:Test2.Person(age=18)
======== Finish =========

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

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


局限性

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

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

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

运行程序,打印输出:

set数据,线程名:main
get数据,线程名:Thread-0,数据为:null
get数据,线程名:main,数据为:Test2.Person(age=18)
======== Finish =========

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

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


InheritableThreadLocal向子线程传递数据

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

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

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

// 使用InheritableThreadLocal作为实现
private static final ThreadLocal<Person> THREAD_LOCAL = new InheritableThreadLocal<>();

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

set数据,线程名:main
get数据,线程名:Thread-0,数据为:Test2.Person(age=18)
get数据,线程名:main,数据为:Test2.Person(age=18)
======== Finish =========

完美。

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


开源框架使用示例

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

RequestContextHolder

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

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不可变变量,如字符串等,那结论尚能成立,但若绑定的是引用类型的变量,结论可就大错特错喽,如下示例:

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,情况如何呢?

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

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的理解和看法(有错则改之嘛~)。
为何会出现此现象,是因为这里面所谓的变量副本都是“引用传递”来着,可以用如下程序证明:

@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 =========");
}

运行程序,控制台输出:

set数据,线程名:main
434455603
434455603
434455603
======== Finish =========

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

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


ThreadLocal使用的正确姿势

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

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

public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

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

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

public static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new InheritableThreadLocal<>();
static {
    DATE_FORMAT_THREAD_LOCAL.set(new SimpleDateFormat("yyyy-MM-dd"));
}

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

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是解决了线程安全问题,大都是望文生义,二者完全非同类问题,读者需要有自己的思考呀。

声明

原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭。你也可【左边扫码/或加wx:fsx641385712】邀请你加入我的 Java高工、架构师 系列群大家庭学习和交流。


转载:https://blog.csdn.net/f641385712/article/details/104573489
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场