田超凡

原创博文,仿冒必究,部分素材转载自每特教育蚂蚁课堂

1 ThreadLocal基本概念

ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程每个线程都保存有一个变量副本,每个线程的变量都不同。

ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定,ThreadLocal适用于在多线程的情况下,实现线程之间的数据隔离,进而解决线程安全性问题

 

2 ThreadLocal常用API

  1. set(T value) 设置当前线程缓存变量的值
  2. get() 获取当前线程缓存变量的值
  3. 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() 实现原理:

  1. 获取当前线程的本地变量ThreadLocalMap

  1. 如果没有获取到本地变量ThreadLocalMap,则创建一个空的ThreadLocalMap并通过构造注入存入到对应的Entry中(Key是当前ThreadLocal实例,Value是缓存变量的值)

如果获取到了本地变量ThreadLocalMap,就会将当前线程绑定的ThreadLocal实例和缓存变量的值存放到ThreadLocalMap的Entry<ThreadLocal,T>中

get() 实现原理:

  1. 获取当前线程的本地变量副本ThreadLocalMap
  2. 如果获取到了当前线程的ThreadLocalMap,则根据当前线程绑定的ThreadLocal实例作为Key获取Entry中的Value
  3. 如果没有获取到当前线程的ThreadLocalMap,那么就会调用setInitialize()创建一个空的ThreadLocalMap,并使用当前线程绑定的ThreadLocal实例作为Entry的Key,null作为Entry的Value存入当前线程的ThreadLocalMap

 

remove() 实现原理:

  1. 获取当前线程的本地变量副本ThreadLocalMap
  2. 将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内存泄漏

  1. 每次set()前先执行一次remove()
  2. 调用remove() 将不要的数据移除避免内存泄漏的问题;

 

10 ThreadLocal应用案例:生成全局ID

目标:在AOP前置增强中生成全局ID,在程序中其他地方获取全局ID

 


  import com.alibaba.fastjson.JSON; 

  import lombok.extern.slf4j.Slf4j;

  import org.aspectj.lang.JoinPoint;

  import org.aspectj.lang.annotation.Aspect;

  import org.aspectj.lang.annotation.Before;

  import org.aspectj.lang.annotation.Pointcut;

  import org.springframework.beans.factory.annotation.Autowired;

  import org.springframework.stereotype.Component;

  import org.springframework.web.context.request.RequestContextHolder;

  import org.springframework.web.context.request.ServletRequestAttributes;

  

  import javax.servlet.http.HttpServletRequest;

  import java.util.Date;

  import java.util.Objects;

  import java.util.UUID;

  

  

  @Aspect

@Component

@Slf4j

  public class TestAop {

    @Autowired

    private GlobalIDContextholder globalIDContextholder;

  

    /**

     * 切入点

     */

    @Pointcut("execution(public * com.super.service.*Service.*(..))")

    public void log() {

    }

  

    /**

     * 前置操作

     *

     * @param point 切入点

     */

    @Before("log()")

    public void beforeLog(JoinPoint point) {

        UUID globalId = UUID.randomUUID();

        globalIDContextholder.set(globalId.toString());

    }

}

 

 

i

  @RestController

  public class TestController {

    @Autowired

    private GlobalIDContextholder globalIDContextholder;

  

    @GetMapping("/doTest")

    public String doTest () {

        return "从aop中获取的全局id:" + globalIDContextholder.getId();

    }

}

 

 

@Component

  public class GlobalIDContextholder {

  

    private ThreadLocal<String> globalIds = new ThreadLocal<String>();

  

    public String getId() {

        return globalIds.get();

    }

  

    public void set(String globalId) {

        globalIds.set(globalId);

    }

}

 

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐