🍰 个人主页:不摆烂的小劉
🍞文章有不合理的地方请各位大佬指正。
🍉文章不定期持续更新,如果我的文章对你有帮助➡️ 关注🙏🏻 点赞👍 收藏⭐️
PageHelper 分页查询【底层代码-图文分析】【原理篇】探讨了PageHelper
的底层原理——拦截MyBatis
的查询操作,在Sql
语句执行前动态地添加分页逻辑,从而实现数据的分页查询。
pagehelper常见问题【分页失效】【ThreadLocal污染线程】探讨了 Pagehelper
使用过程中频繁出现的问题,包括分页失效、多 limit
、 异常查询全部数据等情况,解释了出现这些问题的原因,并给出解决措施。
接着这个问题继续研究ThreadLocal
。
Pagehelper
为什么使用ThreadLocal
存储分页参数?
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("123456789");
System.out.println(threadLocal.get());
}
上面代码做了什么?怎么做的?有什么风险问题?怎么解决?
文章目录
一、前置知识
1.什么是内存溢出和内存泄露,有什么联系?
内存溢出(OOM
)
- 指程序在运行过程中,申请的内存空间超过了系统所能提供的最大内存空间。
- 好比一个容器,已经装满了东西,再也无法容纳更多,一旦继续往里放,就会溢出。
- 常出现情景:
一次性加载大量数据到内存中;
也可能是因为程序中存在无限循环或递归调用,导致内存不断被占用且无法释放。
内存泄露
- 程序在运行过程中,不断申请内存却没有及时释放不再使用的内存空间,导致可用内存逐渐减少。
- 这类似于一个人不断向仓库里存放物品,却从不清理不再需要的东西,使得仓库空间越来越小。
- 常出现情景:
忘记关闭文件流、数据库连接等资源,导致这些资源所占用的内存无法被回收;
在容器类对象中不断添加元素,却没有合理的删除机制
联系:内存泄露是导致内存溢出的一个重要原因。随着内存泄露的不断积累,可用内存越来越少,最终可能引发内存溢出。
2.四种引用类型
1.强引用
new
关键字创建的对象所关联的引用就是强引用。- 只要强引用存在,垃圾回收器就不会回收被引用的对象,即使内存空间不足时也不会考虑回收它,而是会直接抛出
OutOfMemoryError
【内存溢出】错误,直到没有强引用指向该对象时,才会考虑回收它以释放内存
Product product = new Product(); // 强引用product指向堆内存中Product对象
product = null; // 强引用product 被设置为 null 时,不可达,则Product对象被回收
2.软引用
- 内存充足时,所指对象不会回收;内存不足时,垃圾回收器会优先回收它以释放空间,避免内存溢出。它适用于实现缓存。
- 如果回收了软引用对象之后仍然没有足够的内存,才会抛出
OutOfMemoryError
【内存溢出】错误。 - 例
SoftReference<Object> softRef = new SoftReference<>(new Object());
3.弱引用
- 使内存足够,垃圾回收器就可以随时回收该对象。
- 弱引用适用于需要临时引用对象的场景,如临时缓存或临时存储对象。
- 例,
ThreadLocal
中的key
弱引用
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
4.虚引用
- 虚引用的主要作用不是为了获取对象的引用,而是用于在对象被回收时,接收一个系统通知。
- 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收该对象之前,把这个虚引用加入到与之关联的引用队列中,以便程序能得知对象即将被回收的消息。
- 如对象资源释放或日志记录,常用于跟踪对象被垃圾回收的状态,执行一些清理工作。
二、ThreadLocal
1.ThreadLocal
是什么?解决什么问题?
定义
- ThreadLocal提供了线程局部变量的功能,允许每个线程都有自己独立的变量副本,这些变量副本对于其他线程是不可见的(隔离性)。
- 数据结构角度看,
ThreadLocal
内部维护了一个类似于Map的结构,其中键(Key
)是线程对象(Thread
),值(Value
)是该线程对应的变量副本
解决问题:方便传递参数,用户身份信息存储在ThreadLocal
变量中,在这个线程的各个方法中都可以方便地获取
2.ThreadLocal
底层如何存储
下面代码是什么?做了什么?怎么做的
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("123456789");
System.out.println(threadLocal.get());
}
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
进入ThreadLocal
看一下上面代码做了什么?
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// 初始化
protected T initialValue() {
return null;
}
threadLocalHashCode
作用
- 计算
ThreadLocal
对象在内部数组中的存储位置 - 区分不同的
ThreadLocal
对象,没有这个唯一的哈希码,不同的ThreadLocal
对象可能会错误地覆盖
- 存储变量
threadLocal.set
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
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);
}
Thread.currentThread();
先获取当前线程对象getMap(t);
获取当前线程ThreadLocalMap
类型变量副本- 其中
ThreadLocalMap
中也定义了一个内部类Entry
如下
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry
中的key
是ThreadLocal
,value
为咱们要存储的值
所以根据上述代码,线程第一次执行threadLocal.set
时
-
获取当前线程变量副本
-
创建一个
Entry
的数 -
计算存储位置,并存储
private void set(ThreadLocal<?> key, Object value) {
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
再次执行set
方法时
- 计算存储位置,如果有冲突,使用线性探测的方式循环寻找新的插入位置
- 插入元素
- 调整容器大小
3.获取变量threadLocal.get()
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
- 获取当前线程对象
- 获取当前线程的变量副本
ThreadLocalMap
- 调用
map.getEntry
方法,根据threadLocalHashCode
获取元素
三、ThreadLocal
引发的问题
1.ThreadLocal内存泄漏
ThreadLocal
的生命周期结束。如果栈内存中的非静态变量ThreadLocal
引用,方法调用结束后,回收栈内存ThreadLocal
引用,此时只有key
弱引用指向ThreadLocal
对象,下次垃圾回收,会立刻将弱引用 对象回收。key
变成了null
,但是value
还是强引用,对象还在堆里,无法使用,造成了内存泄露jdk
设计这做了优化,调用get/set/remove
的时候,自动把key
为null
的东西删除了,以便下一次GC
把value
回收掉
ThreadLocal
实例通常是作为类的静态成员变量存在的
调用remove()
在垃圾回收的时候,及时将Entry
清理掉。
2.线程复用到导致ThreadLocal污染
- 使用线程池的场景中,线程会被复用。
ThreadLocal
是为每个线程提供独立的变量副本,以实现线程间的数据隔离。- 当线程被复用后,如果前一个任务在
ThreadLocal
中设置了某些值,而后续任务没有正确清理,就可能会出现 “污染” 的情况。
private static ThreadLocal<Integer> pageNum = new ThreadLocal<>();
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
1, // 核心线程数为1
1, // 最大线程数为1
1L, // 线程存活时间1分钟
TimeUnit.MINUTES,
new LinkedBlockingQueue<>(5) // 等待队列大小为5
);
threadPool.submit(() -> {
pageNum.set(1);
System.out.println("Task 1: User ID = " + pageNum.get());//Task 1: User ID = 1
// 清理ThreadLocal
// pageNum.remove();
});
threadPool.submit(() -> {
// 线程复用
System.out.println("Task 2: User ID = " + pageNum.get());//Task 2: User ID = 1
});
// 关闭线程池
threadPool.shutdown();
}
- 在每个任务结束后,及时清理
ThreadLocal
中的值,这样可以避免前一个任务的值对后续任务产生影响。 - 分页插件会在查询后自动清理分页参数,如果
PageHelper.startPage
设置参数后未跟查询或手动清理分页参数,出现线程数据污染,这种情况可以使用PageHelper.clearPage()
手动清理
四、思考
1.ThreadLocal 定义为静态变量优点?
- 避免频繁创建和销毁:非静态的
ThreadLocal
变量会随着对象的创建和销毁而产生和消失。如果在一个频繁创建和销毁对象的场景中使用非静态ThreadLocal
,会导致大量的ThreadLocal
对象的创建和销毁操作。 - 静态变量被整个类所共享。方便跨方法传递数据,在没有
ThreadLocal
的情况下,如果一个线程需要共享数据时需要方法传递,方法调用层次的深和参数数量多,这种参数传递方式可能会变得非常复杂和混乱。 - 在使用线程池的场景下,线程会被复用,方便存取。静态
ThreadLocal
可以让线程在多次复用过程中方便地设置和获取数据。当一个线程再次被分配任务时,它可以通过之前的静态ThreadLocal
来获取之前存储的数据,或者设置新的数据来覆盖旧的数据【使用分页插件时,某个线程多次调用PageHelper.startPage
方法,而不清除,只会覆盖分页参数】 ThreadLocal
使用弱引用本意是内存回收;但业务中常常定义为一个全局静态常量,导致了弱引用失去了他本身的作用
2.ThreadLocal
线程隔离性?
每个线程都有自己独立的ThreadLocalMap
来存储该线程对应的ThreadLocal
变量。
参考:
1.ThreadLocal源码
2.面试官:ThreadLocal为什么会导致内存泄漏?如何解决的?
3.阿里二面:谈谈ThreadLocal的内存泄漏问题?问麻了。。。。
4.内存泄漏和内存溢出的区别和联系
5.ThreadLocal内存泄漏问题
6.强引用、弱引用、软引用和虚引用
7.什么是内存溢出,内存泄漏?以及解决方案
8.ThreadLocal的使用及其原理
🍉文章不定期持续更新,如果我的文章对你有帮助请 关注⚡ 点赞👍 收藏⭐️