你的ThreadLocal线程安全么
想必很多小伙伴们对ThreadLocal
并不陌生,ThreadLocal
叫做线程本地变量,也就是ThreadLocal为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部的副本变量。那么,我们使用ThreadLocal一定线程安全么?话不多说,先上结论:
如果threadlocal.get之后的副本,只在当前线程中使用,那么是线程安全的;如果对其他线程暴露,不一定是线程安全的。
为了演示下错误的使用方式,先看下如下代码(虽然小伙伴们都不会这样写代码 ^_^):
static class Container {
int num;
}
public static void main(String[] args) throws InterruptedException {
ThreadLocal<Container> tl = new ThreadLocal<>();
tl.set(new Container()); // 先set下ThreadLocal
Container container = tl.get();
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
container.num++;
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(tl.get().num);
}
笔者的一次结果输出为:17581
结合代码,我们知道,在执行threadlcoal.get
获取到线程变量副本之后,不要让其他线程来访问它了,否则就是多线程操作同一个变量,可能造成线程安全问题。
除了上述讨论的ThreadLocal线程安全性问题之外,ThreadLocal如果使用不当,可能存在内存泄露问题。ThreadLocal变量是保存在Thread.threadLocals
中(ThreadLocalMap类型)以Entry类型保存的,其中Entry.key(也就是弱引用referent实际指向对象)为ThreadLocal变量,该变量为弱类型;Entry.value为实际set的value。
// Entry,里面保存在ThreadLocal变量,也就是key,是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
虽然Entry.referent是弱类型,指向ThreadLocal变量,但是如果ThreadLocal变量本身引用不置为null的话,这里的Entry.referent指向对象是不会释放的。比如我们常用的定义方式:
// 静态变量和对象属性
static ThreadLocal<String> tls = new ThreadLocal<>();
ThreadLocal<Integer> tli = new ThreadLocal<>();
类似于静态变量和对象属性这种引用,如果不将tls或tli设置为null,那么ThreadLocal变量无法释放(这不是废话么,人家可是强引用呀),此时的Entry.referent弱类型没啥卵用;只有在tls或tli为null时,Entry.referent弱类型就起作用了,在第一次GC时就会将Entry.referent弱类型指向的对象回收。
如果Entry.referent弱类型指向的对象回收了(没调用ThreadLocal.remove操作),Entry.value对象还在,并且Entry.value可是强引用的,此时就发生了内存泄露。这也就是ThreadLocal使用不当(没调用ThreadLocal.remove)时产生的内存泄漏问题。不过,伴随着其他ThreadLocal对象的set/get/remove
的进行,会清除一部分Entry.referent为null但是Entry.value不为null的对象的,也就是修复内存泄露问题,注意,这个只是清除部分这样的Entry,并不能保证一次就能清除全部这样的Entry,所以还是要遵循ThreadLocal.set,用完之后就remove。
讨论完了ThreadLocal的潜在问题之后,你是不是意犹未尽,想深入了解下ThreadLocal实现原理
?OK,那就搬起小板凳,一起唠唠吧~
ps:如果小伙伴对ThreadLocal原理已经熟悉了,那么恭喜你,后面的内容可以不看了~
ThreadLocal实现原理
ThreadLocal变量主要有get/set/remove
三个操作,理解了这三个操作流程,基本上就理解了ThreadLocal实现原理。
get
get流程如下:
- 获取当前线程的threadLocals(map结构),从threadLocals中获取当前ThreadLocal变量对应的ThreadLocalMap.Entry(pair类型,包含了当前ThreadLocal变量及其对应的value),非空直接返回对应的value
- 为空时使用默认值(默认为null)构造ThreadLocalMap.Entry,放到当前线程的threadLocals中,下次再get时直接返回ThreadLocalMap.Entry对应的value即可
/**
* 当前线程的threadLocalMap中获取当前ThreadLocal对应的value
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 设置null值,下次直接返回null了
return setInitialValue();
}
/**
* 如果一次找到了entry,直接返回;否则就是set时hash冲突了
* 遍历后续的slot,进行查找
* 这里其实JDK可以做个优化,在set之后,将slot位置记录在Threadlocal变量中,下次直接到对应slot位置get即可
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
注意:线程的threadLocals是一个基于开放定址法实现的map结构。
set
- set操作就是将ThreadLocal变量的值put到当前线程的threadLocals中,ThreadLocal变量及其对应的值会构造成一个ThreadLocalMap.Entry放到threadLocals中。
- 因为线程的threadLocals是一个基于开放定址法实现的map结构,所以在出现hash冲突后会继续寻找下一个空位进行set操作。
- 因为是基于开放定址法,如果map中元素过多,会影响get和put性能,所以需要扩容,map的数组结构默认大小为
INITIAL_CAPACITY = 16
,默认扩容阈值为threshold = INITIAL_CAPACITY * 2 / 3
,扩容时按照成倍扩容。
/**
* 获取当前线程的threadLocalMap,非空直接set value;
* 否则新建一个包含value的threadLocalMap。
* threadLocalMap的key对应程序中定义的ThreadLocal变量,value对应要set的值
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // Thread.threadLocals
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// Entry,里面保存在ThreadLocal变量,也就是key,是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* hash码的生成,这里所有的ThreadLocal对象hash生成都是基于static变量nextHashCode来做的
* 创建ThreadLocal对象时threadLocalHashCode已初始化完成
*/
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* 当前线程的threadLocalMap非空直接set value
*/
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 如果当前table[i] hash冲突,那么就以i为起点,遍历后续table[i],
// 这其实就是hash冲突中的开放定址法,另外一种是分离链接法
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// key已存在,更新vlaue即可
if (k == key) {
e.value = value;
return;
}
// key为null,复制value即可
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 新建Entry,清理一部分Entry.key为null,value不为null的数据,避免内存泄露
// 超过了threshold时rehash操作
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
remove
/**
* 从ThreadLocalMap删除对应key
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 清除Entry.key弱引用,设置为null
e.clear();
// 清除Entry.value引用,可能还涉及部分key为null的Entry数据清理
expungeStaleEntry(i);
return;
}
}
}
小结
从ThreadLocal的get/set
操作流程来看,ThreadLocal的value 是 Lazy Init(延迟初始化的)
。ThreadLocal为什么是延迟初始化,这个问题应该是容易理解的,原因是:在没有具体业务场景前提下,这样的做法避免内存浪费。
ThreadLocal变量默认放在基于开放定址法实现的map结构中,这种结构在hash冲突时会造成多次get/set
操作,理论上可以通过记录ThreadLocal变量set时的位置,这样下次直接通过该位置获取对应value即可,可以参考netty的FastThreadLocal
,它的实现思路就是这样的,提高了set/get的效率。
最后来一张ThreadLocal的整体图:
参考资料:
1、https://luoxn28.github.io/2019/04/27/ni-de-threadlocal-yi-ding-xian-cheng-an-quan-ma/
你的ThreadLocal线程安全么的更多相关文章
- 【java】ThreadLocal线程变量的实现原理和使用场景
一.ThreadLocal线程变量的实现原理 1.ThreadLocal核心方法有这个几个 get().set(value).remove() 2.实现原理 ThreadLocal在每个线程都会创建一 ...
- Java并发编程原理与实战二十五:ThreadLocal线程局部变量的使用和原理
1.什么是ThreadLocal ThreadLocal顾名思义是线程局部变量.这种变量和普通的变量不同,这种变量在每个线程中通过get和set方法访问, 每个线程有自己独立的变量副本.线程局部变量不 ...
- ThreadLocal线程隔离
package com.cookie.test; import java.util.concurrent.atomic.AtomicInteger; /** * author : cxq * Date ...
- ThreadLocal线程局部变量的使用
ThreadLocal: 线程局部变量 一).ThreadLocal的引入 用途:是解决多线程间并发访问的方案,不是解决数据共享的方案. 特点:每个线程提供变量的独立副本,所有的线程使用同一个Thre ...
- Threadlocal线程本地变量理解
转载:https://www.cnblogs.com/chengxiao/p/6152824.html 总结: 作用:ThreadLocal 线程本地变量,可用于分布式项目的日志追踪 用法:在切面中生 ...
- ThreadLocal线程范围内的共享变量
模拟ThreadLocal类实现:线程范围内的共享变量,每个线程只能访问他自己的,不能访问别的线程. package com.ljq.test.thread; import java.util.Has ...
- ThreadLocal线程本地变量
首先说明ThreadLocal存放的值是线程内共享的,线程间互斥的,主要用于线程内共享一些数据,避免通过参数来传递,这样处理后,能够优雅的解决一些实际问题,比如hibernate中的OpenSessi ...
- 单例模式/ThreadLocal/线程内共享数据
import java.util.Random; public class ThreadDemo3 { public static void main(String[] args) { for(int ...
- ThreadLocal 线程本地变量 及 源码分析
■ ThreadLocal 定义 ThreadLocal通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量 ...
随机推荐
- 《跟唐老师学习云网络》 -第4篇 router路咋走啊【华为云技术分享】
[摘要] 好了,到这里至少你应该能看懂路由表信息了.给你一个目的IP,你也应该知道它会使用哪一条路由了. 路怎么走就看骚年你了~ 一.路由 其实关于网络大家遇到最多的问题就是:卧 槽,为什么不通啊! ...
- LinkedHashMap 的核心就 2 点,搞清楚,也就掌握了
HashMap 有一个不足之处就是在迭代元素时与插入顺序不一致.而大多数人都喜欢按顺序做某些事情,所以,LinkedHashMap 就是针对这一点对 HashMap 进行扩展,主要新增了「两种迭代方式 ...
- C#通过字符串分割字符串Split
string[] strArr = str.Split(new[] {"****==="},StringSplitOptions.None); 更多内容关注公众号 洛水梅家
- delegate里的Invoke和BeginInvoke
Invoke和BeginInvoke都是调用委托实体的方法,前者是同步调用,即它运行在主线程上,当Invode处理时间长时,会出现阻塞的情况,而BeginInvod是异步操作,它会从新开启一个线程,所 ...
- 使用GitHub/码云/Git个性化设置
参考链接:https://www.liaoxuefeng.com/wiki/896043488029600/900937935629664 这似乎很可笑,我还从来没有想过为一个网站的使用方法写一篇来记 ...
- uni-app采坑记录
1. uni-app采坑记录 1.1. 前言 这里记录下uni-app实践中踩的坑 1.2. 坑点 1.2.1. 触发事件@longTap和@longpress 这两个都表示长按触发事件,那么这两个有 ...
- TP5.1 调用common里面自定义的常量
公共文件:\application\common.php define('cms_password', cms); 控制器引用: 调用: $aa = cms_password; dump(cms_pa ...
- windows下搭建vue+webpack的开发环境
1. 安装git其右键git bash here定位比cmd的命令行要准确,接下来的命令都是利用git bash here.2. 安装node.js一般利用vue创建项目是要搭配webpack项目构建 ...
- Mysql-修改用户连接数据库IP地址和用户名
将用户连接数据库(5.7.14-7)的IP地址从 10.10.5.16 修改为 10.11.4.197 Mysql> rename user 'username'@'10.10.5.16' ...
- sqlldr导入数据取消回显记录条数
之前在脚本中使用sqlldr导入数据时,如果表的数据量较大的话,会使日志文件变得极大,之后在网上查找了很久,才在一个偶然的机会找到这个参数 silent=all 但是最近发现这样写有个问题,就是加了这 ...