高并发篇_9 ThreadLocal原理分析
田超凡ThreadLocal基本概念ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定,ThreadLocal适用于在多线程的情况下,实现线程之间的数据隔离,进而解决线程安全性问题ThreadLocal常用APIset(T value) 设置当前线程缓存
田超凡
原创博文,仿冒必究,部分素材转载自每特教育蚂蚁课堂
1 ThreadLocal基本概念
ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。
ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定,ThreadLocal适用于在多线程的情况下,实现线程之间的数据隔离,进而解决线程安全性问题
2 ThreadLocal常用API
- set(T value) 设置当前线程缓存变量的值
- get() 获取当前线程缓存变量的值
- remove() 移除当前线程缓存变量的值
3 ThreadLocal应用场景
1 Spring AOP声明式事务管理,设置事务只读
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
2 Spring MVC/Spring AOP增强处理获取HttpServletRequest
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
3 分布式事务解决方案LCN/Seata生成全局事务ID
4 ThreadLocal和Synchronized的区别
共同点:Synchronized和ThreadLocal都可以用来解决并发场景下的线程安全问题
不同点:
Synchronized是悲观锁的实现方式,它的核心思想是当多个线程同时竞争锁的时候,保证同一时刻只有一个线程能够竞争到这把锁,其他没有竞争到锁的线程会立刻阻塞到内核态,典型的时间换空间的实现方式:多线程竞争锁消耗大量时间(用户态和内核态频繁切换),带来的好处就是不会消耗CPU资源,节省CPU资源占用空间。(时间换空间)
ThreadLocal是线程隔离的实现方式,通过给每个线程都创建一个ThreadLocalMap本地变量副本来存放当前线程的缓存变量值,不同线程之间缓存变量的值完全隔离。
典型的空间换时间的实现方式:每个线程都需要创建一个本地变量副本(ThreadLocalMap),消耗大量内存空间,但是带来的好处是可以节省线程执行的时间(只运行在用户态,不需要在用户态和内核态频繁切换)(空间换时间)
5 内存溢出和内存泄漏
内存溢出:内存已经被占满了,当前程序申请分配内存将会失败(无法再次给当前程序分配内存空间)的问题
解决方案:内存扩容、释放部分程序占用的内存资源
内存泄漏:一开始内存没有被占满,但是有部分长时间占用到内存资源的线程一直占用着,不消耗内存资源,并最终导致内存占满的问题
解决方案:JVM GC垃圾回收、JVM性能调优
6 强、弱、软、虚引用
强引用:无论内存空间占用情况如何(还有没有可用的内存空间,或者已经OOM),当前对象都不会被回收(死都不回收)
弱引用:无论内存空间占用情况如何,只要是触发了JVM GC垃圾回收,当前对象就会立刻被回收(只要JVM GC就回收),依赖java.lang.ref.WeakReference类实现
软引用:当内存空间足够的时候,当前对象不会被回收;当内存空间不足的时候,当前对象会被回收(看内存占用情况决定是否回收)
虚引用:顾名思义,该操作是一个形同虚设的概念,不介入实际的对象回收周期(形同虚设),依赖java.lang.ref.PhantomReference类实现
7 ThreadLocal实现原理
ThreadLocal通过给每个线程都创建一个ThreadLocalMap来存放当前线程缓存变量的值,不同线程之间缓存变量是完全隔离的,互不可访问的,而ThreadLocalMap底层是通过Entry来存储缓存变量的,Entry 通过弱引用指向Key并对应一个Value,这个Key就是当前线程的ThreadLocal实例,Value就是当前线程缓存变量的值
set() 实现原理:
- 获取当前线程的本地变量ThreadLocalMap
- 如果没有获取到本地变量ThreadLocalMap,则创建一个空的ThreadLocalMap并通过构造注入存入到对应的Entry中(Key是当前ThreadLocal实例,Value是缓存变量的值)
如果获取到了本地变量ThreadLocalMap,就会将当前线程绑定的ThreadLocal实例和缓存变量的值存放到ThreadLocalMap的Entry<ThreadLocal,T>中
get() 实现原理:
- 获取当前线程的本地变量副本ThreadLocalMap
- 如果获取到了当前线程的ThreadLocalMap,则根据当前线程绑定的ThreadLocal实例作为Key获取Entry中的Value
- 如果没有获取到当前线程的ThreadLocalMap,那么就会调用setInitialize()创建一个空的ThreadLocalMap,并使用当前线程绑定的ThreadLocal实例作为Entry的Key,null作为Entry的Value存入当前线程的ThreadLocalMap
remove() 实现原理:
- 获取当前线程的本地变量副本ThreadLocalMap
- 将ThreadLocalMap中Key为当前ThreadLocal实例的数据直接移除
8 ThreadLocal产生内存泄漏的原因
上文提到,每个线程都会绑定一个ThreadLocalMap来作为本地变量副本,而ThreadLocalMap底层是通过Entry来存放数据的,Entry基于弱引用指向Key(ThreadLocal实例),通过Key来绑定Value缓存变量。
根据弱引用的原则,当且仅当JVM GC垃圾回收时才会回收对象。如果在此之前,部分线程的ThreadLocal被设置为空了(threadLocal=null),那么该ThreadLocal绑定的Entry(Key-Value键值对)将不会被回收,除非手动触发JVM GC回收才会回收该对象。但是实际上在开发过程中,手动去通过System.gc()触发GC回收的使用概率是非常少的,并且System.gc()调用不一定就会立刻触发JVM GC垃圾回收,System.gc()的作用只是告诉JVM你可以进行GC垃圾回收了,但是有可能最终没有垃圾回收,那么就会导致这些ThreadLocal被置为空的线程的本地变量副本中都会一直存在key为null的Entry键值对,且始终不会被回收,一直存放在内存中占用内存空间,数量多了之后就会导致内存泄漏(已分配到内存的部分一直不释放内存,导致内存爆满)。
总的而言,ThreadLocal产生内存泄漏的原因,主要是因为ThreadLocalMap与
我们当前线程的生命周期一样,如果没有手动的删除的情况下,就有可能会发生内存泄漏的问题。
9 如何避免ThreadLocal内存泄漏
- 每次set()前先执行一次remove()
- 调用remove() 将不要的数据移除避免内存泄漏的问题;
10 ThreadLocal应用案例:生成全局ID
目标:在AOP前置增强中生成全局ID,在程序中其他地方获取全局ID
|
|
|
更多推荐
所有评论(0)