【原创】Java并发编程系列30 | ThreadLocal
线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,而大多数解决线程安全问题的方法都是通过加锁的方式,让同一时间只有一个线程能过访问到共享资源。这篇文章介绍另种解决线程安全的思路——ThreadLocal:
- 介绍
- 使用
- 源码
- 内存泄露
- 总结
1. 介绍
线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,大多数解决线程安全问题的方法都是通过加锁的方式,让同一时间只有一个线程能过访问到共享资源。
ThreadLocal提供了另一种解决思路,让每个线程拥有自己私有的内存空间,将线程私有的数据存入这个私有空间内,线程与线程之间相互隔离,这样就不会有线程安全问题。
数据结构
每个Thread线程内部都有一个ThreadLocalMap变量threadLocals,这个threadLocals就是这个线程的私有空间。
threadLocals是一个key-value的map结构,key是ThreadLoacal对象,value就是线程需要保存的私有数据。
2. 使用
ThreadLocal核心方法:
get():返回当前线程副本中该ThreadLocal对应的值。
initialValue():返回当前线程副本中的该ThreadLocal对应对应的“初始值”。
remove():移除当前线程副本中该ThreadLocal对应的值。
set(T value):当前线程副本中该ThreadLocal对应的值为value。
使用举例:
每个线程保存一个私有的int值count,5个线程count从0加到10,线程之间互不影响。
/**
* 每个线程保存一个私有的int值count
* 5个线程count从0加到10,线程之间互不影响
*/
public class ThreadLocalDemo {
private static ThreadLocal<Integer> countLocal = new ThreadLocal<Integer>(){
public Integer initialValue() {
return 0;
}
};
public static void main(String[] args){
for (int i = 1; i <= 5; i++) {
new Thread("Thread_" + i) {
public void run() {
for (int j = 1; j <= 10; j++) {
countLocal.set(countLocal.get() + 1);
System.out.println(getName() + ": count=" + countLocal.get());
}
};
}.start();
}
}
}
输出结果如下:
Thread_3: count=1
Thread_5: count=1
Thread_5: count=2
Thread_4: count=1
Thread_4: count=2
Thread_2: count=1
Thread_2: count=2
Thread_2: count=3
Thread_2: count=4
Thread_1: count=1
Thread_2: count=5
Thread_4: count=3
Thread_5: count=3
Thread_5: count=4
Thread_3: count=2
Thread_5: count=5
Thread_5: count=6
Thread_5: count=7
Thread_5: count=8
Thread_4: count=4
Thread_4: count=5
Thread_4: count=6
Thread_2: count=6
Thread_2: count=7
Thread_2: count=8
Thread_2: count=9
Thread_1: count=2
Thread_2: count=10
Thread_4: count=7
Thread_5: count=9
Thread_3: count=3
Thread_3: count=4
Thread_5: count=10
Thread_4: count=8
Thread_1: count=3
Thread_4: count=9
Thread_3: count=5
Thread_4: count=10
Thread_1: count=4
Thread_3: count=6
Thread_1: count=5
Thread_3: count=7
Thread_1: count=6
Thread_3: count=8
Thread_1: count=7
Thread_3: count=9
Thread_1: count=8
Thread_3: count=10
Thread_1: count=9
Thread_1: count=10
可以看到,即使5个线程并发执行,但是每个线程内部的count都是按1-10的顺序相加的。
3. 源码
3.1 数据结构
每个Thread线程内部都有一个ThreadLocalMap变量threadLocals,这个threadLocals就是这个线程的私有空间。
threadLocals是一个key-value的map结构,key是ThreadLoacal对象,value就是线程需要保存的私有数据。
/**
* 线程局部变量threadLocals为ThreadLocal.ThreadLocalMap类型
*/
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
/**
* ThreadLocal$ThreadLocalMap 散列表结构
* key=ThreadLocal value=Object
*/
static class ThreadLocalMap {
private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
3.2 get()
在理解的ThreadLocal的存储结构之后,再看get()和set()方法就很简单了。
get():
- 获取当前线程thread。
- 获取当前线程thread.threadLocals,threadLocals是map结构。
- map的key是ThreadLocal类型,获取map中当前threadLocal对应的value值。
- 如果map=null,就创建map并赋初值。
public T get() {
// 获取当前线程私有的map thread.threadLocals
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 获取map的value值
if (map != null) {
// map的key是ThreadLocal类型
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果map=null,初始化map,下文有讲解
return setInitialValue();
}
/**
* 返回ThreadLocalMap类型的thread.threadLocals
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* 初始化value,map=null时会初始化map
*/
private T setInitialValue() {
T value = initialValue();// 初始值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);// 将初始值存入map
else
createMap(t, value);// map=null时初始化map
return value;
}
/**
* 返回map中value的初始值
* 默认为null,一般需要重写该方法以获得非null值
*/
protected T initialValue() {
return null;
}
set():
- 获取当前线程thread。
- 获取当前线程thread.threadLocals,threadLocals是map结构。
- map的key是ThreadLocal类型,设置map中当前threadLocal对应的value值。
- 如果map=null,就创建map并赋值。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
4. 注意问题
4.1 每个线程最好只存一个ThreadLocal
线性探测解决Hash冲突:根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
如下,ThreadLocalMap.set()方法:
private void set(ThreadLocal<?> key, Object value) {
// 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置
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)]) {
ThreadLocal<?> k = e.get();
// key 存在,直接覆盖
if (k == key) {
e.value = value;
return;
}
/*
* key=null而value!=null,因为key是弱引用
* 用新的key-value将旧的null-value替换掉
*/
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
// 清除陈旧的Entry(key == null)
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。所以每个线程最好只存ThreadLocal。
4.2 引发内存泄露
ThreadLocal使用中会有内存泄露问题。
ThreadLocalMap的key是弱引用,而Value是强引用。源码如下:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
ThreadLocalMap的key是弱引用,发生GC时弱引用key会被回收;而value是强引用,GC时不会被回收。
所以ThreadLocalMap中就会出现key为null的Entry,因为key为null,这Entry是不能被访问到的。如果当前线程一直没结束的话,一直有这个引用链:Thread --引用--> ThreaLocalMap --引用--> Entry --引用--> value,这个value就无法被回收,导致内存泄露。
解决:
- ThreadLocalMap的set()、cleanSomeSlots()等方法中都做了相应处理,检查存在key=null而value!=null的Entry就会删掉;
- 在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。
5. 总结
同步机制 VS ThreadLocal
同步机制是通过控制线程访问共享对象的顺序,类似“时间换空间”,同一时刻共享对象只能被一个线程访问造成整体上响应时间增加,但是对象只占有一份内存。
而ThreadLocal是为每一个线程分配一个该对象,各用各的互不影响。类似“空间换时间”,为每个线程都分配了一份对象,自然而然内存使用率增加,但整体上时间效率要增加很多。
ThreadLocal存储结构
每个Thread线程内部都有一个ThreadLocalMap变量threadLocals,这个threadLocals就是这个线程的私有空间。
threadLocals是一个key-value的map结构,key是ThreadLoacal对象,value就是线程需要保存的私有数据。
注意问题
ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。所以每个线程最好只存ThreadLocal。
由于ThreadLocalMap的key是弱引用,ThreadLocal使用中会有内存泄露问题。在使用完ThreadLocal之后调用remove方法删除值,可避免内存泄露问题。
- SpringBoot集成MyBatis的分页插件PageHelper(回头草)
- SpringBoot整合Mybatis之进门篇
- Tomcat和Java Virtual Machine的性能调优总结
- 一次浴火重生的MySQL优化(EXPLAIN命令详解)
- 简单聊聊不可或缺的Nginx反向代理服务器--实现负载均衡【上篇】
- Java设计模式之适配器设计模式(项目升级案例)
- Java设计模式之模板方法设计模式(银行计息案例)
- 多线程之策略模式
- 文件上传的动作不能太俗,必须页面无刷新上传
- 这次真的忽略了一些ActiveMQ内心的娇艳
- 多线程编程:阻塞、并发队列的使用总结
- 多线程编程:多线程并发制单的开发记录【一】
- 如何使用线程锁来提高多线程并发效率
- 如何在分布式环境中同步solr索引库和缓存信息
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释
- 移植lua5.2和luasocket库到嵌入式linux,使能强大的lua脚本和网络功能
- Go或者C中调用Lua业务脚本,实现终端应用的热更新机制
- 多线程基础(八):ReentrantLock的使用及与synchronized的区别
- 在java中notify和notifyAll的区别
- 我在近期求职中遇到的前端面试问题及其解法
- 腾讯云 云开发 部署 Blazor网站
- 最新基准测试:Kafka、Pulsar 和 RabbitMQ 哪个最快?
- 基于飞桨实现高光谱反演:通过遥感数据获取土壤某物质含量
- 飞桨Tracking目标跟踪库开源!涵盖业界主流的VOT算法,精准检测动态目标轨迹
- 基于react的组件库主题设计方案
- Xilinx MPSoC PS/PL之间的数据交互和外设设计
- 基于Res-Unet网络实现肝脏肿瘤分割任务
- golang判断map中key是否存在的方法
- 迁移实战:一次AntDB(基于pgxl分布式架构的数据库)数据库迁移经验分享
- 看完这篇文章,99%的人都会使用Mysql Explain工具