JUC(10):ThreadLocal | 一文打通任督二脉

前言

开启 ThreadLocal 专场。

其实就是一张图的概念,带你打通 ThreadLocal 相关概念。

在 Java 的多线程领域中,如果没有 ThreadLocal 会是怎样的世界?

那会变得非常麻烦,每个线程都没有独属于自己的小秘密,在计算机的缜密计算下不断的去抢占 CPU 资源,成为一个个不会思考的干活机器,而有了 ThreadLocal 的存在,每个线程都有了一个独自使用的小房间,房间里面摆放不同的东西,主人要用的时候,直接存取就是了,这个每个线程的功能扩展性又变强了。

一、简介

1.1 概述

ThreadLocal 线程本地的概念,更应该称为 ThreadLcal Variable 线程本地变量副本更加合适,主要解决:多线程中数据因为并发产生的不一致问题

怎么解决的呢? threadLocal 它为每个线程都提供了变量的副本,使得每个线程在某时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享,缺点就是浪费了小部分内存,但一般也存不到多少数据,几乎没啥影响。

初步理解?ThreadLocal 是个对象,它给每个线程都提供一个变量副本,因此,这些变量就需要跟线程ID做一个绑定关联,那我们就可以简单理解为需要在每个线程对这个变量进行 set 和 get 方法操作,对值进行设置和获取,这个变量只跟当前线程有关噢,这样不就避免了多线程间数据共享的问题。

1.2 API

有了初步理解,我们再来总结下,ThreadLocal 为每个线程都设置一个变量副本,线程与变量做好绑定关系,通过线程可以对值进行设置与获取。

原理后面再来深入。

先看 API ,写个 Demo 跑起来。

重要的也就这几个,get 、set 我们已经接触过了,remove 是什么呢?删除变量,也能理解。

initiaValue 是对值做初始化,withInitial 也是初始化,使用方法如下,调用此初始化方法,设置初始值。通常使用匿名内部类完成。但推荐用 withInitial 来写,更加简洁。

ini 复制代码
    ThreadLocal<Integer> saleNum =new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
    ThreadLocal<Integer> valueNum =ThreadLocal.withInitial(()->0);

1.3 卷王案例

看着不难,来吧,做个案例。

假设工单上有 100 个bug,A、B、C 三名卷王,在某个夜深人静的夜晚,准备对这100个 Bug 下手,将它们全部消灭,谁解决的 bug 越多,就荣升卷王称号。

csharp 复制代码
package com.xiaolei.juc.lock.threadlocal;

import java.util.Random;

/**
* @author xiaolei
* @version 1.0
* @ date 2024-03-20 9:58
*/
public class BugTest {
    public static void main(String[] args) throws InterruptedException {
        Bug bug = new Bug();

        Thread threadA = new Thread(() -> {
            int mybug = new Random().nextInt(40);
            for (int i = 0; i < mybug; i++) {
                bug.myDealBug();
                bug.existBug();
            }

            System.out.println("程序员"+Thread.currentThread().getName() + ":【解决了:"+Bug.dealBug.get()+"个bug】");
        }, "老王");

        Thread threadB = new Thread(() -> {
            int mybug = new Random().nextInt(40);
            for (int i = 0; i < mybug; i++) {
                bug.myDealBug();
                bug.existBug();
            }

            System.out.println("程序员"+Thread.currentThread().getName() + ":【解决了:"+Bug.dealBug.get()+"个bug】");
        }, "老徐");

        Thread threadC = new Thread(() -> {
            int mybug = new Random().nextInt(40);
            for (int i = 0; i < mybug; i++) {
                bug.myDealBug();
                bug.existBug();
            }
            System.out.println("程序员"+Thread.currentThread().getName() + ":【解决了:"+Bug.dealBug.get()+"个bug】");
        }, "老邱");

        threadA.start();
        threadB.start();
        threadC.start();

        Thread.sleep(500);
        System.out.println("剩余bug数:"+Bug.bugAmout);
    }

    static class Bug {
        static int bugAmout = 100;

        // 解决的 bug 数目,可以这样初始化,为0
static ThreadLocal<Integer> dealBug = new ThreadLocal<Integer>(){
            @Override
            protected Integer initialValue() {
                return  0;
            }
        };

        // 剩余的 bug
public synchronized void existBug(){
            bugAmout--;
        }
        // 我处理的bug
public void myDealBug(){
            dealBug.set(dealBug.get() + 1);
        }
    }
}
makefile 复制代码
程序员老徐:【解决了:25个bug】
程序员老王:【解决了:35个bug】
程序员老邱:【解决了:23个bug】
剩余bug数:17

1.4 阿里规范

Remove 方法的使用出现了,必须回收呀,尤其是线程池中,其实也很好理解呀,我们每个线程都绑定一个变量副本,那在线程池中,A线程绑定的变量副本如果不回收,当 A 线程复用的时候,获取变量副本就还是原来的变量,这就造成我们业务代码的逻辑错误了。内存泄漏的话,后面在讨论下。

ThreadLocal 因为要对所有线程操作都是共享的,所以建议设置 static 修饰符,这样只需要分配一次空间,所有线程都指向这个对象实例,这个对象实例实际上是存在 ThreadLocalMap 中的 key,所以key 对于所有对象来说都是一样的,不同的是 map 中的 value,因此,为了确保 ThreadLocal 实例的唯一性,使用 static 修饰,防止使用过程中的动态修改,使用 final 进行加强修饰。

1.5 使用场景

ThreadLocal 是解决线程安全问题的一个不错方案,为每个线程提供一个独立的本地值去解决并发访问的冲突问题。

使用场景大致可以分为以下两类:

  • 1、线程隔离:可以为每个线程绑定一个用户会话信息,数据库连接,http请求等,这样一个线程所有调用到的处理函数都可以非常方便的访问这些资源。常见的数据库连接独享,Session 数据管理等。

  • 2、跨函数传递数据:通常在同一个线程内,跨类、跨方法传递数据时,如果不同 ThreadLocal ,那么相互之间数据传递势必要靠返回值和参数,这样无形中增加了这些类或者方法之间的耦合度。常见的是为每个线程绑定一个session信息,这样就不用参数传递了。

二、底层原理分析

底层,你与别人拉开差距的地方。

2.1 内部演进

早期版本, 一个 ThreadLocal 实例可以形象的理解为一个 MAP(早期版本是这样设计的)

当工作线程 Thread 实例向本地变量保持某个值时,会以 "key-value" 的形式保存到 ThreadLocal 内部的 map 中,其中 key 为线程 Thread 实例,value 为值。

那新版本 JDK8 做了什么改动呢?

来解释一下,JDK8 之前,是把 ThreadLocal 对象作为一个 map,key 是线程ID,value 是值,这样确实很完美,也很好理解,但在大部分应用中,局部变量其实很少,而线程是非常多的,配置数百个线程都是正常的,而这个 map 是 ThreadLocalMap,它与 HashMap 一样,存在高成本,低扩容的问题,引申下,threadLocalMap 扩容时采用开放地址法,不是hashmap的链地址法,开放地址放就是寻找其他数组空位存进去,所以扩容更频繁。

为了解决扩容的性能问题,在 JDK8 版本中,还是用 map,每个线程内部都有一个 ThreadLocalMap 成员变量。

java 复制代码
 /* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

这个 ThreadLocalMap 存储的是一个叫 Entry 的对象,Entry 内部中 key 是 threadLocal,value 是设置的值。真正干活的是这个 threadLocalMap,这样就解决了扩容成本高的问题。

简单的关系就是上图,

2.2 存储分析

这张图非常重要,一图观全局,建议默写。

Thread 内部有个 threadLocalMap 对象,threadLocalmap 内部存储的是 entry 对象

这个 entry 对象是一个key-value 结构的,其中 key 是将传入对象封装成一个弱引用,value 是object 对象,再看泛型,key 是 threadlocal 对象。

问题:

1、threadlocalmap 为什么要继承弱引用?

答:设置为弱引用,gc 扫描到时,发现 threadlocal 没有强引用,会回收该 threadlocal 对象;

如果 threadlocalmap 的 key 是强引用,那么只要线程存在, threadlocalmap 就存在,而 threadlocalmap 结构就是 entry 数组,即对应的 entry 数组就存在(entry数组如果key不为null,就无法回收),而 entry 数组元素的 key 是 threadlocal。即便我们在代码中显示赋值 threadlocal 为 null,但是上面强引用存在, threadlocal 并不会被回收。

若这个 key 引用是强引用,就会导致 key 指向的 ThreadLocal 对象及 value 指向的对象不能被 gc 回收,造成内存泄漏;

若这个 key 引用是弱引用就大概率减少内存泄漏的问题。

什么叫作内存泄漏?不再用到的内存没有及时释放(归还给系统),就叫作内

存泄漏。对于持续运行的服务进程必须及时释放内存,否则内存占用量越来越高,轻则影响

系统性能,重则导致进程崩溃。

2、弱引用就万无一失?

当我们为 threadLocal 变量赋值,实际上就是当前的 Entry(threadLocal 实例为 key,值为 value)往这个 threadLocalMap 中存放。Entry 中的 key 是弱引用,当 threadLocal 外部强引用被置为 null(tl=null),那么系统 GC 的时候,根据可达性分析,这个 threadLocal 实例就没有任何一条链路能够引用到它,这个 threadLocal 势必会被回收。这样一来,ThreadLocalMap 就会出现key 为 null 的Entry,就没有办法访问这些 key 为null 的Entry 的value。

因为,弱引用不能 100% 保证内存不泄漏。我们要在不使用某个 ThreadLocal 对象后,手动调用 remove 方法来删除它。尤其是在线程池中,不仅仅是内存泄漏的问题,因为线程池中的线程是重复使用的,如果我们不手动调用 remove 方法,那么后面的线程就有可能获取到上个线程遗留下来的 value 值,造成bug。

3、清除脏 entry

不管是在 执行 get方法、set方法、remove 方法都会调用 expungeStaleEntry 方法,寻找 key 为 null 的entry,清除脏 entry。

如果,当前线程再不迟迟结束的话,这些 key 为 null 的 Entry 的value 就会一直存在一条强引用链。

get 源码分析:

  • 1、首先获取一个 threadlocalmap ,然后这个map 的key 为线程,值为存入的值;
  • 2、编代码的时候尽量要设置初始值,不然如果map 为null,会返回初始值为null ;

源码理解:

  • ThreadLocalMap 从字面上来看,这是一个保存 ThreadLocal 对象的map(其实是以 ThreadLocal 为 key),不过是经过了两层包装的 ThreadLocal 对象。

  • JVM 内部维护了个线程版的 Map<ThreadLocal,value>,每个线程要用到这个 T 的时候,用当前线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。

2.2 拓展--强引用(默认)

当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。

强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还"活着",垃圾收集器不会碰这种对象。在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。

对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。

csharp 复制代码
public static void strongReference()
{
    MyObject myObject = new MyObject();
    System.out.println("-----gc before: "+myObject);
    myObject = null;
    System.gc();
    try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
    System.out.println("-----gc after: "+myObject);
}

2.3 拓展--软 引用

软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。

对于只有软引用的对象来说,

当系统内存充足时它 不会 被回收,

当系统内存不足时它 会 被回收。

软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!

csharp 复制代码
class MyObject
{
    //一般开发中不用调用这个方法,本次只是为了演示
    @Override
    protected void finalize() throws Throwable
    {
        System.out.println(Thread.currentThread().getName()+"\t"+"---finalize method invoked....");
    }
}
public class ReferenceDemo
{
    public static void main(String[] args)
    {
        //当我们内存不够用的时候,soft会被回收的情况,设置我们的内存大小:-Xms10m -Xmx10m
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
        System.gc();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-----gc after内存够用: "+softReference.get());
        try
        {
            byte[] bytes = new byte[9 * 1024 * 1024];
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println("-----gc after内存不够: "+softReference.get());
        }
    }
    public static void strongReference()
    {
        MyObject myObject = new MyObject();
        System.out.println("-----gc before: "+myObject);
        myObject = null;
        System.gc();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-----gc after: "+myObject);
    }
}

2.4 拓展--弱引用

弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。

csharp 复制代码
class MyObject
{
    //一般开发中不用调用这个方法,本次只是为了演示
    @Override
    protected void finalize() throws Throwable
    {
        System.out.println(Thread.currentThread().getName()+"\t"+"---finalize method invoked....");
    }
}
public class ReferenceDemo
{
    public static void main(String[] args)
    {
        WeakReference<MyObject> weakReference = new WeakReference<>(new MyObject());
        System.out.println("-----gc before内存够用: "+weakReference.get());
        System.gc();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-----gc after内存够用: "+weakReference.get());
    }
    public static void softReference()
    {
        //当我们内存不够用的时候,soft会被回收的情况,设置我们的内存大小:-Xms10m -Xmx10m
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
        System.gc();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-----gc after内存够用: "+softReference.get());
        try
        {
            byte[] bytes = new byte[9 * 1024 * 1024];
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println("-----gc after内存不够: "+softReference.get());
        }
    }
    public static void strongReference()
    {
        MyObject myObject = new MyObject();
        System.out.println("-----gc before: "+myObject);
        myObject = null;
        System.gc();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-----gc after: "+myObject);
    }
}

软引用和弱引用的适用场景:

假如有一个应用需要读取大量的本地图片:

如果每次读取图片都从硬盘读取则会严重影响性能,

如果一次性全部加载到内存中又可能造成内存溢出。

此时使用软引用可以解决这个问题。

设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。

Map<String, SoftReference> imageCache = new HashMap<String, SoftReference>();

2.5 拓展--虚引用

虚引用需要java.lang.ref.PhantomReference类来实现。

顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列 (ReferenceQueue)联合使用。

虚引用的主要作用是跟踪对象被垃圾回收的状态。 仅仅是提供了一种确保对象被 finalize以后,做某些事情的机制。 PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。

其意义在于:说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作。

换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。

csharp 复制代码
class MyObject
{
    //一般开发中不用调用这个方法,本次只是为了演示
    @Override
    protected void finalize() throws Throwable
    {
        System.out.println(Thread.currentThread().getName()+"\t"+"---finalize method invoked....");
    }
}
public class ReferenceDemo
{
    public static void main(String[] args)
    {
        ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue();
        PhantomReference<MyObject> phantomReference = new PhantomReference<>(new MyObject(),referenceQueue);
        //System.out.println(phantomReference.get());
        List<byte[]> list = new ArrayList<>();
        new Thread(() -> {
            while (true)
            {
                list.add(new byte[1 * 1024 * 1024]);
                try { TimeUnit.MILLISECONDS.sleep(600); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println(phantomReference.get());
            }
        },"t1").start();
        new Thread(() -> {
            while (true)
            {
                Reference<? extends MyObject> reference = referenceQueue.poll();
                if (reference != null) {
                    System.out.println("***********有虚对象加入队列了");
                }
            }
        },"t2").start();
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
    }
    public static void weakReference()
    {
        WeakReference<MyObject> weakReference = new WeakReference<>(new MyObject());
        System.out.println("-----gc before内存够用: "+weakReference.get());
        System.gc();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-----gc after内存够用: "+weakReference.get());
    }
    public static void softReference()
    {
        //当我们内存不够用的时候,soft会被回收的情况,设置我们的内存大小:-Xms10m -Xmx10m
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
        System.gc();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-----gc after内存够用: "+softReference.get());
        try
        {
            byte[] bytes = new byte[9 * 1024 * 1024];
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println("-----gc after内存不够: "+softReference.get());
        }
    }
    public static void strongReference()
    {
        MyObject myObject = new MyObject();
        System.out.println("-----gc before: "+myObject);
        myObject = null;
        System.gc();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-----gc after: "+myObject);
    }
}

三、总结

1、建议初始化的时候,赋值,例如 0,避免空指针异常;

2、建议把 ThreadLocal 修饰为 static。 加强 final 修饰,初始化一次,真正执行的是 threadLocalmap

3、用完记得 手动remove。

4、上图关系默写。

相关推荐
seasugar3 分钟前
记一次Maven拉不了包的问题
java·maven
Allen Bright12 分钟前
【Java基础-26.1】Java中的方法重载与方法重写:区别与使用场景
java·开发语言
苹果酱056713 分钟前
Golang的文件解压技术研究与应用案例
java·vue.js·spring boot·mysql·课程设计
秀儿y16 分钟前
单机服务和微服务
java·开发语言·微服务·云原生·架构
ybq1951334543118 分钟前
javaEE-多线程案例-单例模式
java·开发语言
seasugar34 分钟前
Maven怎么会出现一个dependency-reduced-pom.xml的文件
xml·java·maven
三天不学习37 分钟前
C# 中的记录类型简介 【代码之美系列】
后端·c#·微软技术·record·记录类型
一只淡水鱼6637 分钟前
【mybatis】基本操作:详解Spring通过注解和XML的方式来操作mybatis
java·数据库·spring·mybatis
唐叔在学习1 小时前
【唐叔学算法】第19天:交换排序-冒泡排序与快速排序的深度解析及Java实现
java·算法·排序算法
music0ant1 小时前
Idea 配置环境 更改Maven设置
java·maven·intellij-idea