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

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

前言


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


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


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


正文


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


ThreadLocal是什么?


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


ThreadLocal和InheritableThreadLocal均是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,它能够向子线程传递数据。

相关文章
|
2天前
|
安全 Java 调度
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第12天】 在现代软件开发中,多线程编程是提升应用程序性能和响应能力的关键手段之一。特别是在Java语言中,由于其内置的跨平台线程支持,开发者可以轻松地创建和管理线程。然而,随之而来的并发问题也不容小觑。本文将探讨Java并发编程的核心概念,包括线程安全策略、锁机制以及性能优化技巧。通过实例分析与性能比较,我们旨在为读者提供一套既确保线程安全又兼顾性能的编程指导。
|
2天前
|
Java
Java中的多线程编程:基础知识与实践
【5月更文挑战第13天】在计算机科学中,多线程是一种使得程序可以同时执行多个任务的技术。在Java语言中,多线程的实现主要依赖于java.lang.Thread类和java.lang.Runnable接口。本文将深入探讨Java中的多线程编程,包括其基本概念、实现方法以及一些常见的问题和解决方案。
|
2天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第13天】 在Java开发中,并发编程是一个复杂且重要的领域。它不仅关系到程序的线程安全性,也直接影响到系统的性能表现。本文将探讨Java并发编程的核心概念,包括线程同步机制、锁优化技术以及如何平衡线程安全和性能。通过分析具体案例,我们将提供实用的编程技巧和最佳实践,帮助开发者在确保线程安全的同时,提升应用性能。
10 1
|
2天前
|
Java 调度
Java一分钟之线程池:ExecutorService与Future
【5月更文挑战第12天】Java并发编程中,`ExecutorService`和`Future`是关键组件,简化多线程并提供异步执行能力。`ExecutorService`是线程池接口,用于提交任务到线程池,如`ThreadPoolExecutor`和`ScheduledThreadPoolExecutor`。通过`submit()`提交任务并返回`Future`对象,可检查任务状态、获取结果或取消任务。注意处理`ExecutionException`和避免无限等待。实战示例展示了如何异步执行任务并获取结果。理解这些概念对提升并发性能至关重要。
17 5
|
3天前
|
Java
Java一分钟:线程协作:wait(), notify(), notifyAll()
【5月更文挑战第11天】本文介绍了Java多线程编程中的`wait()`, `notify()`, `notifyAll()`方法,它们用于线程间通信和同步。这些方法在`synchronized`代码块中使用,控制线程执行和资源访问。文章讨论了常见问题,如死锁、未捕获异常、同步使用错误及通知错误,并提供了生产者-消费者模型的示例代码,强调理解并正确使用这些方法对实现线程协作的重要性。
14 3
|
3天前
|
安全 算法 Java
Java一分钟:线程同步:synchronized关键字
【5月更文挑战第11天】Java中的`synchronized`关键字用于线程同步,防止竞态条件,确保数据一致性。本文介绍了其工作原理、常见问题及避免策略。同步方法和同步代码块是两种使用形式,需注意避免死锁、过度使用导致的性能影响以及理解锁的可重入性和升级降级机制。示例展示了同步方法和代码块的运用,以及如何避免死锁。正确使用`synchronized`是编写多线程安全代码的核心。
55 2
|
3天前
|
安全 Java 调度
Java一分钟:多线程编程初步:Thread类与Runnable接口
【5月更文挑战第11天】本文介绍了Java中创建线程的两种方式:继承Thread类和实现Runnable接口,并讨论了多线程编程中的常见问题,如资源浪费、线程安全、死锁和优先级问题,提出了解决策略。示例展示了线程通信的生产者-消费者模型,强调理解和掌握线程操作对编写高效并发程序的重要性。
43 3
|
3天前
|
安全 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第11天】在Java并发编程中,线程安全和性能优化是两个重要的主题。本文将深入探讨这两个方面,包括线程安全的基本概念,如何实现线程安全,以及如何在保证线程安全的同时进行性能优化。我们将通过实例和代码片段来说明这些概念和技术。
4 0
|
3天前
|
Java 调度
Java并发编程:深入理解线程池
【5月更文挑战第11天】本文将深入探讨Java中的线程池,包括其基本概念、工作原理以及如何使用。我们将通过实例来解释线程池的优点,如提高性能和资源利用率,以及如何避免常见的并发问题。我们还将讨论Java中线程池的实现,包括Executor框架和ThreadPoolExecutor类,并展示如何创建和管理线程池。最后,我们将讨论线程池的一些高级特性,如任务调度、线程优先级和异常处理。
|
4天前
|
安全 Java
【JAVA进阶篇教学】第十篇:Java中线程安全、锁讲解
【JAVA进阶篇教学】第十篇:Java中线程安全、锁讲解