ThreadLocal = 本地线程?
一、定义
ThreadLocal
是JDK
包提供的,从名字来看,ThreadLocal
意思就是本地线程的意思。
1.1 是什么?
要想知道他是个啥,我们看看ThreadLocal
的源码(基于JDK 1.8
)中对这个类的介绍:
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).
大致能够总结出:
TreadLocal
可以给我们提供一个线程内的局部变量,而且这个变量与一般的变量还不同,它是每个线程独有的,与其他线程互不干扰的;ThreadLocal
与普通变量的区别在于:每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal
变量通常被private static
修饰。当一个线程结束时,它所使用的所有ThreadLocal
相对的实例副本都会被回收;- 简单说
ThreadLocal
就是一种以空间换时间的做法,在每个Thread
里面维护了一个ThreadLocal.ThreadLocalMap
,把数据进行隔离,每个线程的数据不共享,自然就没有线程安全方面的问题了.
1.2 示例
一言不合上代码!
//创建ThreadLocal变量
private static ThreadLocal<String> localParam = new ThreadLocal<>();
@Test
public void threadLocalDemo() {
//创建2个线程,分别设置不同的值
new Thread(() -> {
localParam.set("Hello 风尘博客!");
//打印当前线程本地内存中的localParam变量的值
log.info("{}:{}", Thread.currentThread().getName(), localParam.get());
}, "T1").start();
new Thread(() -> {
log.info("{}:{}", Thread.currentThread().getName(), localParam.get());
}, "T2").start();
}
- 结果:
... T1:Hello 风尘博客!
... T2:null
打印结果证明,T1
线程中设置的值无法在T2
取出,证明变量ThreadLocal
在各个线程中数据不共享。
1.3 ThreadLocal
的API
ThreadLocal
定义了四个方法:
get()
:返回此线程局部变量当前副本中的值;set(T value)
:将线程局部变量当前副本中的值设置为指定值;initialValue()
:返回此线程局部变量当前副本中的初始值;remove()
:移除此线程局部变量当前副本中的值。
set()
和initialValue()
区别
名称 | set() |
initialValue() |
---|---|---|
定义 | 为这个线程设置一个新值 | 该方法用于设置初始值,并且在调用get() 方法时才会被触发,所以是懒加载。但是如果在get() 之前进行了set() 操作,这样就不会调用 |
区别 | 如果对象生成的时机不由我们控制的时候使用 set() 方式 |
对象初始化的时机由我们控制的时候使用initialValue() 方式 |
二、实现原理
ThreadLocal
有一个特别重要的静态内部类ThreadLocalMap
,该类才是实现线程隔离机制的关键。
- 每个线程的本地变量不是存放在
ThreadLocal
实例里面,而是存放在调用线程的threadLocals
变量里面,也就是说:ThreadLocal
类型的本地变量存放在具体的线程内存空间中。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
Thread
类中有两个ThreadLocalMap
类型的变量,分别是threadLocals
和inheritableThreadLocals
,而ThreadLocalMap
是一个定制化的Hashmap
,专门用来存储线程本地变量。在默认情况下,每个线程中的这两个变量都为null
,只有当前线程第一次调用ThreadLocal
的set()
或者get()
方法时才会创建它们。
ThreadLocal
就是一个工具壳,它通过set()
方法把value
值放入调用线程的threadLocals
里面并存放起来,当调用线程调用它的get()
方法时,再从当前线程的threadLocals
变量里面将其拿出来使用。如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的
threadLocals
变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal
变量的remove()
方法,从当前线程的threadLocals
里面删除该本地变量。
另外Thread
里面的threadLocals
被设计为Map
结构是因为每个线程可以关联多个ThreadLocal
变量。
原理小结
- 每个
Thread
维护着一个ThreadLocalMap
的引用; ThreadLocalMap
是ThreadLocal
的内部类,用Entry
来进行存储;- 调用
ThreadLocal
的set()
方法时,实际上就是往ThreadLocalMap
设置值,key
是ThreadLocal
对象,值是传递进来的对象; - 调用
ThreadLocal
的get()
方法时,实际上就是往ThreadLocalMap
获取值,key
是ThreadLocal
对象; ThreadLocal
本身并不存储值,它只是作为一个key
来让线程从ThreadLocalMa
p获取value
。
三、使用场景
3.1 ThreadLocal
的作用
- 保存线程上下文信息,在任意需要的地方可以获取.
由于ThreadLocal
的特性,同一线程在某地方进行设置,在随后的任意地方都可以获取到。从而可以用来保存线程上下文信息。
- 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失.
3.2 场景一:独享对象
每个线程需要一个独享对象(通常是工具类,典型需要使用的类有SimpleDateFormat
和Random
)
这类场景阿里规范里面也提到了:
3.3 场景二:当前信息需要被线程内的所有方法共享
每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。
演示(完整演示见文末Github)
User.java
@Data
public class User {
private String userName;
public User() {
}
public User(String userName) {
this.userName = userName;
}
}
UserContextHolder.java
public class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
Service1.java
public class Service1 {
public void process() {
User user = new User("Van");
//将User对象存储到 holder 中
UserContextHolder.holder.set(user);
new Service2().process();
}
}
Service2.java
public class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service2拿到用户名: " + user.getUserName());
new Service3().process();
}
}
Service3.java
public class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户名: " + user.getUserName());
}
}
- 测试方法
@Test
public void threadForParams() {
new Service1().process();
}
- 结果打印
Service2拿到用户名: Van
Service3拿到用户名: Van
3.4 使用ThreadLocal
的好处
- 达到线程安全的目的;
- 不需要加锁,执行效率高;
- 更加节省内存,节省开销;
- 免去传参的繁琐,降低代码耦合度。
四、问题
4.1 内存泄漏问题
内存泄露:某个对象不会再被使用,但是该对象的内存却无法被收回
- 正常情况
当Thread
运行结束后,ThreadLocal
中的value
会被回收,因为没有任何强引用了。
- 非正常情况
当Thread
一直在运行始终不结束,强引用就不会被回收,存在以下调用链
Thread-->ThreadLocalMap-->Entry(key为null)-->value
因为调用链中的 value
和 Thread
存在强引用,所以value
无法被回收,就有可能出现OOM
。
如何避免内存泄漏(阿里规范)
调用remove()
方法,就会删除对应的Entry
对象,可以避免内存泄漏,所以使用完ThreadLocal
后,要调用remove()
方法。
4.2 ThreadLocal
的空指针问题
ThreadLocalNPE.java
public class ThreadLocalNPE {
ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
public void set() {
longThreadLocal.set(Thread.currentThread().getId());
}
/**
* 当前返回值为基本类型,会报空指针异常,如果改成包装类型Long就不会出错
* @return
*/
public long get() {
return longThreadLocal.get();
}
}
- 空指针测试
@Test
public void threadLocalNPE() {
ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
//如果get方法返回值为基本类型,则会报空指针异常,如果是包装类型就不会出错
System.out.println(threadLocalNPE.get());
}
如果get()
方法返回值为基本类型,则会报空指针异常;如果是包装类型就不会出错。这是因为基本类型和包装类型存在装箱和拆箱的关系,所以,我们必须将get()
方法返回值使用包装类型。
4.3 参考文章
四、技术交流
ThreadLocal = 本地线程?的更多相关文章
- ThreadLocal本地线程变量的理解
一般的Web应用划分为展现层.服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用.在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程. ...
- Java 类 ThreadLocal 本地线程变量
前言:工作中将要使用ThreadLocal,先学习总结一波.有不对的地方欢迎评论指出. 定义 ThreadLocal并不是一个Thread,而是Thread的局部变量.这些变量不同于它们的普通对应物, ...
- Flask中的ThreadLocal本地线程,上下文管理
先说一下和flask没有关系的: 我们都知道线程是由进程创建出来的,CPU实际执行的也是线程,那么线程其实是没有自己独有的内存空间的,所有的线程共享进程的资源和空间,共享就会有冲突,对于多线程对同一块 ...
- Java Concurrency - ThreadLocal, 本地线程变量
共享数据是多线程应用最常见的问题之一,但有时我们需要为每个线程保存一份独立的变量.Java API 提供了 ThreadLocal 来解决这个问题. 一个 ThreadLocal 作用的例子: imp ...
- 本地线程-ThreadLocal
线程本地存储是一个自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储.简单来说,就是对于某个变量,针对不同的线程存储不同的值. 实例: import java.util.Random; i ...
- Filter(过滤器)、ThreadLocal(本地线程)、Listener(监听器)
Filter(过滤器) Filter过滤器它的作用是:拦截请求,过滤响应. 过滤器链 1)执行的顺序依次是: A B C Demo03 C2 B2 A2 2)如果采取的是注解的方式进行配置,那么过滤器 ...
- ThreadLocal 实现线程内共享变量
package com.cn.gbx; import java.util.Date; import java.util.Random; import java.util.Timer; import j ...
- 3、flask之基于DBUtils实现数据库连接池、本地线程、上下文
本篇导航: 数据库连接池 本地线程 上下文管理 面向对象部分知识点解析 1.子类继承父类__init__的三种方式 class Dog(Animal): #子类 派生类 def __init__(se ...
- flask之基于DBUtils实现数据库连接池、本地线程、上下文
本篇导航: 数据库连接池 本地线程 上下文管理 面向对象部分知识点解析 1.子类继承父类__init__的三种方式 class Dog(Animal): #子类 派生类 def __init__(se ...
随机推荐
- H3C 帧中继地址映射
- Spring Boot Thymeleaf 使用详解
在上篇文章Spring Boot (二):Web 综合开发中简单介绍了一下 Thymeleaf,这篇文章将更加全面详细的介绍 Thymeleaf 的使用.Thymeleaf 是新一代的模板引擎,在 S ...
- 2019-8-2-WPF-从文件加载字体
title author date CreateTime categories WPF 从文件加载字体 lindexi 2019-08-02 17:10:33 +0800 2018-2-13 17:2 ...
- P1098 方程解的个数
题目描述 给出一个正整数N,请你求出x+y+z=N这个方程的正整数解的组数(1<=x<=y<=z<1000).其中,1<=x<=y<=z<=N . 输入 ...
- TCP和UDP的联系和用途
一.区别 二者都是有用的和常用的,如果纯粹从概念上区分二者就比较费解了,我们直接从功能上进行区分,简单明了: 这两种传输协议也就是合于适配不同的业务和不同的硬件终端. ...
- CCPC2018 桂林 G "Greatest Common Divisor"(数学)
UPC备战省赛组队训练赛第十七场 with zyd,mxl G: Greatest Common Divisor 题目描述 There is an array of length n, contain ...
- ASP.NET MVC4.0+EF+LINQ+bui+bootstrap+网站+角色权限管理系统(4)
接下来就是菜单管理了,菜单分为两部分,一部分是菜单管理,另一部分是左边的树形菜单 数据库添加菜单表Menus USE [MVCSystem] GO /****** Object: Table [dbo ...
- 【41.34%】【BZOJ 1003】[ZJOI2006]物流运输
Time Limit: 10 Sec Memory Limit: 162 MB Submit: 6420 Solved: 2654 [Submit][Status][Discuss] Descri ...
- Python之end关键字使用
关键字end可以用于将结果输出到同一行,或者在输出的末尾添加不同的字符,实例如下: a, b = 0, 1 while b < 1000: print(b, end=',') a, b = b, ...
- 前端vue——阿里图标的使用方法
阿里图标库的官方网址:https://www.iconfont.cn/ 使用前需要先登录,这里有三种登录方式,本人使用的是新浪微博登录 第一步:找到你需要的图标,点击添加入库 第二步:点击右上角的购物 ...