ThreadLocal即线程本地变量
, 是一种线程隔离机制
, 如果你创建了一个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();
}
}
并发场景下, 会存在多个线程同时修改一个共享变量的情况, 这就会导致线程安全问题
为了解决线程安全问题, 可以使用加锁的方式, 例如使用synchronized
或者Lock
, 但是加锁的方式可能会导致系统变慢, 示意图如下:
还有另外一种方案,就是使用空间换时间的方式,即使用ThreadLocal
。使用ThreadLocal类访问共享变量时,会在每个线程的本地,都保存一份共享变量的拷贝副本。多线程对共享变量修改时,实际上操作的是这个变量副本,从而保证线性安全。
ThreadLocal与Synchronized对比
虽然ThreadLocal与synchronized都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同
Synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用时间换空间的方法, 只提供了一份变量, 让不同的线程排队访问 | ThreadLocal采用空间换时间的方式, 为每一个线程提供了一份变量的副本, 从而实现同时访问, 互不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间数据相互隔离 |
最初的设计
每个ThreadLocal是自己维护一个ThreadLocalMap, key是当前线程, value是要存储的局部变量, 这样就可以达到各个线程的局部变量隔离的效果
JDK8的设计
每个Thread维护一个ThreadLocalMap, 这个Map的key是ThreadLocal本身, value是要存储的变量. 具体的流程如下
这样设计的优点:
源码分析
内存溢出 | 内存泄漏 | |
---|---|---|
定义 | 内存溢出指的是程序在运行过程中申请的内存超过了系统或者进程所能提供的内存大小(结果) | 内存泄漏指的是程序中已经不再需要的内存未被释放,造成系统内存的浪费(起因) |
原因 | 通常是由于程序中存在大量的内存申请,而且没有及时释放,导致系统的可用内存被耗尽 | 内存泄漏通常是由于程序中存在指针或引用,指向了不再使用的内存块,但程序却没有释放这些内存 |
表现 | 当内存溢出发生时,程序通常会崩溃,并且系统可能会报告无法分配内存的错误 | 内存泄漏不会导致程序立即崩溃,但随着时间的推移,系统可用内存会逐渐减少,最终可能导致系统变慢或者崩溃 |
总体来说,内存溢出是由于申请的内存过多,超出了系统限制,而内存泄漏是因为未能及时释放已经不再使用的内存。
解决内存溢出和内存泄漏的方法通常包括合理管理内存的申请和释放过程,使用合适的数据结构,以及利用内存管理工具进行检测和优化。
需要说明一点: 虽然内存泄漏可能会导致内存溢出,但内存溢出也可能是由于其他原因,例如程序中存在大量的内存申请,但这些内存并没有被泄漏,而是在程序执行期间一直保持被占用状态,最终导致系统内存耗尽。
Object obj = new Object(); // 强引用
WeakReference<Object> weakRef = new WeakReference<>(new Object()); // 弱引用
总结:
ThreadLocalMap中使用key为ThreadLocal
的弱引用, 而value是强引用. 所以, 当ThreadLocal没有被外部强引用时, 在垃圾回收时, key会被清理掉, 而value不会被清理. 这样一来就会导致ThreadLocalMap中就会出现key为null的Entry, 假如我们不做任何措施的话,value永远无法被GC 回收,这个时候就可能会产生内存泄露。
ThreadLocalMap实现中已经考虑了这种情况,在调用set()、get()、remove()方法的时候,会清理掉key为null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法
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();
}
}
}