ThreadLocal原理和用法(超全面建议收藏)
一、基本概念1、底层结构:有点像HashMap,可以保存kv键值对,它实质是通过在k位置关联当前线程的,并在v的位置为当前线程绑定一套副本,ThreadLocal内部只能保存一个kv的键值对,且内部不提供遍历和查询方法,不同的线程意味着不同的空间,所以每个线程只能获取自己的副本2 用法:②set是为当前线程设置副本③get则是获取当前线程的副本④remove()用来移除当前线程中变量的副本3 作用
基本概念
ThreadLocal叫做线程变量,意思是ThreadLocal对象中填充的变量属于当前线程,通过该变量可以为当前线程设置它的独立副本,该副本对其他线程而言是隔离的,每个线程只能访问自己内部的副本
数据结构
案例代码
@Data
@Slf4j
public class ThreadDemo {
private String content;
/**
* 用于存储线程id
*/
private static ThreadLocal threadIdCache = new ThreadLocal();
/**
* 用于存储线程name
*/
private static ThreadLocal threadNameCache = new ThreadLocal();
public static void main(String[] args) throws InterruptedException {
ThreadDemo threadDemo = new ThreadDemo();
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
threadIdCache.set(Thread.currentThread().getId());
threadNameCache.set(Thread.currentThread().getName());
log.info("ThreadId:{}, ThreadName:{}", threadIdCache.get(), threadNameCache.get());
threadIdCache.get();
}
}).start();
}
}
}
打印内容
代码图解(直接看比较模糊,建议在新标签页打开图片)
温馨提示
需要案例代码 + 案例代码图解 + 阅读源码(最好自己debug下)才能真正掌握
set方法源码
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
get方法源码
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;
}
}
return setInitialValue();
}
线程私有性举例
案例一
@Data
@Slf4j
public class ThreadDemo {
private String content;
public static void main(String[] args) throws InterruptedException {
ThreadDemo threadDemo = new ThreadDemo();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
// 写threadDemo
threadDemo.setContent("线程:" + Thread.currentThread().getName() + "设置值");
// 读threadDemo
System.out.println("线程:" +Thread.currentThread().getName() + "获取值" + threadDemo.getContent());
}
}).start();
}
}
}
因为局部变量threadDemo是这五个线程所共享的变量,因为for循环执行5次的时间可近乎忽略,所有上述5个线程是并发执行的,并发读写threadDemo就会出现如下读写不一致的现象
案例二:使用threadLocal 优化后就线程安全了
public class ThreadDemo {
private static ThreadLocal<String> threadLocal = new ThreadLocal();
private String content;
public void setContent(String content) {
threadLocal.set(content);
}
public String getContent() {
return threadLocal.get();
}
public static void main(String[] args) throws InterruptedException {
ThreadDemo threadDemo = new ThreadDemo();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
threadDemo.setContent("线程:" + Thread.currentThread().getName() + "设置值");
System.out.println("线程:" +Thread.currentThread().getName() + "获取值" + threadDemo.getContent());
}
}).start();
}
}
}
虽然我们再案例一中,使用threadDemo作为线程对象锁也可以达到同样的效果,如下
但区别在于
- synchronized是用于线程间的数据共享,本质是给共享变量threadDemo 加锁来解决的,例如上述我们对相关代码块加锁后,这5个线程,只能一个个按序去访问,也就是的并行去修改共享变量threadDemo,以时间换空间并发性低;
- threadLocal 是用于线程间的数据隔离, 是通过为每一个线程提供一份线程副本来实现线程间数据隔离的,此时我们这5个线程在并发访问的都是他们自己线程副本,自然不会存在线程安全问题,通过这种机制来保证线程间数据隔离,以空间换时间并发性强
内存安全
ThreadLocal中的remove方法用于释放该threadLocal对象相关的线程的内存,只要在threadLocal使用完毕后及时释放,就不会出现线程问题,源码如下
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
继续以上面的ThreadDemo为例,做一个关于threadIdCache这个静态对象引用的内存图,绿色的虚线是重点关注的地方,意味着threadLocal类型的key是以虚引用的形式存储在threadlocalMap中的,关于弱引用、强引用的概念本文不过多叙述,但要学习如下内容是必不可少的,推荐参考文章:JAVA基础 - 强引用、弱引用、软引用、虚引用
分析Threadlocal的内存泄漏要结合两个因素,即Threadlocal与Thread的使用情况
首先分析Thread,如果thread线程是作为局部变量使用,那么thread生命周期会非常短,在线程执行完毕就会结束,thread被回收,即便我们使用完threadlocal没有remove也不会发生内存泄漏,如图
但实际开发中,我们接触的通常都是使用线程池管理的多线程环境,例如使用springboot做web开发,这种情况下线程的生命周期会伴随整个程序的运行周期
在考虑使用线程池的前提下,假如我们线程池有500个线程,Threadlocal的使用分两种情况
Threadlocal是作为局部变量
有一个方法,里面会创建一个threadlocal,并通过set为当前线程关联一个线程变量,用该线程池执行这个接口/方法100万次,其中会累计创建了100万个threadlocal对象,每个线程平均被关联了2000个线程变量
threadlocal是作为局部变量,方法结束threadlocal的(强)引用出栈,只剩线程Thread中的ThreadLocalMap中某个使用threadlocal(弱)引用作为key的Entry,所以下次GC时堆中的threadlocal对象被GC回收,该entry的key最终指向的是一个null,最终变为entry(null,value),这这个entry对应的value永远无法访问到,出现内存泄漏,测试demo
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadLocalTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(500);
for (int i = 0; i < 1000000; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
threadLocal.set((int)(Math.random() * 1000));
System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get());
// 不执行 remove
}
});
}
executorService.shutdown();
while (!executorService.isTerminated()) {
// 等待所有任务完成
}
System.out.println("所有任务已完成");
}
}
但按我们的计算,真的每个线程平均被绑定了2000个变量,累计出现100万个entry(null,value)吗?这就要提到ThreadLocal另一兜底机制,在我们没有及时remove的情况下,会导致出现大量entry(null,value),ThreadLocal 的 set() 方法在创建新的 Entry 时,如果 ThreadLocalMap 的大小(也即是已使用的 Entry 的数量)超过了阈值(默认为容量的2/3),就会调用 expungeStaleEntries() 方法清理所有键为空的 Entry。防止内存泄漏导致OOM
Threadlocal是作为静态变量
如果ThreadLocal是静态变量,那么它的生命周期就会伴随整个程序运行的生命周期,那么方法区中,就存在一个ThreadLocal是静态引用,这样无论多少次GC,threadlocal对象都不会被回收,所以不存在entry(null,value)这种情况
所以这种情况会不会导致比较严重的内存泄漏呢?
首先,如果是作为静态变量,threadLocal数量是有限的,不会像作为局部变量的时候,可能在程序运行期间累计创建成百上千万个,我们这里撑死算10个吧,线程池的数据500个左右,那么即便是没有及时remove,那么最多会有5000个无效的entry(1个线程关联10个threadlocal),整体来说是内存泄漏程度是可接受的
假如threadlocal这个key是作为强引用存储在threadLocalMap中呢?
那就会存在内存泄漏导致内存溢出的风险,还是接着上面那个例子,threadlocal是作为局部变量,方法结束threadlocal的(强)引用出栈,此时还剩线程Thread中的ThreadLocalMap中某个使用threadlocal引用作为key的Entry,假如这里也是强引用,那么即便是栈中的threadlocal的强引用出栈了,依然还是有ThreadLocalMap中某个Entry的强引用指向它,这导致threadlocal和这个Entry均无法被回收
如果是程序运行过程中,threadlocal作为局部变量可能被创建成百上千万次,这样在程序运行过程中会内存泄漏最终导致OOM
总结
threadlocal内存安全机制还是比较安全的,为我们提供了比较健全的兜底机制,但尽管如此我们也要重视内存的主动释放,且确保释放代码放到finally代码块中,尽可能避免内存泄露问题。
常见场景
线程安全应用 - Spring事务
/*
1 要保证所有的操作要么都成功要么都失败? => 使用数据库事物
2 如何保证全部操作都在同一事物内? => 所有的操作都必须使用同一个connection链接对象
3 如何保证所有的操作都必须使用同一个connection链接对象?=> 首先将这些操作通常是位于同一个线程内的
=> 只需要为一个线程绑定一个数据库连接对象就可
4 用ThreadLocal来防止一个线程获取多个数据库连接对象 (因为ThreadLocal是线程独立的),过程参考下面代码
*/
class JDBCUtils {
//-----------------------------------------------Druid链接池返回链接对象------------------------------------------------
private static DataSource dlSource = null;
//形式是:默认的隐形key threadName => conn;
private static ThreadLocal<Connection> conns = new ThreadLocal<Connection>();
//以静态代码块的形式创建dlSource链接池资源
static {
Properties pros = new Properties();
//读取druid配置文件,内容无非是一些数据库链接使用的url,username,password...
InputStream is = JDBCUtils.class.getClassLoader().getResourceAsStream("Druid.properties");
try {
pros.load(is);
} catch (IOException e) {
e.printStackTrace();
}
try {
dlSource = DruidDataSourceFactory.createDataSource(pros);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getDlConnection() {
//判断当前线程是否已经关联了一个连接对象(意味着这个线程之前已经获取到链接了)
Connection conn = conns.get();
//如果是null,意味着当前线程还没有创建过数据库连接对象,此时我们要去连接池中去获取链接然后保存到conns中
//否则,就意味着当前线程已有数据库连接对象,直接返回之前创建的就行了
if(conn == null){
try {
conn = dlSource.getConnection();
conns.set(conn);//为线程A保存刚刚获取的数据库连接对象conn
conn.setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}
return conn;
}
}
数据跨层传递,线程安全的本地缓存
Threadlocal本质是作为一个map容器,虽然它本身并不持有map对象,但该类的核心api都是对ThradLocalMap操作,所以Threadlocal经常是被拿来做本地缓存的,比如我项目中的拦截器,用于做用户鉴权,功能类似于网关,鉴权完毕,需要把一部分用户信息(比如访客id)缓存起来,并在service层取出使用。那么可以优先考虑Threadlocal,当前线程会绑定唯一一份当前访客的用户信息
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)