文章目录
概念
ThreadLocal
叫做线程变量,意思是ThreadLocal
中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal
为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLocal
主要解决了每个线程绑定自己的值,通过使用get()
和set()
方法,获取默认值或者将其值更改为当前线程的副本从而避免了线程安全问题,避免了多线程之间的共享和竞争问题。
ThreadLocal
是一个泛型类,提供了一种简单的方式来创建线程本地变量。每个线程都可以通过ThreadLocal
获取自己的变量副本,而不会影响其他线程的副本。
ThreadLocal
的简单应用
ThreadLocal
中常见的API
ThreadLocal
的使用案例
class House{
int saleCount;//卖方总数
public synchronized void soldHouse(){
saleCount++;
}
//为每一个线程提供一个副本,来记录每个线程分别卖了多少房子
ThreadLocal<Integer> OnesaleCount=ThreadLocal.withInitial(() -> 0);//初始化
//每一个线程卖了一个房子,OnesaleCount+1
public void OnesaleCountByThreadLocal(){
OnesaleCount.set(1+OnesaleCount.get());
}
}
public class Code01 {
public static void main(String[] args) {
House house=new House();
//1.建立五个线程,每个线程都去买房,再将每个线程的卖房数计数到总线程上
for (int i = 0; i < 5; i++) {
new Thread(()->{
int size=new Random().nextInt(6)+1;
for (int j = 1; j <= size; j++) {
house.soldHouse();
house.OnesaleCountByThreadLocal();
}
System.out.println("线程 : "+Thread.currentThread().getName()+"卖出的房子 : "+house.OnesaleCount.get());
},String.valueOf(i)).start();
}
try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(house.saleCount);
}
}
运行结果:
ThreadLocal
和synchronized
之间的本质区别
Synchronized
是用于线程之间的数据共享,ThreadLocal
则是用于线程之间的数据隔离Synchronized
是利用锁机制,使变量或者代码块在某一个时刻只能被一个线程访问.而ThreadLocal
为每一个线程都提供了变量的副本,使得每个线程在某一个时间访问到的并不是同一个对象,这样就实现了多线程对数据共享的隔离.而Synchronized
正好相反,它用于在多个线程间通信时能够获得数据共享.
ThreadLocal
源码剖析
Thread
,ThreadLocal
,ThreadLocalMap
之间的关系
这里我们翻看源码得出结论:
所以:
Thread
中存在一个ThreadLocalMap
类型的成员变量,而ThreadLocalMap
是ThreadLocal
的静态内部类.
这里我们再用图像来帮忙理解:
接下来我们根据源码来逐个理解.
Thread
public class Thread implements Runnable {
//.......
ThreadLocal.ThreadLocalMap threadLocals = null;
}
说明:每一个线程都有一个指定的
ThreadLocalMap
成员属性,而每一个ThreadLocalMap
属性中又维护了很多的<ThreadLocal,Value>
键值对.
ThreadLocalMap
static class ThreadLocalMap {
//在ThreadLocalMap的内部嵌套了一层静态内部类Entry,此处继承了弱引用WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
//KV键值对
//Key值:ThreadLocal
//value:该ThreadLocal中存储的变量值
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
说明:在
ThreadLocalMap
中又嵌套了一层静态内部类,即Entry
,所以KV
键值对本身是由Entry
来维护的
观察Entry
在get()
方法中是如何应用的:
public class ThreadLocal<T> {
public T get() {
//获取到当前线程
Thread t = Thread.currentThread();
//得到当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果当前线程有ThreadLocalMap
if (map != null) {
//获取到当前线程的map表中的kv键值对
ThreadLocalMap.Entry e = map.getEntry(this);//传入threadlocal对象,相当于key
//如果键值对不为空
if (e != null) {
@SuppressWarnings("unchecked")
//获取值,且将值返回
T result = (T)e.value;
return result;
}
}
return setInitialValue();//设置初始的ThreadLocalMap初始值
}
}
观察Entry
在set()
方法中是如何应用的:
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {//map存在,直接将值放入
map.set(this, value);
} else {//map不存在,先去创建一个map结构
createMap(t, value);
}
}
}
通过阅读源码可以理解:
ThreadLocalMap
是属于每一个线程Thread
所独有的成员属性,我们在调用的get
和set
方法时,可以先获取到当前线程,通过当前线程找到ThreadLocalMap
,再根据ThreadLocalMap
来寻找其内部的键值对结构<TreadLocal,Value>
由Entry
存放.(Entry
是ThreadLocalMap
中的一个静态内部类).
ThreadLocal
优点
- 线程隔离:每个线程都有自己的变量副本,避免了多线程环境下的共享和竞争问题。
- 简单易用:
API
设计简单,使用方便。 - 避免锁:通过使用线程局部变量,可以避免使用显式锁,从而提高性能。
注意事项
- 内存泄漏:如果不调用
remove()
方法,线程局部变量可能会导致内存泄漏,特别是在使用线程池的场景中。 - 适用场景:
ThreadLocal
适用于存储用户会话信息、数据库连接、事务管理等需要线程隔离的场景。
ThreadLocal
中的内存泄漏问题
public class ThreadLocal<T> {
public void remove() {
//通过获取当前线程获取到ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
//如果ThreadLocalMap不为空
if (m != null)
m.remove(this);//直接删除掉ThreadLocal所对应的键值对结构
}
}
remove
方法,直接将ThrealLocal
对应的值从当前线程Thread
对应的ThreadLocalMap
中删除。为什么要删除,这涉及到内存泄漏的问题。
什么是内存泄漏问题?
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏.
弱引用
static class ThreadLocalMap {
//这里的Entry继承了WeakReference弱引用对象
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
我们发现,这里的Entry
使用了WeakReference<ThreadLocal<?>>
进行封装,那么什么是弱引用?这里又为什么使用弱引用呢?
这里我们可以先移步此篇博文【JVM】强引用,软引用,弱引用,虚引用分别是什么分别是什么,了解前置知识.下面我们就假设你已经了解了什么是弱引用
为什么Entry
需要使用弱引用
public static void main(String[] args) {
ThreadLocal<String> tl=new ThreadLocal<>();
String v="treadLocal1 开始 ";
tl.set(v);
tl.get();
//主线程结束,栈帧销毁
}
当该方法执行完毕之后,栈帧销毁,强引用tl
没有了.但此时线程还没结束,所以ThreadLocalMap
还存在,其中的entry
里某个Key
引用还指向这个对象:
- 若这个
Key
引用是强引用,就会导致Key
指向的ThreadLocal
对象不能被gc
回收,从而导致内存泄漏; - 若这个
Key
引用是弱引用,就能大概率减少内存泄漏的问题.使用弱引用,可以使ThreadLocal
对象在方法执行完毕后顺利被回收且Entry
的Key
引用指向null
.
但如此便可以保证不会发生内存泄漏问题了吗?并不是这样的,如果Key
中有null
值,还是会存在内存泄漏的问题.
ThreadLocalMap
使用ThreadLocal
弱引用作为Key
,如果一个ThreadLocal
没有外部的强引用引用他(手动置空),那么系统gc
的时候,这个ThreadLocal
势必会被回收,那么这样一来,ThreadLocalMap
中就会出现Key
为null
的Entry
,就没有办法访问这些Key
为null
的value
,如果当前线程迟迟不结束的话(线程池),这些key
为null
的Entry
的value
就会一直存在一个强引用链.- 虽然弱引用,保证了
Key
指向的ThreadLocal
对象能被及时回收,但是v
指向的value
对象是需要ThreadLocalMap
调用get
方法或者set
方法时,发现key
为null
才会去回收整个entry
,value
,因此弱引用不能100%
保证内存不泄漏.我们要在不使用某个ThreadLocal
对象后,手动调用remove
方法来删除它,尤其是在线程池中,如果我们不手动调用remove
方法,那么后面的线程就有可能会获取到上一个线程遗留下来的value
值,造成bug.
清除脏Entry
我们再一次深入源码,理解一下我们是如何在get
方法和set
方法中对key=null
的entry
进行清除的.
//set()方法
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//调用set方法
map.set(this, value);
} else {
createMap(t, value);
}
}
}
-------------------------------------------------------------------------------------
static class ThreadLocalMap {
private void set(ThreadLocal<?> key, Object value) {
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)]) {
//如果此时的key不为空
if (e.refersTo(key)) {
e.value = value;//将value值放到value位置上
return;
}
//如果此时的key是null
if (e.refersTo(null)) {
//当key是null的时候调用该方法
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
}
replaceStaleEntry
方法是怎样实现在key
为null
时,保证value
可以被垃圾回收?
建议
在<阿里巴巴开发规范手册>中,建议ThreadLocal
对象使用static
来修饰.
说明:ThreadLocal
是针对一个线程内的所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说类第一次使用的时候被装载,只分配一块存储空间,所有此类的对象(在同一个线程内定义的)都可以操作这个变量.