目录
? ?ThreadLocal
是 Java 中的一个类,它提供了线程本地变量的支持。线程本地变量是指被线程拥有并独立于其他线程的变量。每个线程都可以独立地改变自己的副本,而不会影响其他线程的副本。ThreadLocal
主要用于在多线程环境下保持变量的线程封闭性,以实现线程安全。
1. set(T value)
用于设置当前线程的线程本地变量的值。
参数?value
?是要设置的值。
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Hello, ThreadLocal!");
?2.?get()
用于获取当前线程的线程本地变量的值。
ThreadLocal<String> threadLocal = new ThreadLocal<>();
String value = threadLocal.get();
3.?remove()
用于移除当前线程的线程本地变量。
在一些情况下,手动调用?remove()
?可以帮助避免内存泄漏。
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.remove();
?4.?initialValue()
该方法是一个 protected 方法,可以被子类重写以提供线程本地变量的初始值。默认情况下,initialValue()
?返回?null。
通常情况下,我们会通过?ThreadLocal
?的子类并重写?initialValue()
?方法来设定线程本地变量的初始值。
public class MyThreadLocal extends ThreadLocal<String> {
@Override
protected String initialValue() {
return "Default Value";
}
}
// 使用自定义的 MyThreadLocal
MyThreadLocal myThreadLocal = new MyThreadLocal();
String value = myThreadLocal.get(); // 返回 "Default Value"
?上述方法综合使用如下:
public class ThreadLocalExample {
// 创建一个 ThreadLocal 实例
private static final ThreadLocal<String> threadLocalValue = new ThreadLocal<>();
public static void main(String[] args) {
// 在主线程设置值
threadLocalValue.set("Main Thread Value");
// 创建两个子线程
Thread thread1 = new Thread(() -> {
// 在子线程1获取值
String value = threadLocalValue.get();
System.out.println("Thread 1: " + value); // 输出:Thread 1: null
});
Thread thread2 = new Thread(() -> {
// 在子线程2设置值
threadLocalValue.set("Thread 2 Value");
// 在子线程2获取值
String value = threadLocalValue.get();
System.out.println("Thread 2: " + value); // 输出:Thread 2: Thread 2 Value
});
// 启动子线程
thread1.start();
thread2.start();
// 在主线程获取值
String mainThreadValue = threadLocalValue.get();
System.out.println("Main Thread: " + mainThreadValue); // 输出:Main Thread: Main Thread Value
// 清理主线程的值
threadLocalValue.remove();
}
}
? ? ? ?在上述案例中可以看出,每个线程中的Threadlocal变量都是独立的副本。ThreadLocal
提供了一种在多线程环境下安全地存储和访问线程本地变量的机制。每个线程都可以独立地对其进行操作,互不干扰。
任务中不执行任何有意义的代码
//? 将堆内存大小设置为-Xmx256m
public class ThreadLocalMemoryLeak {
private static final int TASK_LOOP_SIZE = 500;
/*线程池*/
final static ThreadPoolExecutor poolExecutor
= new ThreadPoolExecutor(5, 5, 1,
TimeUnit.MINUTES,
new LinkedBlockingQueue<>());
static class LocalVariable {
private byte[] a = new byte[1024*1024*5];/*5M大小的数组*/
}
ThreadLocal<LocalVariable> threadLocalLV;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
poolExecutor.execute(new Runnable() {
public void run() {
// LocalVariable localVariable = new LocalVariable();
// ThreadLocalMemoryLeak oom = new ThreadLocalMemoryLeak();
// oom.threadLocalLV = new ThreadLocal<>();
// oom.threadLocalLV.set(new LocalVariable());
// oom.threadLocalLV.remove();
System.out.println("use local varaible");
}
});
}
System.out.println("pool execute over");
}
}
执行后,我们通过cmd输入jvisualvm启动java性能监控工具(jdk自带),查看当前的性能消耗情况。
查看内存会发现内存消耗稳定在25MB左右。
在每个任务中new出一个数组,执行完成后我们可以看见,内存占用基本和场景1同
public class ThreadLocalMemoryLeak {
private static final int TASK_LOOP_SIZE = 500;
/*线程池*/
final static ThreadPoolExecutor poolExecutor
= new ThreadPoolExecutor(5, 5, 1,
TimeUnit.MINUTES,
new LinkedBlockingQueue<>());
static class LocalVariable {
private byte[] a = new byte[1024*1024*5];/*5M大小的数组*/
}
ThreadLocal<LocalVariable> threadLocalLV;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
poolExecutor.execute(new Runnable() {
public void run() {
LocalVariable localVariable = new LocalVariable();
// ThreadLocalMemoryLeak oom = new ThreadLocalMemoryLeak();
// oom.threadLocalLV = new ThreadLocal<>();
// oom.threadLocalLV.set(new LocalVariable());
// oom.threadLocalLV.remove();
System.out.println("use local varaible");
}
});
}
System.out.println("pool execute over");
}
}
启动后内存使用情况如下
?我们可以看到有毛刺现象,这是因为GC造成的,但是没有出现内存泄漏的情况。
启用Threadlocal
public class ThreadLocalMemoryLeak {
private static final int TASK_LOOP_SIZE = 500;
/*线程池*/
final static ThreadPoolExecutor poolExecutor
= new ThreadPoolExecutor(5, 5, 1,
TimeUnit.MINUTES,
new LinkedBlockingQueue<>());
static class LocalVariable {
private byte[] a = new byte[1024*1024*5];/*5M大小的数组*/
}
ThreadLocal<LocalVariable> threadLocalLV;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
poolExecutor.execute(new Runnable() {
public void run() {
// LocalVariable localVariable = new LocalVariable();
ThreadLocalMemoryLeak oom = new ThreadLocalMemoryLeak();
oom.threadLocalLV = new ThreadLocal<>();
oom.threadLocalLV.set(new LocalVariable());
// oom.threadLocalLV.remove();
System.out.println("use local varaible");
}
});
Thread.sleep(100);
}
System.out.println("pool execute over");
}
}
启动后内存使用情况如下
占用内存情况在100MB-125MB左右。
手动调用remove清除threadlocal,执行查看内存情况。
public class ThreadLocalMemoryLeak {
private static final int TASK_LOOP_SIZE = 500;
/*线程池*/
final static ThreadPoolExecutor poolExecutor
= new ThreadPoolExecutor(5, 5, 1,
TimeUnit.MINUTES,
new LinkedBlockingQueue<>());
static class LocalVariable {
private byte[] a = new byte[1024*1024*5];/*5M大小的数组*/
}
ThreadLocal<LocalVariable> threadLocalLV;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
poolExecutor.execute(new Runnable() {
public void run() {
// LocalVariable localVariable = new LocalVariable();
ThreadLocalMemoryLeak oom = new ThreadLocalMemoryLeak();
oom.threadLocalLV = new ThreadLocal<>();
oom.threadLocalLV.set(new LocalVariable());
oom.threadLocalLV.remove();
System.out.println("use local varaible");
}
});
Thread.sleep(100);
}
System.out.println("pool execute over");
}
}
启动后内存使用情况如下
此时内存使用情况跟场景1相同。?
总结:通过以上场景3,我们可以得出在启用了threadlocal以后发生了内存泄漏。
? ? ? ? 每一个Thread线程都维护了一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal 实例本身,value 是真正需 要存储的Object,也就是说ThreadLocal本身并不存储值,它只是作为一个 key来让线程从ThreadLocalMap获取value。ThreadLocalMap是使用ThreadLocal的弱引用作为Key的,弱引用的对象在GC 时会被回收。
? ? ? ? 图中虚线表示弱引用。
? ? ? ? 当把 threadlocal 变量置为 null 以后,没有任何强引用指向 threadlocal 实例,所以 threadlocal将会被gc回收。这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前 线程再迟迟不结束的话,这些key为null的Entry的 value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块value永远不会被访问到了,所以存在着内存泄露。
? ? ? ?只有当前线程结束后,线程就不会在栈中,强引用断开,才会被GC回收掉。解决内存泄漏做法是当不使用线程中threadlocal变量时及时remove清除数据。
? ? ? ?场景3中,虽然线程池里面的任务执行完毕了,但是线程池里面的5个线程会一直存在直到JVM退出,我们set了线程的localVariable变量后没有调用 localVariable.remove()方法,导致线程池里面的5个线程的threadLocals变量里面的new LocalVariable()实例没有被释放。当我们手动再每次都调用remove清除数据时,内存正常。
? ? ? ThreadLocal的实现中,无论是get()、set()在某些时候,调用了expungeStaleEntry方法用来清除Entry中Key为null的Value,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。 只有 remove()方法中显式调用了expungeStaleEntry 方法。
? ? ? ?JVM利用设置ThreadLocalMap的Key为弱引用,来避免内存泄露。
? ? ? ?JVM 利用调用remove、get、set方法的时候,回收弱引用。
? ? ? ?当ThreadLocal存储很多Key为 null的Entry的时候,而不再去调用 remove、 get、set 方法,那么将导致内存泄漏。
? ? ? ? 使用线程池+ ThreadLocal 时要多注意,这种情况下,线程是一直在不断的重复运行的,从而也就造成了value可能造成累积的情况。