JUC并发编程第十篇,谈谈ThreadLocal原理与内存泄露的那些事儿

一、ThreadLocal是什么?能干嘛?

  • ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问 ThreadLocal 实例的时候都有自己的、独立初始化的变量副本。
  • ThreadLocal 实例通常是类中的私有静态字段,使用它的目的是希望将状态与线程关联起来。
    在这里插入图片描述
    它实现了让每一个线程都有自己专属的本地变量副本,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

二、ThreadLocal 使用场景举例

  • 案例一:三个售票员卖完50张票,总量完成即可,售票员每个月固定月薪。
/**
 * 三个售票员卖完50张票,总量完成即可,售票员每个月固定月薪
 */
public class ThreadLocalDemo {
    public static void main(String[] args) {
        MovieTicket movieTicket = new MovieTicket();

        for (int i = 1; i <= 3; i++) {
            new Thread(() -> {
                for (int j = 0; j < 20; j++) {
                    movieTicket.saleTicket();
                    try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
                }
            },String.valueOf(i)).start();
        }
    }
}

class MovieTicket{
    int number = 50;

    public synchronized void saleTicket(){
        if (number > 0){
            System.out.println(Thread.currentThread().getName()+"\t"+"号售票员卖出第: "+(number--));
        }else{
            System.out.println("--------------卖完了");
        }
    }
}
  • 案例二:比如房屋销售,要求员工根据自己的能力卖房,各自统计各自的,根据卖出的数量工资提成。
public class ThreadLocalDemo2 {
    public static void main(String[] args) {
        House house = new House();

        new Thread(() ->{
            try {
                for (int i = 1; i <= 3; i++) {
                    house.saleHouse();
                }
                System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
            } finally {
                //记得remove,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题
                house.threadLocal.remove();
            }

        },"t1").start();

        new Thread(() ->{
            try {
                for (int i = 1; i <= 8; i++) {
                    house.saleHouse();
                }
                System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
            } finally {
                //记得remove,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题
                house.threadLocal.remove();
            }

        },"t2").start();

        new Thread(() ->{
            try {
                for (int i = 1; i <= 12; i++) {
                    house.saleHouse();
                }
                System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
            } finally {
                //记得remove,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题
                house.threadLocal.remove();
            }

        },"t3").start();

        System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
    }
}

class House{

    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public void saleHouse(){
        Integer value = threadLocal.get();
        value++;
        threadLocal.set(value);
    }
}

要记得remove(),如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题

案例总结:

  • 想要做到线程不争抢,有两种方式,第一:加 synchronized 或者 Lock 控制资源的访问顺序(效率低);第二:每个线程拥有自己独立的值(ThreadLocal)
  • 每个 Thread 内有自己的实例副本且该副本只由当前线程自己使用,ThreadLocal 统一设置初始值,但是每个线程对这个值的修改是各自线程互相独立的,既然其它 Thread 不可访问,那就不存在多线程间共享的问题。

三、阿里开发规范中 ThreadLocal 的使用(SimpleDateFormat)

在这里插入图片描述

  • SimpleDateFormat 是非线程安全的,官方建议为每个线程创建独立的格式实例,如果多个线程同时访问一个格式,则它必须保持外部同步。

并发环境下使用 SimpleDateFormat 的 parse 方法将字符串转换成 Date 对象,使用静态的成员变量是非常不安全的。

  • 代码演示:
public class DateUtils {
    public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static Date parseDate(String stringDate) throws Exception {
        return sdf.parse(stringDate);
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                try {
                    System.out.println(DateUtils.parseDate("2022-12-12 11:11:11"));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

在这里插入图片描述
在这里插入图片描述

  • 这是为什么呢?

SimpleDateFormat 类内部有一个Calendar对象引用,它用来储存和这个 SimpleDateFormat 相关的日期信息,

如果你的 SimpleDateFormat 是个 static 的,那么多个 thread 之间就会共享这个 SimpleDateFormat,同时也是共享这个Calendar引用。

  • 如何解决?

方法一:将SimpleDateFormat定义成局部变量。

public class DateUtils {
    public static void main(String[] args) {
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                try {
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    System.out.println(sdf.parse("2020-11-11 11:11:11"));
                    sdf = null;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

缺点:每调用一次方法就会创建一个SimpleDateFormat对象,方法结束又要作为垃圾回收。

方法二:ThreadLocal,线程本地变量或者线程本地存储

/**
 * ThreadLocal可以确保每个线程都可以得到各自单独的一个SimpleDateFormat的对象,那么自然也就不存在竞争问题了。
 */
public class DateUtils2 {

    private static final ThreadLocal<SimpleDateFormat> sdf_threadLocal =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static Date parseDateTL(String stringDate) throws ParseException {
        return sdf_threadLocal.get().parse(stringDate);
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                try {
                    System.out.println(DateUtils2.parseDateTL("2022-12-12 11:11:11"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

方法三:DateTimeFormatter 代替 SimpleDateFormat

public class DateUtils2 {

    public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static String format(LocalDateTime localDateTime){
        return DATE_TIME_FORMATTER.format(localDateTime);
    }

    public static LocalDateTime parse(String dateString){
        return LocalDateTime.parse(dateString,DATE_TIME_FORMATTER);
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                    System.out.println(DateUtils2.parse("2022-12-12 11:11:11"));
            },String.valueOf(i)).start();
        }
    }
}

四、ThreadLocal 底层源码架构分析

  • 从源码进行分析,Thread 类里边有一个 ThreadLocal
    在这里插入图片描述
  • ThreadLocal 里边有一个 ThreadLocalMap
  • ThreadLocalMap 里边实际干活的是一个 Entry
    在这里插入图片描述
  • 所以,Thread、ThreadLocal、ThreadLocalMap 组织关系总结如下:
    在这里插入图片描述

threadLocalMap 实际上就是一个以 threadLocal 实例为 key,任意对象为 value 的 Entry 对象。

ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map,不过它进行了两层包装,JVM内部维护了一个线程版的Map<Thread,T>,通过 ThreadLocal 对象的 set 方法,把 ThreadLocal 对象自己当做 key,放进了 ThreadLoalMap 中,每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,保证并发模式下的安全。

五、ThreadLocal中的内存泄露问题

内存泄露:指不再会被使用的对象或者变量占用的内存不能被回收。

内存泄露是如何造成的呢?这里我们先要了解什么是强、软、弱、虚四个引用?

  • 强引用:最常见的普通对象引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用,当一个对象被强引用变量引用时,它处于可达状态,是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,即使OOM,JVM也不会回收。
  • 软引用:通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收。
  • 弱引用:只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
  • 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列 (ReferenceQueue)联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态,在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。
假如有一个应用需要读取大量的本地图片:
 
	如果每次读取图片都从硬盘读取则会严重影响性能,
	如果一次性全部加载到内存中又可能造成内存溢出。
 
此时使用软引用可以解决这个问题:
 
  用一个HashMap来保存  图片的路径  和  相应图片对象关联的软引用  之间的映射关系,在内存不足时,
JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。

然后,回想刚才在上边看到的 ThreadLocalMap 源码,这个Map里边保存的是以 ThreadLocal 为 key 的对象,这个对象经过了两层包装:第一层使用 WeakReference 将 ThreadLocal 对象变成一个弱引用的对象,第二层是 定义了一个专门的类 Entry 来扩展 WeakReference。

这里为什么要使用弱引用呢?
在这里插入图片描述

  • 举个例子:
    • 当 func1 方法执行完后,栈帧销毁强引用 tl 也就没有了。但此时线程的 ThreadLocalMap 里某个 entry 的 key 引用还指向这个对象,如果这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
      如果使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。

使用弱引用就不会出问题了吗?这里还存在一个问题

  • Entry中的key是弱引用,当 threadLocal 外部强引用被置为null(tl=null)时,那么系统 GC 的时候,根据可达性分析,这个threadLocal 实例就没有任何一条链路能够引用到它,这个ThreadLocal 就会被回收,这样一来,ThreadLocalMap 中就会出现一个 key 为 null 的 Entry。
  • 这样一来,我们就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的Entry 的 value 就会迟迟无法回收,造成内存泄漏。
  • 只有当前 thread 运行结束,threadLocal,threadLocalMap,Entry没有引用链可达时,在垃圾回收的时候才都会被系统进行回收。
  • 但是,在使用线程池的情况下,为了复用,我们是不会结束线程的,就会造成内存泄漏。

总结:弱引用不能百分百保证内存不泄露,我们要在不使用某个ThreadLocal对象后,手动调用remove() 方法来删除它。

六、ThreadLocal总结

1、ThreadLocal 不是解决线程间共享数据问题的,而是用于 变量在线程间隔离且在方法间共享的场景。

2、它隐式的在不同线程内创建独立实例副本,避免了实例线程的安全问题。

3、每个线程持有一个只属于自己的Map,维护了ThreadLocal对象与具体实例的映射,该Map只能被持有它的线程访问,故不存在线程安全以及锁的问题。

4、ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题。

5、通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry 这三个方法回收键为 null 的 Entry 对象,从而防止内存泄漏。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Anton丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值