浅聊 【ThreadLocal】(超级详细)
写在开始 :
本文主要讲述 : ThreadLocal简介; 常用API; demo案例; 特点引用场景;以及部分底层原理源码内容。
引言 :
从常见面试题看 ThreadLocal:
**①解释 **: ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分
配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal 同时
实现了线程内的资源共享;
②案例:使用JDBC操作数据库时,会将每一个线程的Connection放入各自的
ThreadLocal中,从而保证每个线程都在各自的 Connection 上进行数据库的操
作,避免A线程关闭了B线程的连接。
public class ThreadLocalTest {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
String name = Thread.currentThread().getName();
threadLocal.set("itcast"); // set(v)方法 : 设置值
print(name);
System.out.println(name + "-after remove : " +
threadLocal.get()); // get()方法:获取值
}, "t1").start();
new Thread(() -> {
String name = Thread.currentThread().getName();
threadLocal.set("itheima");
print(name);
System.out.println(name + "-after remove : " +
threadLocal.get());
}, "t2").start();
}
static void print(String str) {
//打印当前线程中本地内存中本地变量的值
System.out.println(str + " :" + threadLocal.get());
//清除本地内存中的本地变量
threadLocal.remove(); // remove()方法 清除值
}
}
③源码
ThreadLocal本质来说就是一个线程内部存储类,从而让多个线程只操作自己内
部的值,从而实现线程数据隔离;
在ThreadLocal中有一个内部类叫做ThreadLocalMap,类似于HashMap
ThreadLocalMap中有一个属性table数组,这个是真正存储数据的位置
Ⅰ:set方法:
Ⅱ:get/remove方法
内存泄漏问题
每一个Thread维护一个ThreadLocalMap,在ThreadLocalMap中的Entry对象继承
了WeakReference。其中key为使用弱引用的ThreadLocal实例,value为线程变量
的副本;
引用类型 : ①弱引用:内存不太够时候优先回收;②强引用:不会被回收
使用 ThreadLocal 时候 ,强烈建议:务必手动remove。
正文第一部分–编程实战
1.ThreadLocal 简介
ThreadLocal 见名知意 : 线程本地 或者 本次线程;
但是从它实际作用而言, 可能解释为 【线程局部变量】or 【线程本地变量】更为合适。
2.ThreadLocal作用
ThreadLocal是java.lang包中的一个泛型类,可以实现为线程创建独有变量,这个变量对于其他线程是隔离的,也就是线程本地的值,这也是ThreadLocal名字的来源;
package java.lang
public class ThreadLocal<T>{}
每个使用该变量的线程都要初始化一个完全独立的实例副本(也就是变量的本地拷贝),不存在多线程间共享问题
3.ThreadLocal的 API 方法
ThreadLocal用来存储当前线程的独有数据,相关API就是存值,取值,清空值的简单操作
- withInitial:创建一个ThreadLocal实例,并给定初始值【JDK8推出的新方法,一般都是用该方法初始化ThreadLocal】
- get:返回当前线程ThreadLocal的值,如果没有设置值返回null
- set:设置当前线程ThreadLocal的值
- remove:删除当前线程ThreadLocal的值
- initialValue:此方法默认返回null, 通过ThreadLocal构造方法初始化时一般重写此方法,来设置初始值,在JDK8之后通过withInitial方法初始化
4.初始化ThreadLocal
初始化ThreadLocal有三种方式:
- 直接通过构造方法创建,此时初始值为null
- 通过构造方法同时重写initialValue方法给定初始值
- 通过JDK8的withInitial()静态方法创建,可以通过Lambda直接给初始值【推荐使用】
①构造方法
@Test
public void test1() {
// 通过构造方法,初始化ThreadLocal
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
// 未给定初始值,通过get方法获取值为null
System.out.println(threadLocal.get());
}
@Test
public void test2() {
// 初始化ThreadLocal
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
System.out.println("设置前 -->" + threadLocal.get()); // null
// 通过 set 方法,设置初始值
threadLocal.set(1);
System.out.println("设置后 -->" + threadLocal.get()); // 1
}
②重写initialValue方法
@Test
public void test3() {
// 初始化ThreadLocal,重写initialValue方法设置默认值
ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
// 设置,初始值为1024
return 1024;
}
};
// 启动5个线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":threadLocal初始值 -->" + threadLocal.get());
},"线程:" + i).start();
}
}
打印结果 : 5个线程每个线程的初始值都为1024
// 返回当前线程ThreadLocal的初始值,但是在ThreadLocal中默认实现返回为null
// 这个值和具体的泛型类型有关,通常需要根据实际需求重写此方法,定义初始值
protected T initialValue(){
return null;
}
③withInitial静态方法
// JDK8中新增了withInitial静态方法接收Supplier供给型函数接口设置初始值
@Test
public void test4() {
// 通过withInitial方法设置初始值
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> {
return 100;
});
// 启动5个线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":threadLocal初始值 -->" + threadLocal.get());
},"线程:" + i).start();
}
}
打印结果 :
启动5个线程之后初始值为100,
强烈推荐此种方式来实现ThreadLocal的初始化
推荐通过withInitial静态方法实现ThreadLocal的初始化
如果线程内需要修改值则可以使用set方法,如果需要获取值则使用get方法
ThreadLocal数据存储:一个ThreadLocal实例,在每个线程中都有独自的初始化副本,接下来每个线程对ThradLocal的操作都在线程内,对其他线程隔离
5.demo案例
背景说明:
@Test
public void demo() {
// 通过withInitial方法设置初始账户余额为0
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
// 设置随机数
Random random = new Random();
// 通过map集合映射线程名字
Map<Integer, String> map = new HashMap<>();
map.put(0,"账户一");
map.put(1,"账户二");
map.put(2,"账户三");
// 启动3个线程,分别为3个账户
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 去存钱5次
for (int j = 0; j < 5; j++) {
// 每次随机,金额在200以内
int money = random.nextInt(200);
// 金额加1
threadLocal.set(money + threadLocal.get());
}
System.out.println(Thread.currentThread().getName() + "共存入:" + threadLocal.get() + "元的钱");
},map.get(i)).start();
}
}
三个账户各自存款五次,金额实现单独记录
打印结果 : 账户一共存入:499元; 账户二共存入:528元; 账户三共存入:799元;
线程池实现
实际开发对于多线程的场景都推荐使用线程池实现,可以避免线程频繁创建和销毁的资源浪费,使用线程池是一定要记得在finally代码块中关闭线程池
@Test
public void test6() {
// 通过withInitial方法设置初始值
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
// 设置随机数
Random random = new Random();
Map<Integer, String> map = new HashMap<>();
map.put(0,"账户一");
map.put(1,"账户二");
map.put(2,"账户三");
// 开启有3个核心线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
try {
// 3个账户
for (int i = 0; i < 3; i++) {
// 记录当前循环值,映射对应的账户
int index = i;
threadPool.submit(() -> {
// 设置线程名:Thread.currentThread().getName()为默认的线程池给的名字,方便查看是哪一个线程执行的此任务
Thread.currentThread().setName(Thread.currentThread().getName() + map.get(index));
// 存钱,比如5次
for (int j = 0; j < 5; j++) {
// 每家随机,金额200以内
int money = random.nextInt(200);
// 金额加1
threadLocal.set(money + threadLocal.get());
}
System.out.println(Thread.currentThread().getName() + "共:" + threadLocal.get() + "元");
});
}
}catch (Exception e) {
e.printStackTrace();
}finally {
// 关闭线程池
threadPool.shutdown();
}
}
假设运行程序的电脑为8核,可以通过线程池中的3个核心线程,同时执行3条任务,此时的结果没有任何问题
打印结果:
pool-1-thread - 1 账户一共:477元
pool-1-thread - 3 账户三共:777元
pool-1-thread - 2 账户二共:557元
线程池中线程可复用引发的问题
如果此时又多出来一个账户,或者核心线程数变为2,即任务数大于核心线程数,会复用线程处理其他任务,即一个线程需要处理多个任务,这里减少核心线程数为例演示:
@Test
public void test6() {
......
// 修改线程池核心线程数为2,其他不变
ExecutorService threadPool = Executors.newFixedThreadPool(2);
......
}
**原因在于:**1号线程处理完账户一之后,发现账户二任务没有执行,此时1号线程处理两个任务,ThreadLocal也还是同一个,即处理账户二任务时,ThreadLocal的值为账户一任务处理后的值,并不是初始值0;
在阿里Java开发规范中强制要求回收自定义的ThreadLocal变量
通过try块将任务逻辑包裹,在finally中通过remove方法回收该任务执行后的ThreadLocal值
@Test
public void test7() {
......
try {
......
try {
for (int j = 0; j < 5; j++) {
// 随机的200以内
int money = random.nextInt(200);
// 金额加1
threadLocal.set(money + threadLocal.get());
}
System.out.println(Thread.currentThread().getName() + "共:" + threadLocal.get() + "元");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 回收数据
threadLocal.remove();
}
}catch (Exception e) {
e.printStackTrace();
}finally {
// 关闭线程池
threadPool.shutdown();
}
}
回收ThreadLocal变量后,复用该线程也不会对后续程序造成影响
6.ThreadLocal特点
- 统一设置初始值,每个线程可以通过set方法设置值,也可以通过get方法获取当前值
- ThreadLocal被每一个线程单独持有副本,相互独立,只能在该线程内部使用
- 如果配合线程池使用,线程可复用,需要调用remove方法回收数据,即重新设置为初始值,避免对后续程序造成影响和内存泄漏
- ThreadLocal变量因为线程独立,所以不在线程安全问题
7.ThreadLocal应用场景
ThreadLocal 适用于如下两种场景
- 每个线程需要自己独立的数据
- 数据在线程内的共享,不需要在多线程之间共享
举例:
- 游戏玩家个人的属性,装备,积分等
- Spring中也通过ThreadLocal解决线程安全问题,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的;
正文第二部分–底层原理
1.ThreadLocal线程安全原理
ThreadLocal存储数据是通过ThreadLocalMap实现,ThreadLocalMap底层是Entry数组,以键值对的形式存放,键是ThreadLocal对象,value是set()设置的值
- 当执行set方法时:其value是保存在threadlocals变量中的
- 当执行get方法时:就是读取threadlocals变量中的值。
- 各个线程的数据互不打扰。一个线程可以存放多个ThreadLocal变量,他们都是存在了ThreadLocalMap中
/* 记录与此线程相关的ThreadLocal值。这个map由ThreadLocal类维护 */
ThreadLocal.ThreadLocalMap threadLocals = null;
首先,在Java中的线程是一个Thread类的实例对象!而一个实例对象中实例成员字段的内容肯定是这个对象独有的,所以如果将ThreadLocal线程本地变量作为Thread类的成员字段,就可实现线程私有,在Thread类中恰巧就是这么实现的,这个成员字段就是上述代码
map是一个在ThreadLocal中通过静态内部类定义的Map对象,保存了该线程中的所有本地变量【即一个线程可以使用多个ThreadLocal,存储在指定Thread的Map中】。ThreadLocalMap中的Entry的定义如下:
static class ThreadLocalMap{ // 静态内部类
static class Entry extends WeakReference<ThreadLocal<?>>{ // 通过Entry对象维护数据,继承弱引用
Object value;
Entry(ThreadLocal<?> k,Object v){
super(k); // 弱引用,内存不够优先回收
value = v; // 强引用,不会回收
}
}
}
- 每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
- ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。
- 我们还要注意Entry, 它的key是ThreadLocal<?> k ,继承自WeakReference, 也就是我们常说的弱引用类型。
- Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap
- ThreadLocalMap是ThreadLocal类中的静态内部类,通过内部类Entry记录值,
- 在ThreadLocalMap中维护这一个Entry数组,因为一个线程可以使用多个ThreadLocal变量
- Entry将该Thread中使用的ThreadLocal当做key,将值当做value【实际上key并不是ThreadLocal本身,而是它的一个弱引用】。
- ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value
Set源码
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的threadLocals字段
ThreadLocalMap map = getMap(t);
// 判断线程的threadLocals是否初始化了
if (map != null) {
map.set(this, value);
} else {
// 没有则创建一个ThreadLocalMap对象进行初始化
createMap(t, value);
}
}
createMap方法的源码
void createMap(Thread t, T firstValue) {
// 新建一个ThreadLocalMap
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
map.set方法的源码
/**
* 往map中设置ThreadLocal的关联关系
* set中没有使用像get方法中的快速选择的方法,因为在set中创建新条目和替换旧条目的内容一样常见,
* 在替换的情况下快速路径通常会失败(对官方注释的翻译)
*/
private void set(ThreadLocal<?> key, Object value) {
// map中就是使用Entry[]数据保留所有的entry实例
Entry[] tab = table;
int len = tab.length;
// 返回下一个哈希码,哈希码的产生过程与神奇的0x61c88647的数字有关
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
// 已经存在则替换旧值
e.value = value;
return;
}
if (k == null) {
// 在设置期间清理哈希表为空的内容,保持哈希表的性质
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
// 扩容逻辑
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
get方法原理
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 获取ThreadLocal对应保留在Map中的Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 获取ThreadLocal对象对应的值
T result = (T)e.value;
return result;
}
}
// map还没有初始化时创建map对象,并设置null,同时返回null
return setInitialValue();
}
remove方法原理
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
// 键在直接移除
if (m != null) {
m.remove(this);
}
}
2.ThreadLocal设计
JDK8之后,每个Thread维护一个ThreadLocalMap对象,这个Map的key是ThreadLocal,value是存储的是泛型值,其具体过程如下:
- 每个Thread线程内部都有一个Map【ThreadLocalMap.threadlocals】;
- Map里面存储ThreadLocal对象【key】和线程的变量副本【value】;
- Thread内部Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置变量值;
- 对于不同的线程,每次获取副本值时,别的线程不能获取当前线程的副本值,就形成了数据之间的隔离。
这样设计的好处是:
- 每个Map存储的Entry的数量变少,在实际开发过程中,ThreadLocal的数量往往要少于Thread的数量,Entry的数量减少就可以减少哈希冲突;
- 当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存使用,早期的ThreadLocal并不会自动销毁。
使用ThreadLocal的好处
- 保存每个线程绑定的数据,在需要的地方可以直接获取,避免直接传递参数带来的代码耦合问题;
- 各个线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。
3.ThreadLocal内存泄露问题
**内存泄露问题:**指程序中动态分配的堆内存由于某种原因没有被释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢或者系统奔溃等严重后果。内存泄露堆积将会导致内存溢出。
ThreadLocal的内存泄露问题一般考虑和Entry对象有关,Entry继承WeakReference也就是弱引用。**JVM会将弱引用修饰的对象在下次垃圾回收中清除掉。**这样就可以实现ThreadLocal的生命周期和线程的生命周期解绑。但实际上并不是使用了弱引用就会发生内存泄露问题,考虑下面几个过程:
强引用
当ThreadLocal Ref被回收了,由于Entry使用的是强引用,在Current Thread还存在的情况下就存在着到达Entry的引用链,无法清除掉ThreadLocal的内容,同时Entry的value也同样会被保留;也就是说就算使用了强引用仍然会出现内存泄露问题。
弱引用
当ThreadLocal Ref被回收了,由于在Entry使用的是弱引用,因此在下次垃圾回收的时候就会将ThreadLocal对象清除,这个时候Entry中的KEY=null。但是由于ThreadLocalMap中任然存在Current Thread Ref这个强引用,因此Entry中value的值任然无法清除。还是存在内存泄露的问题。
综上所述,使用ThreadLocal造成内存泄露的问题是因为:ThreadLocalMap的生命周期与Thread一致,如果不手动清除掉Entry对象的话就可能会造成内存泄露问题。因此,需要我们在每次在使用完之后需要手动的remove掉Entry对象。
避免内存泄露的两种方式:使用完ThreadLocal,调用其remove方法删除对应的Entry或者使用完ThreadLocal,当前Thread也随之运行结束。第二种方法在使用线程池技术时是不可以实现的。
所以一般都是自己手动调用remove方法,调用remove方法弱引用和强引用都不会产生内存泄露问题,使用弱引用的原因如下:
在ThreadLocalMap的set/getEntry中,会对key进行判断,如果key为null,那么value也会被设置为null,这样即使在忘记调用了remove方法,当ThreadLocal被销毁时,对应value的内容也会被清空。多一层保障!
:::info
TIP:
存在内存泄露的有两个地方:ThreadLocal和Entry中Value;最保险还是要注意要自己及时调用remove方法!!
:::
补充面试题部分
问题一:实际开发有用过ThreadLocal吗?
举例1 : 用来做用户信息上下文存储;
当系统应用是MVC架构的背景,用户每次登录访问接口,都会在请求头携带一个 token,控制层可以根据这个 token ,解析用户基本信息,那么在服务层,持久层也要用户信息,比如 rpc调用,更新用户获取等等,就可以采取如下两种方式:
方式一: 显式定义用户相关参数,比如账户,用户名等等,但是这样会大面积修改代码。这时候用 ThreadLocal ,控制层拦截请求把用户信息存入ThreadLocal,就能在需要的地方,取出ThreadLocal里存的用户数据;
其它有些场景的 cookie session 等数据隔离也可以通过 ThreadLocal 去实现;
之前引言案例举例的数据库连接池也用到 ThreadLcoal:
DB连接池的连接交给ThreadLocal进行管理,保证当前线程的操作都是一个Connection;
问题二:谈谈你对ThreadLocal的理解?
ThreadLocal 主要功能有两个,第一个是可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题,第二个是实现 了线程内的资源共享
问题三:ThreadLocal的底层原理实现?
①在ThreadLocal内部维护了一个一个 ThreadLocalMap 类型的成员变量,用来 存储资源对象
②当我们调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
③当调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联 的资源值
④当调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联 的资源值
问题四:ThreadLocal内存溢出?
- 因为ThreadLocalMap 中的 key 被设计为弱引用,它是被动的被GC调用释 放key,不过关键的是只有key可以得到内存释放,而value不会,因为value 是一个强引用。
- 在使用ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依 靠 GC 回收,建议主动的remove 释放 key,这样就能避免内存溢出。
小结
:::info
- ThreadLocal更像是对其他类型变量的一层包装,通过ThreadLocal的包装使得该变量可以在线程之间隔离和当前线程全局共享。
- 线程的隔离性和变量的线程全局共享性得益于在每个Thread类中的threadlocals字段。(从类实例对象的角度抽象的去看Java中的线程!!!)
- ThreadLocalMap中Entry的Key不管是否使用弱引用都有内存泄露的可能。引起内存泄露主要在于ThreadLocal对象和Entry中的Value对象,因此要确保每次使用完之后都remove掉Entry!
:::
写在最后 : 码字不易,如果觉得还不错,或者对您有帮助,麻烦动动小手,点赞或关注,谢谢! 祝大家周末愉快!