1 问题
学习了尚硅谷周阳老师的JUC课程,关于ThreadLocal输出一篇博客!
- ThreadLocal是什么?具体应用场景?
- Thread、ThreadLocal、ThreadLocalMap三者关系?
- ThreadLocalMap的key为什么设计为弱引用?
- ThreadLocal内存泄漏及解决?
2 概述
ThreadLocal提供线程局部变量,这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(get/set)都有独立初始化的变量副本,ThreadLocal实例通常是类中的私有静态字段
,使用它的目的是希望将状态(事务ID/用户ID等上下文值)与线程关联起来
ThreadLocal实现了线程间的数据隔离,使每个线程都有专属的变量副本
- T get():返回此线程局部变量副本中的值
- void set(T value):将此线程局部变量的当前线程副本设置为指定值
- protected T initialValue():返回此线程局部变量的当前线程的“初始值”
- static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier):创建一个线程局部变量
- void remove():删除此线程局部变量的当前值
package tl;
import lombok.Getter;
import java.util.concurrent.CountDownLatch;
/**
* 需求1: 5个销售员卖车,准确统计全部销售的总量
* <p>
* 需求2: 5个销售员卖车,记录每个销售的各自销量
*
* @author linxh
* @date 2023/02/21
*/
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
Car car = new Car();
int length = 5;
CountDownLatch countDownLatch = new CountDownLatch(length);
for (int i = 0; i < length; i++) {
new Thread(() -> {
int size = (int) (Math.random() * 5 + 1);
try {
for (int j = 0; j < size; j++) {
car.saleByLock();
countDownLatch.countDown();
car.saleBySelf();
}
System.out.println(Thread.currentThread().getName() + "单独卖出\t" + car.getThreadLocal().get());
} finally {
car.getThreadLocal().remove();
}
}, "销售员" + (i + 1)).start();
}
countDownLatch.await();
System.out.println("全部销售员合计卖出\t" + car.getCount());
}
}
class Car {
@Getter
private Integer count = 0;
public synchronized void saleByLock() {
this.count++;
}
@Getter
private final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public void saleBySelf() {
threadLocal.set(threadLocal.get() + 1);
}
}
3 源码分析
使用下面一段代码作为调试入口
public static void main(String[] args) {
ThreadLocal<Integer> threadLocal1 = ThreadLocal.withInitial(() -> 0);
ThreadLocal<String> threadLocal2 = ThreadLocal.withInitial(() -> "");
ThreadLocal<Integer> threadLocal3 = ThreadLocal.withInitial(() -> 0);
threadLocal1.set(1);
threadLocal2.set("test");
threadLocal3.set(3);
System.out.println(threadLocal3.get());
threadLocal1.remove();
threadLocal2.remove();
threadLocal3.remove();
}
- public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) :该方法入参为供给型lambda表达式需提供一个初始值,SuppliedThreadLocal类型继承了ThreadLocal因此返回ThreadLocal对象,此方法仅是初始化ThreaLocal对象而已
- 在main线程中创建了三个ThreadLocal对象,那么这三个ThreadLocal对象有和当前线程有什么关联呢?上面说了withInitial方法仅是初始化了ThreadLocal对象并没关联上任何线程。那直接查看set方法源码可以看到,是从当前线程即main线程中获取了一个ThreadLocalMap对象,此时查看Thread类源码发现ThreadLocalMap是Thread的实例变量,而ThreadLocalMap又是ThreadLocal的内部类
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
- 如下调试结果,当前线程t为main,在执行完第三次set方法查看main线程中的实例变量ThreadLocalMap的table数据,threadLocal1的值1存放在数组下标1,threadLocal2的值“test”存放在数组下表8,threadLocal3的值3存放在数组下表15。此时估计大家都很清楚ThreadLocalMap的内部结构了!
- 查看ThreadLocal.ThreadLocalMap源码可以发现
- 类似HashMap内部维护了一个键值对Entry对象数组table
- Entry对象的key为ThreadLocal,value为我们设置的值
- ThreadLocal为弱引用
- 因此可以得出调用get()/set()方法的时候是通过key(ThreadLoccal对象)进行自定义hash算法得到对应的table下标进行访问的(具体哈希算法就不分析了)
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// ...
private Entry[] table;
// ...
}
- 当ThreadLocalMap对象为null时会进入else语句块中调用createMap方法直接创建Entry对象(没有好说的),但是当ThreadLocalMap对象不为null的时候会调用该对象的set方法进行赋值,下面截取set方法一部分核心源码进行分析
private void set(ThreadLocal<?> key, Object value) {
// table循环遍历不为null的Entry对象,目的是查看这些Entry是否存在和入参ThreadLocal对象相等的key(做替换),key为null的Entry(做脏Entry的替换)
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// 获取当前Entry的ThreadLocal对象
ThreadLocal<?> k = e.get();
// key重复替换value,并结束当前方法
if (k == key) {
e.value = value;
return;
}
// key为null替换脏Entry,并结束当前方法
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 上面遍历的table下标的Entry,既不存在key为null的Entry,也不存在重复的ThreadLocal对,就直接新增一个Entry并清理一些key为null的table插槽,也是做脏Entry清理,即当前方法中使用replaceStaleEntry和cleanSomeSlots方法清理ThreadLocalMap中的脏Entry
tab[i] = new Entry(key, value);
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
- 查看replaceStaleEntry和cleanSomeSlots方法清除脏Entry都是调用expungeStaleEntry方法进行清除的
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 将当前下标Entry的value和Entry都置为null
tab[staleSlot].value = null;
tab[staleSlot] = null;
// 容量-1(非数组长度,是table中的元素总个数)
size--;
// ...
// 上面得到空key的Entry索引,下面将通过重新散列(哈希)位于这个索引到下一个key为null的Entry之间的任何哈希碰撞(else代码块),还会删除尾随key为null的脏Entry(if代码块)
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
// 清除staleSlot索引后面的空key脏Entry
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
// hash冲突处理,将发生hash冲突的Entry前面的索引Entry置为null,找到一个可用索引槽位存放冲突的Entry。
//个人理解:不同的hash算法都需要解决hash冲突,类似HashMap解决冲突使用的是链表和红黑树,而ThreadLocalMap则是直接使用前面的Entry替换了后面的Entry,并清理前面Entry的槽位
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
- 类似的get方法源码的调试就不演示了,remove方法的源码就是上面expungeStaleEntry而已
4 开发规约
package tl;
import lombok.Getter;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 阿里规约明确指出必须回收自定义的ThreadLocal变量
*
* @author linxh
* @date 2023/02/21
*/
public class ThreadLocalDemo2 {
@Getter
private final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public void add() {
this.threadLocal.set(threadLocal.get() + 1);
}
public static void main(String[] args) {
ThreadLocalDemo2 demo = new ThreadLocalDemo2();
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(3, 5, 5, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5));
try {
for (int i = 0; i < 10; i++) {
threadPoolExecutor.submit(() -> {
Integer beforeValue = demo.getThreadLocal().get();
demo.add();
Integer afterValue = demo.getThreadLocal().get();
System.out.println(Thread.currentThread().getName() + "\tbeforeValue:" + beforeValue + "\tafterValue:" + afterValue);
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPoolExecutor.shutdown();
}
}
}
pool-1-thread-1 beforeValue:0 afterValue:1
pool-1-thread-4 beforeValue:0 afterValue:1
// 由于上面的线程1使用完局部变量未清理,导致再次复用线程1初始值错误,需要在finally块里手动调用remove()方法
// 第二个问题是有可能存在内存泄漏的问题
pool-1-thread-1 beforeValue:1 afterValue:2
pool-1-thread-5 beforeValue:0 afterValue:1
pool-1-thread-3 beforeValue:0 afterValue:1
pool-1-thread-2 beforeValue:0 afterValue:1
pool-1-thread-3 beforeValue:1 afterValue:2
pool-1-thread-5 beforeValue:1 afterValue:2
pool-1-thread-1 beforeValue:2 afterValue:3
pool-1-thread-4 beforeValue:1 afterValue:2
5 Java引用及ThreadLocal内存泄漏
1)强软弱虚
-
强引用
- 当内存不足,JVM开始垃圾回收,对于强引用的对象就算时出现了OOM也不会对该对象进行回收。
- Java中最常见的就是强引用,把一个对象赋值给一个引用变量,这个引用变量就是一个强引用
- 当一个对象被强引用变量引用时,它处于可达状态此时时不会被垃圾回收机制回收的
- 对于一个普通的对象,如果没有其它的引用关系,只要超过了引用的作用域或者显式地将相应的引用赋值为null,一般认为就是可以被垃圾收集(具体看虚拟机的垃圾回收策略)
-
软引用
- 软引用时一种相对于强引用弱化了的引用,需要用java.lang.ref.SoftReference类来实现
- 当系统内存充足时弱引用的对象不回被回收,当系统内存不足时弱引用对象会被回收
- 软引用通常用于在对内存敏感的程序中,类似高速缓存的实现可使用弱引用:内存足够时候就保留,不够就回收
-
弱引用
- 弱引用需要用java.lang.ref.WeakReference类来实现
- 对于弱引用对象,只要JVM虚拟机只要执行垃圾回收,不管JVM内存是否足够都会回收该弱引用对象
-
虚引用
- 虚引用需要java.lang.ref.PhantomReference类来实现
- 虚引用必须和引用队列(ReferenceQueue)联合使用
- 如果一个对象仅持有虚引用,那么他就和没有任何引用一样,在任何时候都可能被垃圾收集器回收
- PhantomReferen的get方法总是返回null,因此无法访问对应的引用对象
- 虚引用的主要作用时跟踪对象被垃圾回收的状态,仅仅时提供了一种确保对象被finalize以后,做某些事情的通知机制,用来实现比finalize机制更灵活的回收操作
package tl;
import java.lang.ref.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
class MyObject {
/**
* 对象GC前回调的方法
*/
@Override
protected void finalize() throws Throwable {
System.out.println("MyObject执行清除!!!");
}
}
/**
* @author linxh
* @date 2023/02/21
*/
public class ReferenceDemo {
public static void main(String[] args) throws InterruptedException {
//strongReference();
//softReference();
//weakReference();
//phantomReference();
}
private static void weakReference() throws InterruptedException {
WeakReference<MyObject> weakReference = new WeakReference<>(new MyObject());
System.out.println("gc before:\t" + weakReference.get());
System.gc();
TimeUnit.SECONDS.sleep(1);
System.out.println("gc after:\t" + weakReference.get());
}
/**
* -Xms10m -Xmx10m
*/
private static void softReference() throws InterruptedException {
SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
System.gc();
TimeUnit.SECONDS.sleep(1);
System.out.println("gc 内存够用:\t" + softReference.get());
try {
byte[] bytes = new byte[10 * 1024 * 1024];
} finally {
System.out.println("gc 内存不够用:\t" + softReference.get());
}
}
private static void strongReference() throws InterruptedException {
MyObject myObject = new MyObject();
System.out.println("gc before:\t" + myObject);
// 根可达算法,只要引用一直不断开,GC就不会回收,手动断开
myObject = null;
// 人工开启GC
System.gc();
TimeUnit.SECONDS.sleep(1);
System.out.println("gc after:\t" + myObject);
}
/** 虚引用场景不好复现,没有demo */
private static void phantomReference() {}
}
2)内存泄漏原因
有了上面源码分析和Java引用的基础下面就分析一下ThreadLocal内存泄漏问题
当我们创建ThreadLocal对象,实际上就是往当前Thread对象的ThreadLocal.ThreadLocalMap实例变量存放一个Entry。而Entry的key是当前ThreadLocal对象且为弱引用,当methodA方法栈执行完/当前线程结束,这个ThreadLocal的强引用被指为null,根据可达性分析这个ThreadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal实例就会被垃圾收集器回收(Hospot)
如果:当前thread运行结束,ThreadLocal、ThreadLocalMap、Entry对象均没有引用链可达,都会收垃圾收集器回收
但是:实际开发中,methodA方法栈出栈后强引用置为null且k是弱引用,这时候ThradLocal对象势必被垃圾收集器回收,但此时value不为null。如果我们使用的是线程池去维护我们的线程,这时候的线程是可复用且不会结束的,就会导致一种情况是:ThreadLocalMap中出现key为null,value不为null的Entry,且我们没有办法访问这些Entry(key=null)中的value,由于这个线程结束不了这些key为null的Entry中的value就会一直存在一条强引用链:Thread Ref =》Thread =》ThreadLocalMap =》Entry =》value,那么这个value就永远无法回收造成内存泄漏
3)解决方法
阿里规约强制
要求必须回收自定义的ThreadLocal变量,因此在finally块中手动调用ThreadLocal对象的remove()方法,此外当我们调用get()和set()方法的时候也会帮我们清理脏Entry(key = null),具体清理脏Entry的三个方法为:expungeStaleEntry
、replaceStaleEntry、cleanSomeSlots
- 调用set()方法时,采样清理、全量清理,扩容时还会继续检查
- 调用get()方法,没有直接命中,向后查找时会清理
- 调用remove()时,除了清理当前Entry,还会向后继续清理和解决碰撞