一. 基础
什么是ThreadLocal: 前线程的线程私有的本地变量,每个线程在通过get(),set()访问ThreadLocal时,都有自己的独立初始化的变量副本 你用ThreadLocal实现了什么功能:利用每个线程都有自己的ThreadLocal,多线程操作是互不影响线程安全的特性 ThreadLocal常见面试题示例
ThreadLocal中ThreadLocalMap的数据结构和关系了解吗 ThreadLocal的key是什么 ThreadLocal内存泄漏问题讲一下,怎么防止 ThreadLocal中最后为什么要加remove方法
常用api 示例
import java. util. concurrent. TimeUnit ;
public class ThreadLocalDemo {
private static ThreadLocal < Object > threadLocal = new ThreadLocal < Object > ( ) {
@Override
protected Object initialValue ( ) {
return "" ;
}
} ;
private static ThreadLocal < Integer > threadLocal1 = ThreadLocal . withInitial ( ( ) -> 0 ) ;
public static Integer get ( ) {
return threadLocal1. get ( ) ;
}
public static void set ( int value) {
threadLocal1. set ( value) ;
}
public static void clear ( ) {
threadLocal1. remove ( ) ;
}
public static void main ( String [ ] args) throws InterruptedException {
new Thread ( ( ) -> {
for ( int i = 0 ; i < 5 ; i++ ) {
ThreadLocalDemo . set ( i) ;
}
System . out. println ( Thread . currentThread ( ) . getName ( ) + "线程,获取ThreadLocal值" + ThreadLocalDemo . get ( ) ) ;
} , "t1" ) . start ( ) ;
new Thread ( ( ) -> {
for ( int i = 0 ; i < 10 ; i++ ) {
ThreadLocalDemo . set ( i) ;
}
System . out. println ( Thread . currentThread ( ) . getName ( ) + "线程,获取ThreadLocal值" + ThreadLocalDemo . get ( ) ) ;
} , "t2" ) . start ( ) ;
TimeUnit . SECONDS. sleep ( 5L ) ;
System . out. println ( "主线程执行完毕,主线程的ThreadLocal值" + ThreadLocalDemo . get ( ) ) ;
}
}
使用ThreadLocal存储数据时的几个注意点
如果使用SimpleDateFormat,SimpleDateFormat是线程不安全的,不要定义为static,如果必须定义为staitc,必须加锁,或使用DateUtils工具类,下方代码中在执行第三步main方法时,会报错,
import java. text. ParseException ;
import java. text. SimpleDateFormat ;
import java. util. Date ;
public class ThreadLocalDeom2 {
private static SimpleDateFormat sdf = new SimpleDateFormat ( "yyyy-MM-dd HH:mm:class" ) ;
public static Date parse ( String dateStr) throws ParseException {
return sdf. parse ( dateStr) ;
}
public static void main ( String [ ] args) {
for ( int i = 0 ; i < 5 ; i++ ) {
new Thread ( ( ) -> {
try {
ThreadLocalDeom2 . parse ( "2021-09-23 11:20:33" ) ;
} catch ( ParseException e) {
e. printStackTrace ( ) ;
}
} ) . start ( ) ;
}
}
}
原因: SimpleDateFormat类内部又一个Calender对象引用,它用来存储SimpleDateFormat相关的时间信息,当SimpleDateFormat使用static修饰时,那么多个线程之间共享SimpleDateFormat,同时也会共享这个Calendar,Caleandar内部会多次引用与clear(),是线程不安全的,存在bug,所以会报错 使用ThreadLocal方式每个线程都有自己的SimpleDateFormat,不存在多线程问题(另外还有不使用static方式,加锁方式等,但是性能低,或使用DateUtils工具类) 一定要清除ThreadLocal(ThreadLocal使用完毕后必须在finally中remove,防止内存泄漏,需考虑线程池下线程会被复用)
二 ThreadLocal 源码分析
先提出一个问题: Thread, ThreadLocal, ThreadLocalMap 三者有什么关系(了解一下整体结构)
Thread 是一个线程类,在Thread线程类中有一个"ThreadLocal.ThreadLocalMap threadLocals = null;" 属性,这说明每个线程中都有一个ThreadLocal,是线程的局部变量 查看 ThreadLocalMap, 是ThreadLocal的内部的"static class ThreadLocalMap" 静态内部类 而ThreadLocalMap中还有一个静态内部类 “static class Entry extends WeakReference<ThreadLocal<?>>”
由这个整体关系图我们了解到: ThreadLocal底层使用ThreadLocalMap存储,ThreadLocalMap底层使用一个静态内部类Entry进行存储,由Entry继承自WeakReference了解到ThreadLocal使用弱引用,由Entry结构了解到,ThreadLocal底层存储的是key value键值对 进而提出问题
ThreadLocal底层使用key value键值对存储, key是什么, value是什么 什么是弱引用,Entry 中的key为什么使用弱引用
先了解一下ThreadLocalMap: 实际就是一个以当前ThreadLocal实例为key,存储的任意数据对象为value的Entry,当我们为ThreadLocal变量赋值时,实际就是当前ThreadLocal为key,值为value的entry放入ThreadLocalMap中 可以这样理解在JVM中维护了一个线程版的Map,该Map结构是Map<ThreadLocal,Value>, 通过ThreadLocal的set方法存储数据时,结果是把ThreadLocal自身作为key放进了ThreadLocalMap中,在用到这个数据时通过get方法去这个map中寻找,不同的线程由不同的ThreadLocal,这样每个线程都拥有了自己的独立变量
通过 ThreadLocal 的 get() 方法进行源码分析
查看ThreadLocal 的get()方法源码, 在get() 方法中会获取当前线程,获取当前线程的ThreadLocal.ThreadLocalMap,如果获取不到说明未被初始化,调用setInitialValue()方法进行初始化 在setInitialValue() 方法中 重点是调用 “createMap(t, value)” 创建 ThreadLocalMap
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 ( ) ;
}
private T setInitialValue ( ) {
T value = initialValue ( ) ;
Thread t = Thread . currentThread ( ) ;
ThreadLocalMap map = getMap ( t) ;
if ( map != null )
map. set ( this , value) ;
else
createMap ( t, value) ;
return value;
}
查看setInitialValue()中创建ThreadLocalMap方法createMap(),前面我们了解过在ThreadLocal中有一个静态内部类ThreadLocalMap, 在ThreadLocalMap中有一个静态内部类Entry, 在createMap()方法中会调用ThreadLocalMap构造器,首先初始化长度INITIAL_CAPACITY为16,以当前ThreadLocal为key,存储的值为value,创建Entry,获取一个下标位置"firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);",将创建的Entry存储到数组中
void createMap ( Thread t, T firstValue) {
t. threadLocals = new ThreadLocalMap ( this , firstValue) ;
}
private static final int INITIAL_CAPACITY = 16 ;
ThreadLocalMap ( ThreadLocal < ? > firstKey, Object firstValue) {
table = new Entry [ INITIAL_CAPACITY] ;
int i = firstKey. threadLocalHashCode & ( INITIAL_CAPACITY - 1 ) ;
table[ i] = new Entry ( firstKey, firstValue) ;
size = 1 ;
setThreshold ( INITIAL_CAPACITY) ;
}
private int threshold;
private void setThreshold ( int len) {
threshold = len * 2 / 3 ;
}
三 通过 ThreadLocal 引出四大引用
什么是内存泄漏: 不会再被使用的对象或变量占用内存不能被回收,就是内存泄漏 前面我们了解到在ThreadLocal中底层实际存储数据的是一个静态内部类ThreadLocalMap中的的静态内部类Entry,这个Entry继承了 WeakReference<ThreadLocal<?>> 父类,说明Entry的key是个弱引用
四大引用
在java中分为: 强引用Reference, 软引用SoftReference, 弱引用WeakReference, 虚引用PhantomReference, (下图中的ReferenceQueue引用队列) 复习JVM: 在java中如果一个对象被判定为不可达对象时,在执行垃圾回收该对象会被回收掉,在该对象被回收前会执行finalize()方法,进行指定的清理工作(一般这个方法工作中不用)
默认强引用Reference
示例代码: 只要对象还是被引用对象不会回收,当对象不再被引用,例如第二步中设置为null,执行gc,该对象才会被回收
import java. util. concurrent. TimeUnit ;
class MyReference {
@Override
protected void finalize ( ) throws Throwable {
System . out. println ( "gc, finalize() invoked" ) ;
}
}
public class ReferenceDemo {
public static void main ( String [ ] args) throws InterruptedException {
MyReference my = new MyReference ( ) ;
System . out. println ( "gc 前 my:" + my) ;
my = null ;
System . gc ( ) ;
TimeUnit . SECONDS. sleep ( 5L ) ;
System . out. println ( "gc 后 my:" + my) ;
}
}
把一个对象赋值给引用变量,这个引用变量就是一个强引用,当一个对象被强引用变量引用时,它处于可达状态,只要被判定为可达对象,哪怕内存不足,发送OOM,也不会被垃圾回收,只有在判定为不可达对象时例如设置为null才会被回收,既强引用是产生内存泄漏的原因之一
软引用SoftReference
软引用的特点: 内存够用不会被回收,内存不够时会被回收,即使该对象还在引用中 方便测试设置手动设置内存为10m 示例:软引用使用 SoftReference 实现,创建软引用对象 SoftReference softReference, 当内存不够用时,例如上面设置内存为10m,在第三步中创建一个9m对象,此时会出现内存溢出问题,MyReference中的finalize会执行,在第四步获取对象,打印为null,说明软引用对象在内存不够用时会被回收掉
public class ReferenceDemo {
public static void main ( String [ ] args) throws InterruptedException {
SoftReference < MyReference > softReference = new SoftReference < > ( new MyReference ( ) ) ;
try {
byte [ ] bytes = new byte [ 9 * 1024 * 1024 ] ;
TimeUnit . SECONDS. sleep ( 5L ) ;
} finally {
System . out. println ( "my: " + softReference. get ( ) ) ;
}
}
}
弱引用WeakReference
弱引用使用WeakReference实现,对于弱引用来说只要垃圾回收执行,不管JVM内存够不够用,弱引用对象都会被回收 示例,在第二步手动触发gc(), 被修饰为弱引用的MyReference对象会被回收掉
public class ReferenceDemo {
public static void main ( String [ ] args) throws InterruptedException {
WeakReference < MyReference > weakReference = new WeakReference < > ( new MyReference ( ) ) ;
System . gc ( ) ;
TimeUnit . SECONDS. sleep ( 5L ) ;
System . out. println ( "my: " + weakReference. get ( ) ) ;
}
}
软引用与弱引用的适用场景:例如将图片文件等对象数据作为软/弱引用,当内存不住或,垃圾回收时,将该对象回收掉,防止出现oom等等
虚引用PhantomReference
四. ThreadLocal 中存在的内存泄漏问题与
Thread 内部维护了一个ThreadLocalMap的引用, 而ThreadLocalMap是ThreadLocal的内部类,底层使用Entry结构存储数据,在ThreadLocal调用set()方法时,实际就是往ThreadLocalMap中存储数据,Key是ThreadLocal本身,Value是传递进来的值通过这两个key value创建Entry对象,当调用get()方法时,就是通过ThreadLocal本身为key,让线程在ThreadLocalMap中获取对应的value值,进而实现每个线程之间相互隔离的本地局部变量 那么结合前面了解到的四大引用的特点,思考一下ThreadLocalMap中的key为什么要使用弱引用,以下方代码为案例进行解释: 创建了ThreadLocal 变量,存储了数据"111",此时t1是强引用,当setVal()方法执行完毕后,栈帧销毁,那么t1对象跟随销毁掉,但是前面我们看源码了解到在ThreadLocal底层使用ThreadLocal自身为key,存储数据为value创建Entry对象后存储为ThreadLocalMap,如果这个key是强引用,会出现key引用指向的这个ThreadLocal与对应这个key的value都在引用中,即使setVal()方法执行完毕也不能被判定为不可达对象,进而不会被回收造成内存泄漏问题
public void setVal ( ) {
ThreadLocal < Integer > t1= new ThreadLocal < > ( ) ;
t1. set ( 111 ) ;
Integer i = t1. get ( ) ;
}
上面我们知道ThreadLoca使用弱引用的原因" Entry extends WeakReference<ThreadLocal<?>>" 防止一直是强引用,不能回收对象造成内存泄漏, 但是当外部的ThreadLocal被设置为null,根据可达性分析,这个ThreadLocal实例不在被任何链路引用,此时就会被回收掉,进而又引出一个问题ThreadLocal被回收为null,造成key为null了那么这个ThreadLocalMap中就变成了"< null, value >", 假如说持有这个ThreadLocalMap的线程迟迟不结束(例如使用线程池情况下),这些key为null的Entry就会一直存在,也会造成无法回收的问题,最终造成内存泄漏: 因此弱引用并不能百分百保证内存不泄漏问题,所以在使用ThreadLocal对象后,需要手动调用remove方法来删除它,防止线程池场景下线程一直不结束,大量key为null的Entry出现,或复用线程获取到上一个业务逻辑中创建的ThreadLocal数据 查看get(), set(), remove()源码会发现,在执行时会调用 expungeStaleEntry() 检查key是否为null,如果为null会设置对应的Entry为null, (下方remove()方法为例)
private void remove ( ThreadLocal < ? > key) {
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) ] ) {
if ( e. get ( ) == key) {
e. clear ( ) ;
expungeStaleEntry ( i) ;
return ;
}
}
}
五. 总结
在Thread中持有ThreadLocal.ThreadLocalMap 属性, ThreadLocalMap是ThreadLocal的静态内部类, 在ThreadLocalMap中有一个继承了WeakReference的静态内部类Entry, 说明这个Entry是弱引用,查看ThreadLocal中的get()方法的源码会发现, 在调用get方法获取数据时,首先会调用currentThread()获取到当前线程,获取该线程中的ThreadLocal.ThreadLocalMap属性,如果获取为空,会执行setInitialValue()方法初始化ThreadLocal, 最终会执行到一个createMap(t, value)方法,创建ThreadLocalMap, 在该方法中,首先会创建一个长度为16的Entry数组,创建一个Entry对象,key是当前ThreadLocal本身,value为存储的值,创建完成后,将这个Entry放入数组中 那么ThreadLocalMap中的Entry为什么设置为弱引用, 首先要了解一下四大引用: 强引用Reference, 软引用SoftReference, 弱引用WeakReference, 虚引用PhantomReference, 几种引用的特点
强引用对象: 只要还存在引用,垃圾回收时不会给回收掉 软引用对象: 垃圾回收时,如果内存不足才会被回收掉 弱引用对象: 只要垃圾回收都会被回收掉
前面了解到在存储ThreadLocal数据时ThreadLocalMap中以当前ThreadLocal本身做为key创建了Entry对象,就会造成key引用指向的这个ThreadLocal与对应这个key的value都在引用中,即使存储方法setVal()执行完毕也不能被判定为不可达对象,进而不会被回收造成内存泄漏问题, 所以将ThreadLocalMap中的Entry内部类设置为弱引用,只要执行垃圾回收都会被回收调 这样又引出一个问题,ThreadLocal被回收为null,造成key为null了那么这个ThreadLocalMap中就变成了"< null, value >" key为null的键值对, 假设持有当前ThreadLocalMap的线程一直不结束,这些key为null的Entry就会一直存在,也会造成无法回收的问题,最终造成内存泄漏: 因此弱引用并不能百分百保证内存不泄漏问题,所以在使用ThreadLocal对象后,需要手动调用remove方法来删除它,防止线程池场景下线程一直不结束,大量key为null的Entry出现,或复用线程获取到上一个业务逻辑中创建的ThreadLocal数据