一、定义

ThreadLocalJDK包提供的,从名字来看,ThreadLocal意思就是本地线程的意思。

1.1 是什么?

要想知道他是个啥,我们看看ThreadLocal的源码(基于JDK 1.8)中对这个类的介绍:

  1. This class provides thread-local variables. These variables differ from
  2. their normal counterparts in that each thread that accesses one (via its
  3. {@code get} or {@code set} method) has its own, independently initialized
  4. copy of the variable. {@code ThreadLocal} instances are typically private
  5. static fields in classes that wish to associate state with a thread (e.g.,
  6. a user ID or Transaction ID).

大致能够总结出:

  1. TreadLocal可以给我们提供一个线程内的局部变量,而且这个变量与一般的变量还不同,它是每个线程独有的,与其他线程互不干扰的;
  2. ThreadLocal 与普通变量的区别在于:每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都会被回收;
  3. 简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个ThreadLocal.ThreadLocalMap把数据进行隔离,每个线程的数据不共享,自然就没有线程安全方面的问题了.

1.2 示例

一言不合上代码!

  1. //创建ThreadLocal变量
  2. private static ThreadLocal<String> localParam = new ThreadLocal<>();
  3. @Test
  4. public void threadLocalDemo() {
  5. //创建2个线程,分别设置不同的值
  6. new Thread(() -> {
  7. localParam.set("Hello 风尘博客!");
  8. //打印当前线程本地内存中的localParam变量的值
  9. log.info("{}:{}", Thread.currentThread().getName(), localParam.get());
  10. }, "T1").start();
  11. new Thread(() -> {
  12. log.info("{}:{}", Thread.currentThread().getName(), localParam.get());
  13. }, "T2").start();
  14. }
  • 结果:
  1. ... T1:Hello 风尘博客!
  2. ... T2:null

打印结果证明,T1线程中设置的值无法在T2取出,证明变量ThreadLocal在各个线程中数据不共享。

1.3 ThreadLocalAPI

ThreadLocal定义了四个方法:

  1. get():返回此线程局部变量当前副本中的值;
  2. set(T value):将线程局部变量当前副本中的值设置为指定值;
  3. initialValue():返回此线程局部变量当前副本中的初始值;
  4. remove():移除此线程局部变量当前副本中的值。
  • set()initialValue()区别
名称 set() initialValue()
定义 为这个线程设置一个新值 该方法用于设置初始值,并且在调用get()方法时才会被触发,所以是懒加载。但是如果在get()之前进行了set()操作,这样就不会调用
区别 如果对象生成的时机不由我们控制的时候使用 set() 方式 对象初始化的时机由我们控制的时候使用initialValue() 方式

二、实现原理

ThreadLocal有一个特别重要的静态内部类ThreadLocalMap,该类才是实现线程隔离机制的关键。

  • 每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面,也就是说:ThreadLocal类型的本地变量存放在具体的线程内存空间中。
  1. ThreadLocal.ThreadLocalMap threadLocals = null;
  2. ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  • Thread类中有两个ThreadLocalMap类型的变量,分别是threadLocalsinheritableThreadLocals,而ThreadLocalMap是一个定制化的Hashmap,专门用来存储线程本地变量。在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocalset()或者get()方法时才会创建它们。

  • ThreadLocal就是一个工具壳,它通过set()方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get()方法时,再从当前线程的threadLocals变量里面将其拿出来使用。

  • 如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的threadLocals变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal变量的remove()方法,从当前线程的threadLocals里面删除该本地变量。

另外Thread里面的threadLocals被设计为Map结构是因为每个线程可以关联多个ThreadLocal变量。

原理小结

  1. 每个Thread维护着一个ThreadLocalMap的引用;
  2. ThreadLocalMapThreadLocal的内部类,用Entry来进行存储;
  3. 调用ThreadLocalset()方法时,实际上就是往ThreadLocalMap设置值,keyThreadLocal对象,值是传递进来的对象;
  4. 调用ThreadLocalget()方法时,实际上就是往ThreadLocalMap获取值,keyThreadLocal对象;
  5. ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value

三、使用场景

3.1 ThreadLocal的作用

  • 保存线程上下文信息,在任意需要的地方可以获取.

由于ThreadLocal的特性,同一线程在某地方进行设置,在随后的任意地方都可以获取到。从而可以用来保存线程上下文信息。

  • 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失.

3.2 场景一:独享对象

每个线程需要一个独享对象(通常是工具类,典型需要使用的类有SimpleDateFormatRandom

这类场景阿里规范里面也提到了:

3.3 场景二:当前信息需要被线程内的所有方法共享

每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。

演示(完整演示见文末Github

  • User.java
  1. @Data
  2. public class User {
  3. private String userName;
  4. public User() {
  5. }
  6. public User(String userName) {
  7. this.userName = userName;
  8. }
  9. }
  • UserContextHolder.java
  1. public class UserContextHolder {
  2. public static ThreadLocal<User> holder = new ThreadLocal<>();
  3. }
  • Service1.java
  1. public class Service1 {
  2. public void process() {
  3. User user = new User("Van");
  4. //将User对象存储到 holder 中
  5. UserContextHolder.holder.set(user);
  6. new Service2().process();
  7. }
  8. }
  • Service2.java
  1. public class Service2 {
  2. public void process() {
  3. User user = UserContextHolder.holder.get();
  4. System.out.println("Service2拿到用户名: " + user.getUserName());
  5. new Service3().process();
  6. }
  7. }
  • Service3.java
  1. public class Service3 {
  2. public void process() {
  3. User user = UserContextHolder.holder.get();
  4. System.out.println("Service3拿到用户名: " + user.getUserName());
  5. }
  6. }
  • 测试方法
  1. @Test
  2. public void threadForParams() {
  3. new Service1().process();
  4. }
  • 结果打印
  1. Service2拿到用户名: Van
  2. Service3拿到用户名: Van

3.4 使用ThreadLocal的好处

  1. 达到线程安全的目的;
  2. 不需要加锁,执行效率高;
  3. 更加节省内存,节省开销;
  4. 免去传参的繁琐,降低代码耦合度。

四、问题

4.1 内存泄漏问题

内存泄露:某个对象不会再被使用,但是该对象的内存却无法被收回

  • 正常情况

Thread运行结束后,ThreadLocal中的value会被回收,因为没有任何强引用了。

  • 非正常情况

Thread一直在运行始终不结束,强引用就不会被回收,存在以下调用链

  1. Thread-->ThreadLocalMap-->Entry(keynull)-->value

因为调用链中的 valueThread 存在强引用,所以value无法被回收,就有可能出现OOM

如何避免内存泄漏(阿里规范)

调用remove()方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal后,要调用remove()方法。

4.2 ThreadLocal的空指针问题

  • ThreadLocalNPE.java
  1. public class ThreadLocalNPE {
  2. ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
  3. public void set() {
  4. longThreadLocal.set(Thread.currentThread().getId());
  5. }
  6. /**
  7. * 当前返回值为基本类型,会报空指针异常,如果改成包装类型Long就不会出错
  8. * @return
  9. */
  10. public long get() {
  11. return longThreadLocal.get();
  12. }
  13. }
  • 空指针测试
  1. @Test
  2. public void threadLocalNPE() {
  3. ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
  4. //如果get方法返回值为基本类型,则会报空指针异常,如果是包装类型就不会出错
  5. System.out.println(threadLocalNPE.get());
  6. }

如果get()方法返回值为基本类型,则会报空指针异常;如果是包装类型就不会出错。这是因为基本类型和包装类型存在装箱和拆箱的关系,所以,我们必须将get()方法返回值使用包装类型。

4.3 参考文章

  1. 再也不学Threadlocal了,看这一篇就忘不掉了(万字总结)
  2. 使用 ThreadLocal 一次解决老大难问题

四、技术交流

Github 示例代码

  1. 风尘博客:https://www.dustyblog.cn
  2. 风尘博客-掘金
  3. 风尘博客-博客园
  4. Github

ThreadLocal = 本地线程?的更多相关文章

  1. ThreadLocal本地线程变量的理解

     一般的Web应用划分为展现层.服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用.在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程.       ...

  2. Java 类 ThreadLocal 本地线程变量

    前言:工作中将要使用ThreadLocal,先学习总结一波.有不对的地方欢迎评论指出. 定义 ThreadLocal并不是一个Thread,而是Thread的局部变量.这些变量不同于它们的普通对应物, ...

  3. Flask中的ThreadLocal本地线程,上下文管理

    先说一下和flask没有关系的: 我们都知道线程是由进程创建出来的,CPU实际执行的也是线程,那么线程其实是没有自己独有的内存空间的,所有的线程共享进程的资源和空间,共享就会有冲突,对于多线程对同一块 ...

  4. Java Concurrency - ThreadLocal, 本地线程变量

    共享数据是多线程应用最常见的问题之一,但有时我们需要为每个线程保存一份独立的变量.Java API 提供了 ThreadLocal 来解决这个问题. 一个 ThreadLocal 作用的例子: imp ...

  5. 本地线程-ThreadLocal

    线程本地存储是一个自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储.简单来说,就是对于某个变量,针对不同的线程存储不同的值. 实例: import java.util.Random; i ...

  6. Filter(过滤器)、ThreadLocal(本地线程)、Listener(监听器)

    Filter(过滤器) Filter过滤器它的作用是:拦截请求,过滤响应. 过滤器链 1)执行的顺序依次是: A B C Demo03 C2 B2 A2 2)如果采取的是注解的方式进行配置,那么过滤器 ...

  7. ThreadLocal 实现线程内共享变量

    package com.cn.gbx; import java.util.Date; import java.util.Random; import java.util.Timer; import j ...

  8. 3、flask之基于DBUtils实现数据库连接池、本地线程、上下文

    本篇导航: 数据库连接池 本地线程 上下文管理 面向对象部分知识点解析 1.子类继承父类__init__的三种方式 class Dog(Animal): #子类 派生类 def __init__(se ...

  9. flask之基于DBUtils实现数据库连接池、本地线程、上下文

    本篇导航: 数据库连接池 本地线程 上下文管理 面向对象部分知识点解析 1.子类继承父类__init__的三种方式 class Dog(Animal): #子类 派生类 def __init__(se ...

随机推荐

  1. Codeforces Round #174 (Div. 1 + Div. 2)

    A. Cows and Primitive Roots 暴力. B. Cows and Poker Game 模拟. C. Cows and Sequence 线段树维护. D. Cow Progra ...

  2. laravel的Eloquent关联关系

    1.简介: 1>Eloquent 关联关系以Eloquent模型类方法的形式被定义(是模型类的一个方法). 2>同 Eloquent 模型本身一样,关联关系也是强大的查询构建器,定义关联关 ...

  3. H3C RIP路由表的初始化

  4. 【js】 vue 2.5.1 源码学习(一) 大体结构 (自写版本,非源码)

    一.整体思路 1. 首先我们需要解析data,并且data里面的属性添加为vue的属性,并且拿到属性值 . 通过 原型方法 _peoxy实现     Obsever(代理函数) ==> walk ...

  5. ES6,ES7重点介绍

    1. 字符串模板 <!--旧版拼接字符串--> var str = '我是时间:'+new Date(); <!--新版拼接字符串--> let str = `我是时间${ne ...

  6. HashMap深入理解

    Map 的实现类有 HashMap.LinkedHashMap.TreeMap.IdentityHashMap.WeakHashMap.Hashtable.Properties 等等. 关于 Hash ...

  7. TransactionDefinition接口中定义了七个事务传播行为

    1.PROPAGATION_REQUIRED如果存在一个事务,则支持当前事务,如果没有事务则开启一个新的事务.使用spring声明式事务,spring使用AOP来支持声明式事务,会根据事务属性,自动在 ...

  8. 2018-2-13-win10-uwp-判断设备类型

    title author date CreateTime categories win10 uwp 判断设备类型 lindexi 2018-2-13 17:23:3 +0800 2018-2-13 1 ...

  9. 【15.93%】【codeforces 672D】Robin Hood

    time limit per test1 second memory limit per test256 megabytes inputstandard input outputstandard ou ...

  10. Qt3升至Qt4需要注意的几件事项浅谈

    Qt3升至Qt4需要注意的几件事项浅谈 公司以前的项目是用Qt3写的,随着时间的推移慢慢显示出Qt3有多方面的限制,因此先公司决定用Qt4来改写这个项目,并为软件添加新功能,在此背景先编写此文章. 先 ...