一、什么是ThreadLocal
1、什么是ThreadLocal&为什么用ThreadLocal
ThreadLocal,即线程本地变量,在类定义中的注释如此写This class provides thread-local variables
。如果创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是在操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了并发场景下的线程安全问题。属于空间换时间的解决线程安全问题的方案。
2、ThreadLocal的使用场景
- 在某些项目中,日志需要存储用户的信息,因此在切面中,可以使用ThreadLocal存储用户信息,或者使用ThreadLocal存储各个接口的返回结果,在切面中统一处理
- 在格式化日期的时候,用到SimpleDateFormat,需要使用Thread Local来保证线程安全,如下
public class test {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
System.out.println(sdf.parse("2023-03-17 10:34:30"));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
出现了线程安全问题,接下来,我们使用ThreadLocal处理
public class test {
private static final ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
System.out.println(sdf.get().parse("2023-03-17 10:34:30"));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
这是因为SimpleDateFormat是一个线程不安全类,在多线程情况下会出现问题,而通过ThreadLocal处理后,变成了每个线程私有的一个类,因此成功运行。
二、ThreadLocal的原理
1、关键代码分析
先来看下Thread类中与ThreadLocal有关的代码和ThreadLocal的关键代码
class Thread{
ThreadLocal.ThreadLocalMap threadLocals = null;
}
从代码中可以看到,ThreadLocal.ThreadLocalMap是Thread的一个属性,也就是说,一个线程持有一个ThreadLocal.ThreadLocalMap对象。
class ThreadLocal{
//ThreadLocal的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);
}
//ThreadLocal的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();
}
//ThreadLocalMap类
static class ThreadLocalMap{
private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>>{
Object value;
Entry(ThreadLocal<?>k, Object v){
super(k);
value = v;
}
}
}
//获取map方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
}
ThreadLocalMap是ThreadLocal类的静态内部类,ThreadLocalMap内部维护了Entry数组,每个Entry是一个完整的key-value对象,其中key为ThreadLocal本身。接着看ThreadLocal的get/set方法,都是先获取当前线程对象t,然后通过getMap方法返回当前线程对象t的threadLocals对象,也就是线程持有的ThreadLocalMap对象,然后将当前线程对象作为key传入,从ThreadLocalMap对象中获取值或者设置值。综上,我们可以做出一个结构图。
2、ThreadLocal和Thread结构图
3、ThreadLocal原理概述
综合上面的代码分析和结构图,我们可以得出ThreadLocal的基本原理
Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
并发多线程场景下,每个线程Thread,在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而可以实现了线程隔离。
三、引用问题&内存泄漏
1、Java的引用类型
- 强引用:我们常用的new对象就是强引用,
Object obj = new Object();
这种引用对象在内存不足时,jvm宁愿抛出oom错误也不会被回收 - 软引用:在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
- 弱引用:无论内存是否足够,只要jvm开始进行垃圾回收,那些被弱引用关联的对象都会被回收
- 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
/**
* 测试引用类型,jvm参数设置为-Xms10M -Xmx10M
*/
public class test {
private static List<Object>list = new ArrayList<>();
public static void main(String[] args) {
testWeakReference();
}
/**
* 强引用
* 直接报错OOM
*/
public static void testStrongReference(){
byte[] bytes = new byte[1024*1024*11];
}
/**
* 软引用,当发生gc时,内存不存会回收对象
* null
* null
* null
* null
* [B@7eda2dbb
*/
public static void testSoftReference(){
for (int i = 0; i < 5; i++) {
byte[] bytes = new byte[1024*1024*5];
SoftReference<byte[]> softReference = new SoftReference<>(bytes);
list.add(softReference);
}
System.gc();
for (int i = 0; i < list.size(); i++) {
Object o = ((SoftReference)list.get(i)).get();
System.out.println(o);
}
}
/**
* 弱引用,发生gc时,无论内存是否足够,都回收对象
* null
* null
* null
* null
* null
*/
public static void testWeakReference(){
for (int i = 0; i < 5; i++) {
byte[] bytes = new byte[1024*1024*5];
WeakReference<byte[]> weakReference = new WeakReference<>(bytes);
list.add(weakReference);
}
System.gc();
for (int i = 0; i < list.size(); i++) {
Object o = ((WeakReference)list.get(i)).get();
System.out.println(o);
}
}
}
2、为什么ThreadLocalMap的Entry对象的key用的是弱引用
如下图所示,Entry对象的key继承了弱引用的ThreadLocal
我们先看下这个ThreadLocal的引用图
我们先假设用的是强引用,只要我们的线程一直存活(或者使用了线程池),那么,无论ThreadLocal变量的引用存在与否,ThreadLocal对象都会被entry对象引用,那么就造成了即使ThreadLocal变量的引用不存在了,这个ThreadLocal对象也不会被回收,造成内存泄漏;而设置成弱引用,那么发生gc,检查到ThreadLocal对象只存在弱引用,就会被回收。
3、内存泄漏
先来看一段代码及其两种演示结果
/**
* jvm设置为最大20m
* 如果不调用remove方法,运行过程中出现oom错误
* 执行remove方法,不会出现oom错误
*/
public class test {
static class zyObject{
private byte[] bytes = new byte[10 * 1024 * 1024];
}
private static ThreadLocal<zyObject> zyObjectThreadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
for (int i = 0; i < 10; i++) {
int a = i;
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("第"+a+"个线程");
zyObject zyObject = new zyObject();
zyObjectThreadLocal.set(zyObject);
zyObject = null;//将对象设置为 null,表示此对象不在使用了
// zyObjectThreadLocal.remove();
}
});
Thread.sleep(1000);
}
}
}
上面说到,Entry对象的key使用弱引用的ThreadLocal对象,发生GC时会被回收,那么,还存在什么内存泄漏问题呢?这是因为ThreadLocal回收的话,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如线程池的核心线程),这些key为null的Entry的value就会一直存在一条强引用链:Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object
永远无法回收,造成内存泄漏。所以我们使用完ThreadLocal之后,要记得调用remove
方法。实际上,ThreadLocal在设计的时候也考虑过这些问题,在set、get、remove方法中都有对key为null的移除。
四、如何使用ThreadLocal父子线程传值
我们知道,ThreadLocal作为本地线程变量,它的变量是私有的,那么如何进行线程间的传值呢?先来看下面这段代码
/**
* 运行结果
* null
* 我是BBBBBBBBBBBBBBB
*/
public class test {
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
threadLocal.set("我是AAAAAAAAAAAAAAAAA");
inheritableThreadLocal.set("我是BBBBBBBBBBBBBBB");
new Thread(()->{
System.out.println(threadLocal.get());
System.out.println(inheritableThreadLocal.get());
}).start();
}
}
可以看到,InheritableThreadLocal实现了父子线程间的传值。我们看下Thread类的初始化代码
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
}
可以发现,当parent的inheritableThreadLocals不为null时,就会将parent的inheritableThreadLocals,赋值给前线程的inheritableThreadLocals。说白了,就是如果当前线程的inheritableThreadLocals不为null,就从父线程哪里拷贝过来一个过来,类似于另外一个ThreadLocal,数据从父线程那里来的。
总结
这篇文章主要从ThreadLocal的结构、原理、常见问题方面阐述了ThreadLocal的相关知识,并没有去讲解ThreadLocalMap的初始化、扩容、重新hash等知识,这些内容在日常使用中不算特别重要,如果需要了解,适当看一下源码及Map的原理即可。