Java高阶私房菜:深入解析多线程场景下ThreadLocal应用

        相信小伙伴们在工作中有听说过Threadlocal,或者在实际项目中有大量的使用Threadlocal,有些人可能没用过,不过没关系,通过本文你就能从小白到高手蜕变,如果使用过,同样也能收获不一样的知识点。

什么是ThreadLocal

        全称thread local variable(线程局部变量)功用非常简单,使用场合主要解决多线程中数据因并发产生不一致问题。ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享,这样的结果是耗费了内存
但大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。 其核心只做了一件事情:同个线程共享数据

使用时需注意的是ThreadLocal不能使用原子类型,只能使用Object类型

应用场景

        ThreadLocal 用作每个线程内需要独立保存信息,方便同个线程的其他方法获取该信息的场景。 每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念,比如用户登录令牌解密后的信息传递(用户权限信息、从用户系统获取到的用户名、用户ID)。

         假设利用Threadlocal实现赛跑Demo,其主要利用了线程外隔离,线程内变量共享的特性。

public class Runner {
    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()->0);

    public void run(){
        Integer distenc = threadLocal.get();
        threadLocal.set(++distenc);
    }

    public void sore(){
        String name = Thread.currentThread().getName();
        Integer distenc = threadLocal.get();
        System.out.println(name + " " + distenc);
    }

    public static void main(String[] args) {
        Runner runner = new Runner();
//        张三一小时跑10公里
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                runner.run();
            }
            runner.sore();
        }, "张三").start();
//        李四一小时跑5公里
        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                runner.run();
            }
            runner.sore();
        }, "李四").start();
//        王五一小时跑3公里
        new Thread(()->{
            for (int i = 0; i < 3; i++) {
                runner.run();
            }
            runner.sore();
        }, "王五").start();
    }
}

当使用threadlocal完成后需要remove,如runner.threadLocal.remove();其remove原因后面将详细讲解

ThreadLocal核心源码解读

查看源码




 三者之间关系(Thread 、ThreadLocal、ThreadLocalMap)

        Thread类中维护着一个ThreadLocalMap类型的变量,变量名为threadLocals,它在ThreadLocal类中声明,并作为静态内部类来存在。ThreadLocal提供了一系列方法用于操作ThreadLocalMap(且仅能使用ThreadLocal操作),如get/set/remove等操作。它的作用用于隔离Thread和ThreadLocalMap,防止直接创建ThreadLocalMap,并且自身的get/set内部会判断当前线程是否已经绑定一个ThreadLocalMap,有就继续使用,没有就为其自身绑定。
        ThreadLocalMap就是保存ThreadLocal的map结构,key就是ThreadLocal本身
所以一个线程只能存储一个值,可以理解为JVM内部维护的一个Map<Thread, Object>,当线程需要用到Object,就用【当前线程】去Map里面获取对应的Object。


        简而言之,ThreadLocal本身并不存储值 ( 是一个壳子 ), 它只是自己作为一个key来让线程从ThreadLocalMap获取value。因此,ThreadLocal能够实现 “每个线程之间的数据隔离”,获取当前线程的局部变量值,不受其他线程影响。

JVM的四大引用类型

        为了更加深入理解Theadlocal,我们需要先了解一下JVM的四大引用类型。

强引用

        强引用是使用最普遍的引用,当一个对象被强引用关联后,它就不会被垃圾回收器回收
比如String str = “abc”,变量str就是字符串“abc”的强引用,即使在【内存不足】的情况下,JVM宁愿抛出OutOfMemoryError,也不会回收这种对象。

具体编码

        1)创建User类,覆盖finalize()函数,在finalize()中输出打印信息,方便追踪,来完成“非内存资源”的清理工作。
        2)finalize()是Object基类的一个方法,在JVM回收内存时执行的,是GC前对待回收的对象进行标记,标记成功后会回调此函数;
        3)JVM并不保证在回收内存时一定会调用finalize(),可以用这个System.gc(),手动提醒GC,可以进行标记并回收垃圾。

public class User {
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("User对象被标记为垃圾了");
    }
}


public static void main(String[] args) throws InterruptedException {
        testReference();
    }
    /**
     * 强引用测试
     * @throws InterruptedException
     */
    public static void testReference() throws InterruptedException {
        User user  = new User();
        System.out.println("GC前 = "+user);
        user = null;
        System.gc();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("GC后 = "+user);
}

弱引用

        软引用是用来描述一些还有用但非必需的对象,当系统内存资源不足时,垃圾回收器会回收这些对象。只有当内存不足时,才会回收软引用关联的对象;当内存资源充足时,不会回收软引用关联的对象,直接调用GC也不回收。一般在高速缓存中会使用,内存不够时则回收相关对象释放内存。使用 SoftReference< > 包装对象就可以转换为软引用。

具体编码

    /**
     * 只有当内存不足时,才会回收 软引用 关联的对象。当内存资源充足时,不会回收软引用关联的对象, 不用手工调用GC
     * -Xms100m -Xmx100m
     */
    public static void testSoftReference() {
        SoftReference<User> softReference = new SoftReference<>(new User());
        System.out.println("内存够用,GC前 = " + softReference.get());
        try {
            TimeUnit.SECONDS.sleep(1);
            consumeMemory();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("内存不够用,GC后 = " + softReference.get());
        }

        try {
            TimeUnit.SECONDS.sleep(1);}catch (Exception e){e.printStackTrace();}
    }


    /**
     * 消耗大量内存, -Xms100m -Xmx100m
     */
    public static void consumeMemory() {
        List<Byte[]> list = new ArrayList<>();
        for (int i = 0; i < 99; i++) {
            //1MB
            Byte[] bytes = new Byte[1 * 1024 * 1024];
            list.add(bytes);
        }
    }


//输出下面
内存够用,GC前 = thread.User@9c71l1g1
User对象被标记为垃圾了
内存不够用,GC后 = null
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  at thread.ReferenceTest.consumeMemory(ReferenceTest.java:63)
  at thread.ReferenceTest.testSoftReference(ReferenceTest.java:47)
  at thread.ReferenceTest.main(ReferenceTest.java:19)

弱引用

        弱引用也是用来描述非必需对象,但是它的强度比软引用更弱一些,只能生存到下一次垃圾收集发生之前,只要垃圾回收器工作时,无论内存是否充足,都会回收被弱引用关联的对象。使用了WeakReference类来实现弱引用。

/**
     * 只要垃圾回收器工作时,无论内存是否充足,都会回收被弱引用关联的对象。
     */
    public static void testWeakReference() {
        WeakReference<User> weakReference = new WeakReference<>(new User());
        System.out.println("内存够用,GC前 = " + weakReference.get());
        try {
            TimeUnit.SECONDS.sleep(1);
            System.gc();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("无论内存是否充足,GC后 = " + weakReference.get());
        }
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

虚引用

         最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。一个对象设置虚引用关联,目的就是能在这个对象被收集器回收时收到一个系统通知。使用PhantomReference类来实现虚引用,必需要组合使用一个引用队列ReferenceQueue。当垃圾回收器要回收一个对象时,如果发现它还有虚引用,会在回收对象的内存之前,把这个虚引用加入到关联的引用队列中。在虚引用对象传到它的引用队列之前会调用对象的finalize方法。

 /**
     *  弱引用和虚引用指向的对象在发生GC时一定会被回收,虚引用是得不到引用的对象实例
     */
    public static void testPhantomReference() {
        ReferenceQueue queue = new ReferenceQueue();
        PhantomReference phantomReference = new PhantomReference(new User(), queue);
        System.out.println("内存够用,GC前 = " + phantomReference.get());
        System.gc();
        consumeMemory();

    }

整体分析

引用类型被垃圾回收时刻用途生存时间
强引用从来不会对象的一般状态JVM停止运行时终止
软引用在内存不足时对象简单,缓存,文件缓存,图片缓存内存不足时终止
弱引用在gc垃圾回收时对象简单,缓存,文件缓存,图片缓存gc运行后终止
虚引用任何时候都可能被垃圾回收器回收基本不写,虚拟机使用, 用来跟踪对象被垃圾回收器回收的活动未知

使用完threadLocal为什么要remove?

        前面已讲解到,ThreadLocal的ThreadLocalMap是一个静态内部类,每个线程都将持有一个ThreadLocalMap对象,即每一个新的线程Thread都会实例化一个ThreadLocalMap,并将其赋值给成员变量threadLocals,使用时若已经存在threadLocals,则直接使用已经存在的对象。

        但通过源码可以发现这个类没有实现map接口,即是一个普通的Java类,但是该类实现了类似于map的功能,里面每个数据都用Entry来保存。

Key问题回收

         其继承了WeakReference指向ThreadLocal(弱引用)键值对存储,键为ThreadLocal的自身引用。其设计为弱引用目的在于如果ThreadLocal的引用丢失,TheadLocalMap的key能够被GC回收,避免了内存泄露问题。

        当执行以下代码时将会产生以下引用关系:

public static void main(String[] args) {
        ThreadLocal threadLocal = new ThreadLocal();
        threadLocal.set(new User());
        threadLocal = null;
    }

 Value问题回收

        由于Key是弱引用被回收了,然后key是null,但是value是强引用对象没法被回收和访问,就导致内存泄露,所以使用完theadlocal就需要remove相关的value。

        常规使用的线程,如果线程对象结束被回收,则上面的key和value都可以被回收,但是业务里面多数是使用线程池,就导致线程不能被回收,从而如果没remove对应的值,则会导致OOM。常规set/get方法里面也会清除key为null的entry对象的方法,但实际开发还是需要直接调用remove方法删除。

Theadlocal使用案例传送门:分布式应用下登录检验解决方案-CSDN博客

  • 42
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值