ThreadLocal是一种线程隔离机制
, 使得每个线程都可以拥有自己独立的变量副本,从而避免了多线程环境下的线程安全问题。
public class Demo {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
static void print(String str){
System.out.println(str + ":" + threadLocal.get());
}
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("abc");
print("thread1 variable");
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("def");
print("thread2 variable");
}
});
thread1.start();
thread2.start();
}
}
内部结构和原理
最初的设计
每个ThreadLocal是自己维护一个ThreadLocalMap, key是当前线程, value是要存储的局部变量, 这样就可以达到各个线程的局部变量隔离的效果
JDK8的设计
每个Thread维护一个ThreadLocalMap, 这个Map的key是ThreadLocal本身, value是要存储的变量. 具体的流程如下
这样设计的优点:
ThreadLocal与synchronized对比
虽然ThreadLocal与synchronized都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用时间换空间的方法, 只提供了一份变量, 让不同的线程排队访问 | ThreadLocal采用空间换时间的方式, 为每一个线程提供了一份变量的副本, 从而实现同时访问, 互不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间数据相互隔离 |
源码分析
内存泄漏和内存溢出的区别
内存溢出 | 内存泄漏 | |
---|---|---|
定义 | 内存溢出指的是程序在运行过程中申请的内存超过了系统或者进程所能提供的内存大小(结果) | 内存泄漏指的是程序中已经不再需要的内存未被释放,造成系统内存的浪费(起因) |
原因 | 通常是由于程序中存在大量的内存申请,而且没有及时释放,导致系统的可用内存被耗尽 | 内存泄漏通常是由于程序中存在指针或引用,指向了不再使用的内存块,但程序却没有释放这些内存 |
表现 | 当内存溢出发生时,程序通常会崩溃,并且系统可能会报告无法分配内存的错误 | 内存泄漏不会导致程序立即崩溃,但随着时间的推移,系统可用内存会逐渐减少,最终可能导致系统变慢或者崩溃 |
总体来说,内存溢出是由于申请的内存过多,超出了系统限制,而内存泄漏是因为未能及时释放已经不再使用的内存。
解决内存溢出和内存泄漏的方法通常包括合理管理内存的申请和释放过程,使用合适的数据结构,以及利用内存管理工具进行检测和优化。
需要说明一点: 虽然内存泄漏可能会导致内存溢出,但内存溢出也可能是由于其他原因,例如程序中存在大量的内存申请,但这些内存并没有被泄漏,而是在程序执行期间一直保持被占用状态,最终导致系统内存耗尽。
强引用和弱引用的区别
Object obj = new Object(); // 强引用
WeakReference<Object> weakRef = new WeakReference<>(new Object()); // 弱引用
总结:
哪些情况下, ThreadLocal会导致内存泄漏?
public class MyRunnable implements Runnable {
private static ThreadLocal<MyObject> myThreadLocal = new ThreadLocal<>();
@Override
public void run() {
MyObject obj = new MyObject();
myThreadLocal.set(obj);
// 执行任务...
// 如果线程一直存活,myThreadLocal 将一直持有对 obj 的引用,即使任务执行完毕。
}
}
在这个例子中,即使任务执行完毕,ThreadLocal 对象仍然持有对 MyObject 的引用,而线程的生命周期可能会很长,导致 MyObject 无法被垃圾回收,从而引发内存泄漏。
为了避免这种情况,需要在不再需要 ThreadLocal 存储的对象时,显式调用 remove() 方法来清理 ThreadLocal。这样可以确保 ThreadLocal 对象中的弱引用被正确清理,从而防止内存泄漏。例如:
public class MyRunnable implements Runnable {
private static ThreadLocal<MyObject> myThreadLocal = new ThreadLocal<>();
@Override
public void run() {
try {
MyObject obj = new MyObject();
myThreadLocal.set(obj);
// 执行任务...
} finally {
// 清理 ThreadLocal,防止内存泄漏
myThreadLocal.remove();
}
}
}
如果在使用线程池的情况下,ThreadLocal被设置在某个任务中,而这个任务在线程池中执行完成后线程被放回线程池而不是销毁,那么ThreadLocal可能在下一次任务执行时仍然持有对上次设置的对象的引用。
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.submit(() -> {
MyObject obj = new MyObject();
myThreadLocal.set(obj);
// 执行任务...
// 线程被放回线程池,但 ThreadLocal 可能仍然持有对 obj 的引用。
});
为了避免这类问题,确保在ThreadLocal不再需要时,调用remove()方法清理它所持有的对象引用。这通常在任务执行结束时或者线程即将被销毁时执行。例如:
public class MyRunnable implements Runnable {
private static ThreadLocal<MyObject> myThreadLocal = new ThreadLocal<>();
@Override
public void run() {
try {
MyObject obj = new MyObject();
myThreadLocal.set(obj);
// 执行任务...
} finally {
// 清理 ThreadLocal,防止内存泄漏
myThreadLocal.remove();
}
}
}