本文是学习极客时间每日一课《ThreadLocal原理分析及内存泄漏演示》和《ThreadLocal如何在父子线程及线程池中传递?》的学习笔记。
这两节小课主要讨论了以下几个问题:
在看ThreadLocal的本地变量实现原理之前,我们首先需要了解的是Java语言中引用相关的知识,这样更利于学习ThreadLocal相关的知识。
自JDK开始,Java提供了以下四种引用关系:
当使用ThreadLocal维护变量时,该变量存储在线程本地,其他线程无法访问,做到了线程间隔离,也就没有线程安全的问题。
程序运行时,栈中会存储Thread和ThreadLocal的引用。堆中的每一个Thread中都有一个ThreadLocalMap对象,ThreadLocalMap中有一个Entry数组,一个Entry对象中,又包含一个key和一个value,key就是ThreadLocal对象实例。这里的ThreadLocal的key就是弱引用,value是通过java.lang.ThreadLocal#set方法实际写入的值。
这里,主要看java.lang.ThreadLocal中的ThreadLocalMap内部静态类
static class ThreadLocalMap {
...
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
...
}
这个方法里面的逻辑是数据存储到ThreadLocal中的全过程,主要包含以下几步:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);//这里的this指的是ThreadLocal对象
} else {
createMap(t, value);//首次设置
}
}
ThreadLocal并不存储值,它只是作为一个key,让线程从ThreadLocalMap中获取value,ThreadLocalMap是使用ThreadLocal的弱引用作为key的,一个对象只剩下弱引用,则该对象在GC时就会被回收。
ThreadLocalMap使用ThreadLocal的弱引用作为key时,如果一个ThreadLocal没有外部强引用来引用它,比如,下图中,手动将ThreadLocal A这个对象赋值为null,系统GC时,这个ThreadLocal A会被回收,这时,ThreadLocalMap中就会出现key为null的Entry,Java程序无法访问这些key为null的Entry的value。
如果当前线程迟迟不结束,如使用了线程池,或者当前线程还要执行其他耗时的任务,那么这些key为null的Entry的value就会存在下图中,标红的这条强引用链:
TheadRef引用Thread,Thread引用ThreadLocalMap,ThreadLocalMap又引用Entry,Entry对象又引用了value,这个Map的Key已经是null,这个value则永远无法被回收。因为这条强引用链的存在,造成了内存泄漏。只有当前线程thread结束以后,ThreadRef就不存在与栈中,强引用断开,Thread对象、ThreadLocalMap,Entry数组、Entry对象、ObjC对象将全部被GC回收。
public class MyThreadLocalOOM {
public static final Integer SIZE = 500;
static ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 1,
TimeUnit.MINUTES,new LinkedBlockingDeque<>());
static class Stu{
private byte[] locla = new byte[1024*1024*5];
}
static ThreadLocal<Stu> local = new ThreadLocal<>();
public static void main(String[] args) {
try {
for(int i = 0; i < SIZE; i++){
executor.execute(()->{
local.set(new Stu());
System.out.println("开始执行");
});
Thread.sleep(100);
}
local = null; //会内存泄漏
System.out.println("执行完毕");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
第20行代码,将local置为null后,总共有5*5=25MB的堆内存泄漏。for循环结束后,手动GC,visualvm监控到的堆内存使用情况如下:
主动调用remove()方法:
public class MyThreadLocalOOM {
public static final Integer SIZE = 500;
static ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 1,
TimeUnit.MINUTES,new LinkedBlockingDeque<>());
static class Stu{
private byte[] locla = new byte[1024*1024*5];
}
static ThreadLocal<Stu> local = new ThreadLocal<>();
public static void main(String[] args) {
try {
for(int i = 0; i < SIZE; i++){
executor.execute(()->{
local.set(new Stu());
System.out.println("开始执行");
local.remove();
});
Thread.sleep(100);
}
System.out.println("执行完毕");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
for循环执行完毕后,手动GC,效果如下,内存泄漏完美解决:
主动调用ThreadLocal对象的remove方法,将ThreadLocal对象中的值删除。
ThreadLocal在设计时,也已经做了一些防护措施,在调用ThreadLocal的get()、set()方法操作数据时,会调用expungeStaleEntry(int staleSlot)方法清除当前线程中ThreadLocalMap中key为null的value。如果ThreadLocal对象的强引用被删除后,线程长时间存活,又没有再对该线程的ThreadLocal对象进行操作,依然会造成内存泄漏。
所以,在使用ThreadLocal时,要主动调用remove方法,将ThreadLocal对象中的值删除。
ThreadLocal提供了存储变量的能力,这些变量都是私有的,但是实际工作当中,我们经常会遇到多个线程共同访问同一个共享变量的情况。此时,如果对并发不是很了解,很可能就会造成并发问题。解决并发问题常用的手段有以下三种:
以下内容介绍的是线程本地变量的解决方案。
JDK提供了InheritableThreadLocal(ITL)可以在创建子线程时,拷贝父线程的本地变量的值到子线程本地变量中。
public class MyInheritableThreadLocal {
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException{
threadLocal.set(12345);
System.out.println("获取父线程本地变量:"+threadLocal.get());
new Thread(()-> System.out.println("获取子线程本地变量:"+threadLocal.get())).start();
TimeUnit.SECONDS.sleep(1);
}
}
通过main执行父线程,在main方法中创建子线程,使用threadLocal.set(12345);给父线程的本地变量赋值。运行结果如下,子线程获取不到父线程的本地变量。
public class MyInheritableThreadLocal {
// use TL
// public static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
//use ITL
public static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException{
threadLocal.set(12345);
System.out.println("获取父线程本地变量:"+threadLocal.get());
new Thread(()-> System.out.println("获取子线程本地变量:"+threadLocal.get())).start();
TimeUnit.SECONDS.sleep(1);
}
}
运行结果如下,子线程能获取到父线程的本地变量。
原因是,创建子线程时,ITL会拷贝一份父线程的本地变量给子线程。
如果父子线程都引用了同一个对象,会有线程安全问题。
public class InheritableThreadLocalTest {
public static ThreadLocal<Stu> threadLocal = new InheritableThreadLocal<>();
public static ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程开启");
threadLocal.set(new Stu("张三",1));
System.out.println("获取主线程本地变量:"+ threadLocal.get());
executorService.submit(() -> System.out.println("获取子线程本地变量:"+threadLocal.get()));
TimeUnit.SECONDS.sleep(1);
threadLocal.get().setAge(2);
System.out.println("主线程读取本地变量:"+threadLocal.get());
executorService.submit(()-> {
System.out.println("子线程获取本地变量:"+threadLocal.get());
threadLocal.get().setAge(3);
System.out.println("子线程获取本地变量:"+threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
System.out.println("主线程读取本地变量:"+threadLocal.get());
}
}
重写ITL中的childValue方法,实现对象的深拷贝,复制到子线程中的对象就是一个全新的对象,与父线程无关。
public class MyInheritableThreadLocalImpl<T> extends InheritableThreadLocal<T>{
@Override
protected T childValue(T parentValue) {
String s = JSONObject.toJSONString(parentValue);
return (T) JSONObject.parseObject(s, parentValue.getClass());
}
}
ITL只会在创建子线程时进行拷贝,如果使用线程池时,线程一直会存在于线程池中,后续可以用于执行多个提交到线程池的任务,此时,每次提交任务时,无法获取父线程的本地变量。
TransmittableThreadLocal是阿里开源的用于解决在使用线程池等会缓存线程的组件情况下传递ThrealLocal问题的ITL的扩展,通常简称为TTL,具体的实现原理就不分析了,今天写得太长了。
使用方法与ITL类似,如果要传递对象的话,需要重写TTL中的copy方法,代码如下:
/**
* 实现对象深拷贝
* @param <T>
*/
public class MyTransmittableThreadLocal<T> extends TransmittableThreadLocal<T> {
@Override
public T copy(T parentValue) {
String s = JSONObject.toJSONString(parentValue);
return (T) JSONObject.parseObject(s, parentValue.getClass());
}
}
昨天看曹政老师的公众号文章《谈谈关于学历的取舍》这篇文章时,有一段话对我触动挺大的:
如果你自学学不进去,烦请果断放弃,你说曹老师,你不经常卖课么,卖课其实也都是自学为主,看完课就学会了?怎么可能,至少要投入五倍以上的课程时间来实践和验证课程内容。很多人报了一堆课,最后学了啥?劝退劝退。我说句难听的话,虽然我卖了不少技术课程,但我觉得能认真学的进去的,1/5都不一定有。你真的学进去了,学扎实了,你超过80%的人。 话说回来,如果一门技术课程你听了就学会了,精通了,才卖你66?88?99?129?卖你5万、10万都是应该的!
别的办法我也不会,只能用练习+写笔记的这种本办法,让自己扎扎实实地学,踏踏实实地往前走。以后的学习我都会这么做。
写作平台真好用。
领取专属 10元无门槛券
私享最新 技术干货