首页 > 编程语言 >Java中ThreadLocal说明 使用线程内变量,完成后需调用remove()方法将其移除,即使异常也记得remove()回收,创建ThreadLocal线程变量 public static T

Java中ThreadLocal说明 使用线程内变量,完成后需调用remove()方法将其移除,即使异常也记得remove()回收,创建ThreadLocal线程变量 public static T

时间:2023-11-14 09:01:27浏览次数:41  
标签:value 变量 ThreadLocalMap remove ThreadLocal 线程 引用

Java中ThreadLocal说明,完成后需调用remove()方法将其移除,即使异常也记得remove()回收,创建ThreadLocal线程变量 public static ThreadLocal threadLocal = new ThreadLocal<>();

1、ThreadLocal是什么

  • ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。
    这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
    ——《Java并发编程艺术》
  • 如图:
    threadlocal.png
  • ThreadLocal,可以拆成Thread+Local•Thread—线程;local—本地的,局域的。•拼在一起就是线程局域的。线程私有的,ThreadLocal类顾名思义可以理解为线程本地变量。
    定义了一个ThreadLocal,每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的

2、ThreadLocal怎么用

package test;
public class ThreadLocalTest {
    static ThreadLocal<String> localVar = new ThreadLocal<>();
    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar.get());
        //清除本地内存中的本地变量
        localVar.remove();
    }
    public static void main(String[] args) {
        Thread t1  = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                localVar.set("localVar1");
                //调用打印方法
                print("thread1");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
            }
        });
        Thread t2  = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                localVar.set("localVar2");
                //调用打印方法
                print("thread2");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
            }
        });
        t1.start();
        t2.start();
    }
}
  • 输出
    1368768201906132249453851072836449.png

3、ThreadLocal的实现原理

  • Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量,我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为null,只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们(后面我们会查看这两个方法的源码)。
  • 每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量
  • 流程图
    136876820190614000329689872917045.png
  • set 方法
public void set(T value) {
    //(1)获取当前线程(调用者线程)
    Thread t = Thread.currentThread();
    //(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //(3)如果map不为null,就直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //(4)如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
}
  • get 方法
public T get() {
    //(1)获取当前线程
    Thread t = Thread.currentThread();
    //(2)获取当前线程的threadLocals变量
    ThreadLocalMap map = getMap(t);
    //(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
    return setInitialValue();
}
private T setInitialValue() {
    //protected T initialValue() {return null;}
    T value = initialValue();
    //获取当前线程
    Thread t = Thread.currentThread();
    //以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
    return value;
}
  • remove方法的实现
public void remove() {
    //获取当前线程绑定的threadLocals
     ThreadLocalMap m = getMap(Thread.currentThread());
     //如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
     if (m != null)
         m.remove(this);
 }
  • ThreadLocal不支持继承性

1、同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的)

package test;
public class ThreadLocalTest2 {
    //(1)创建ThreadLocal变量
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static void main(String[] args) {
        //在main线程中添加main线程的本地变量
        threadLocal.set("mainVal");
        //新创建一个子线程
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程中的本地变量值:"+threadLocal.get());
            }
        });
        thread.start();
        //输出main线程中的本地变量值
        System.out.println("mainx线程中的本地变量值:"+threadLocal.get());
    }
}

4、从ThreadLocalMap看ThreadLocal使用不当的内存泄漏问题

  • 首先我们先看看ThreadLocalMap的类图,在前面的介绍中,我们知道ThreadLocal只是一个工具类,他为用户提供get、set、remove接口操作实际存放本地变量的threadLocals(调用线程的成员变量),也知道threadLocals是一个ThreadLocalMap类型的变量,下面我们来看看ThreadLocalMap这个类。在此之前,我们回忆一下Java中的四种引用类型,相关GC只是参考前面系列的文章(JVM相关)

  • ①强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。

  • ②软引用:简言之,如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中

  • ③弱引用(这里讨论ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null

  • ④虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象)

20220823.png

  • 分析ThreadLocalMap内部实现

1、上面我们知道ThreadLocalMap内部实际上是一个Entry数组,我们先看看Entry的这个内部类

/**
 * 是继承自WeakReference的一个类,该类中实际存放的key是
 * 指向ThreadLocal的弱引用和与之对应的value值(该value值
 * 就是通过ThreadLocal的set方法传递过来的值)
 * 由于是弱引用,当get方法返回null的时候意味着坑能引用
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** value就是和ThreadLocal绑定的 */
    Object value;
    //k:ThreadLocal的引用,被传递给WeakReference的构造方法
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
//WeakReference构造方法(public class WeakReference<T> extends Reference<T> )
public WeakReference(T referent) {
    super(referent); //referent:ThreadLocal的引用
}
//Reference构造方法
Reference(T referent) {
    this(referent, null);//referent:ThreadLocal的引用
}
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
  • 在上面的代码中,我们可以看出,当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中的key为ThreadLocal的弱引用。当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。

  • 考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。

  • 总结:THreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。

5、ThreadLocalMap结构

  • ThreadLocalMap是ThreadLocal的一个静态内部类。里面的核心是一个Entry数组,Entry继承了WeakReference,在创建Entry的时候,将ThreadLocal对象设置成了弱引用。

注意:ThreadLocalMap虽然是ThreadLocal里面的一个静态内部类,但是它的实例是放在Thread里面的


 static class ThreadLocalMap {
     /**
     * 初始容量,默认为16,必须为2的幂
     */
    private static final int INITIAL_CAPACITY = 16;
    /**
     * 表里entry的个数
     */
    private int size = 0;
    /**
     * Entry表,大小必须为2的幂
     */
    private Entry[] table;
    /**
     * Entry继承了WeakReference
     * Entry的构造方法中调用了super(k),将ThreadLocal对象设置成了弱引用
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
            /** value就是和ThreadLocal绑定的,为实际放入的值 */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
 }
  • 为什么要用弱引用?

而弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。

当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。

  • 什么情况下会内存泄漏?

我们把JVM的最大堆设置成100MB,运行下面的代码。
用JProfiler查看内存使用情况,发现内存使用不断增大,直到抛出java.lang.OutOfMemoryError: Java heap space也就是OOM异常。

202208230101.png

  • 分析如下:

创建了一个核心线程数和最大线程数为5的线程池,这个保证了线程池里面随时都有5个线程在运行•模拟50个任务,每隔2秒往线程池里面加一个任务•任务:创建一个User对象user,给user的threadLocal赋值一个新创建的ThreadLocal对象,往这个ThreadLocal对象里面加一个5MB的Memory对象。

2022082302.gif

  • ThreadLocal对象存在两个引用,实现代表强引用,虚线代表弱引用。•强引用因为引用对象被回收了引用不存在了•虚引用是不能阻止GC的回收的•最终Entry的key最终指向的是null,而value指向的还是占用5MB内存空间的Memory对象。•这个时候,存在一条ThreadRef->Thread->ThreadLocalMap->Entry->Memory的强引用链,导致Memory无法被回收,造成内存泄漏,最终导致OOM。

  • 通过上面的内存分配图,我们不能得出:

  • 如果线程运行完任务就结束了,ThreadRef->Thread->ThreadLocalMap->Entry->Memory这条引用链就不存在了,就不存在内存泄漏的问题了。

  • 但是现在的Java应用,为了节省开销,大部分都会采用线程池的模式。为了不造成内存泄漏,最简单有效的方法是使用后调用remove()方法将其移除。

原文链接:https://blog.csdn.net/qq_25475445/article/details/128604233

标签:value,变量,ThreadLocalMap,remove,ThreadLocal,线程,引用
From: https://www.cnblogs.com/sunny3158/p/17830805.html

相关文章

  • 从理解和实战安排多线程学习-知识点整理
    确认目标一个是对知识点的理解,另外一个是对知识点的运用.相辅相成.同时带着Arthus去观察代码的情况.压测出代码的性能.先阅读书籍,理解知识点,这部分速度要快.然后针对知识点做一些练习,这部分速度略慢,不懂的需要查书.提高难度,挑战一些有创意的编程,去综合实现和......
  • 多线程案例
    111200  #ifndefMAINWINDOW_H#defineMAINWINDOW_H#include<QMainWindow>#include"subthread.h"#include<QThread>namespaceUi{classMainWindow;}classMainWindow:publicQMainWindow{Q_OBJECTpublic:explicitM......
  • 2023蚂蚁金服/理想/字节/快手面试笔试题——5个线程交叉打印1~100
    原题来自牛客网面经。类似这种多线程轮流打印的手撕题会出现很多次,比如以前就看过类似的3个线程轮流打印ABC。 关键点在于:怎么设计机制保证这个顺序,至于要打印的数字,肯定是要用互斥量保护起来。C++代码如下:#include<iostream>#include<mutex>#include<thread>#include......
  • JVM 里 new 对象时,堆会发生抢占吗?JVM是怎么设计来保证 线程安全的?
    会。假设JVM虚拟机上,每一次new对象时,指针就会向右移动一个对象size的距离,一个线程正在给A对象分配内存,指针还没来得及修改,另一个为对象B分配内存的线程又引用了这个指针来分配内存,这就发生了抢占。有两种方案来解决这个问题:1、CAS采用CAS分配重试的方式来保证更新操作的原子性2、TL......
  • 线程执行
    importthreading#新线程执行的代码:defloop():print('thread%sisrunning...'%threading.current_thread().name)n=0whilen<5:n=n+1print('thread%s>>>%s'%(threading.current_thread().na......
  • 随笔 复习 连接池 线程池
    连接池实现思路classConnectPool{public:ConnectPool(intnumber){for(inti=0;i<=number;i++){intfd=socket(); //创建通信的fdconect(); //连接服务器m_list.push(fd);//往容器中存储链接......
  • Netty(四)NIO多线程优化
    Netty(四)NIO多线程优化​ 前面的代码都只有一个选择器,没有充分利用多核CPU,因此可以分两组选择器boss:单线程配一个选择器,专门处理accept事件,不负责数据的读写worker:创建CPU核心数的线程,每个线程配一个选择器,轮流处理read事件1多线程问题分析关键是这一部分的代码,需要保......
  • 每个.NET开发都应掌握的C#多线程知识点
    上篇文章讲述了C#特性(Attribute)知识点,本文将介绍多线程的知识点。多线程编程是现代软件开发中的重要组成部分,它能够充分利用多核处理器,提高应用程序的性能和响应性。C#作为.NET开发的主要语言,提供了强大的多线程支持。本文将介绍C#多线程知识点,帮助.NET开发者更好地应对多线程编程......
  • 多线程锁
    常见锁介绍synchronized锁的八中情况packagecom.shaonian.juc.more_thread_lock;importjava.util.concurrent.TimeUnit;classPhone{publicstaticsynchronizedvoidsendSMS()throwsException{//停留4秒TimeUnit.SECONDS.sleep(4);......
  • 进程和线程的区别
    1.进程简单来说就是一个正在运行的程序,QQ就是个进程,微信也是个进程。线程是系统分配处理器时间的基本单元。2.进程有自己的堆栈空间和数据段,开销是比较大的。线程有独立的堆栈空间,但是数据段是共享的,开销会更小,切换速度更快。但是安全性比进程要差。在保护模式下,进程崩溃不会对其......