ThreadLocal :在 Java中隱匿的魔法之力

这篇具有很好参考价值的文章主要介绍了ThreadLocal :在 Java中隱匿的魔法之力。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

优质博文:IT-BLOG-CN

ThreadLocal 并不是一个Thread,而是 ThreadLocalVariable(线程局部变量)。也许把它命名为 ThreadLocalVar更加合适。线程局部变量就是为每一个使用该变量的线程都提供一个变量值的副本,是 Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。ThreadLocal是除了加锁这种同步方式之外的另一种保证多线程访问出现线程不安全的方式。

从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的,ThreadLocal实例就是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。

通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。在 ThreadLocal类中有一个 Map,用于存储每一个线程变量的副本,Map中元素的键为线程对象,而值为对应线程的变量副本。ThreadLocal采用了 “以空间换时间” 的方式。为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

一、API 说明

【1】new ThreadLocal(): 创建一个线程本地变量。
【2】T get(): 返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本。
【3】void set(T value): 将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 initialValue() 方法来设置线程局部变量的值。
【4】void remove(): 移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其 initialValue。
【5】protected T initialValue(): 返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用 get() 方法访问变量的时候。如果线程先调用 set(T) 方法再调用 get() 方法,则不会在线程中再调用 initialValue() 方法。若该实现只返回 null;如果程序员希望将线程局部变量初始化为 null 以外的某个值,则必须为 ThreadLocal 创建子类,并重写此方法。通常,将使用匿名内部类。initialValue 的典型实现将调用一个适当的构造方法,并返回新构造的对象。

在程序中一般都重写 initialValue方法,以给定一个特定的初始值。

二、ThreadLocal 简单使用

下面的例子中,开启两个线程,在每个线程内部设置了本地变量的值,然后调用 print方法打印当前本地变量的值。如果在打印之后调用本地变量的 remove方法会删除本地内存中的变量,代码如下所示:

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();
    }
}

下面是运行后的结果:

thread1 :localVar1
after remove : null
thread2 :localVar2
after remove : null

三、ThreadLocal 的实现原理

下面是 ThreadLocal的类图结构,从图中可知:Thread类中有两个变量 threadLocals和 inheritableThreadLocals,二者都是 ThreadLocal内部类 ThreadLocalMap类型的变量,我们通过查看内部内 ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为null。

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

只有当线程第一次调用 ThreadLocal的 set或者 get方法的时候才会创建他们(后面我们会查看这两个方法的源码)。除此之外,每个线程的本地变量不是存放在 ThreadLocal实例中,而是放在调用线程的 ThreadLocals变量里面(前面也说过,该变量是Thread类的变量)。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本相当于一个装载本地变量的工具壳,通过 set方法将 value添加到调用线程的 threadLocals中,当调用线程调用 get方法时候能够从它的 threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的 threadLocals中,所以不使用本地变量的时候需要调用 remove方法将 threadLocals中删除不用的本地变量。下面我们通过查看 ThreadLocal的set、get以及remove方法来查看 ThreadLocal具体实怎样工作的。

ThreadLocal :在 Java中隱匿的魔法之力,Java并发编程(多线程),Java基础,java,开发语言,后端,面试,架构,性能优化,职场和发展

【1】set方法源码

public void set(T value) {
    //(1)获取当前线程(调用者线程)
    Thread t = Thread.currentThread();
    //(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //(3)如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //(4)如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
}

在上面的代码中,(2)处调用 getMap方法获得当前线程对应的 threadLocals(参照上面的图示和文字说明),该方法代码如下:

ThreadLocalMap getMap(Thread t) {
   return t.threadLocals; //获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上
}

如果调用 getMap方法返回值不为 null,就直接将 value值设置到 threadLocals中(key为当前线程引用,值为本地变量);如果getMap方法返回 null说明是第一次调用 set方法(前面说到过,threadLocals默认值为null,只有调用 set方法的时候才会创建map),这个时候就需要调用 createMap方法创建 threadLocals,该方法如下所示:createMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了 threadLocals中。

void createMap(Thread t, T firstValue) {
   t.threadLocals = new ThreadLocalMap(this, firstValue);
}

【2】get方法源码: 在 get方法的实现中,首先获取当前调用者线程,如果当前线程的 threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行 setInitialValue方法初始化 threadLocals变量。在 setInitialValue方法中,类似于 set方法的实现,都是判断当前线程的 threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建 threadLocals变量,同样添加的值为null。

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;
}

【3】remove方法的实现: remove方法判断当前线程对应的 threadLocals变量是否为null,不为 null就直接删除当前线程中指定的 threadLocals变量。

public void remove() {
    //获取当前线程绑定的threadLocals
     ThreadLocalMap m = getMap(Thread.currentThread());
     //如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
     if (m != null)
         m.remove(this);
}

【4】如下图所示: 每个线程内部有一个名为 threadLocals的成员变量,该变量的类型为 ThreadLocal.ThreadLocalMap类型(类似于一个HashMap),其中的 key为当前定义的 ThreadLocal变量的 this引用,value为我们使用 set方法设置的值。每个线程的本地变量存放在自己的本地内存变量 threadLocals中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(可能会导致内存溢出),因此使用完毕需要将其 remove掉。

ThreadLocal :在 Java中隱匿的魔法之力,Java并发编程(多线程),Java基础,java,开发语言,后端,面试,架构,性能优化,职场和发展

四、ThreadLocal 不支持继承性

同一个 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());
    }
}

五、InheritableThreadLocal 类

在上面说到的 ThreadLocal类是不能提供子线程访问父线程的本地变量的,而 InheritableThreadLocal类则可以做到这个功能,下面是该类的源码

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

从上面代码可以看出,InheritableThreadLocal类继承了 ThreadLocal类,并重写了childValue、getMap、createMap三个方法。其中 createMap方法在被调用(当前线程调用 set方法时得到的 map为 null的时候需要调用该方法)的时候,创建的是inheritableThreadLocal而不是 threadLocals。同理,getMap方法在当前调用者线程调用 get方法的时候返回的也不是threadLocals 而是 inheritableThreadLocal。下面我们看看重写的 childValue方法在什么时候执行,怎样让子线程访问父线程的本地变量值。我们首先从 Thread类开始说起

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize) {
    init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    //判断名字的合法性
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }

    this.name = name;
    //(1)获取当前线程(父线程)
    Thread parent = currentThread();
    //安全校验
    SecurityManager security = System.getSecurityManager();
    if (g == null) { //g:当前线程组
        if (security != null) {
            g = security.getThreadGroup();
        }
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }
    g.checkAccess();
    if (security != null) {
        if (isCCLOverridden(getClass())) {
            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }

    g.addUnstarted();

    this.group = g; //设置为当前线程组
    this.daemon = parent.isDaemon();//守护线程与否(同父线程)
    this.priority = parent.getPriority();//优先级同父线程
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
    this.inheritedAccessControlContext =
            acc != null ? acc : AccessController.getContext();
    this.target = target;
    setPriority(priority);
    //(2)如果父线程的inheritableThreadLocal不为null
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        //(3)设置子线程中的inheritableThreadLocals为父线程的inheritableThreadLocals
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    this.stackSize = stackSize;

    tid = nextThreadID();
}

在 init方法中,首先(1)处获取了当前线程(父线程),然后(2)处判断当前父线程的 inheritableThreadLocals是否为null,然后调用createInheritedMap将父线程的 inheritableThreadLocals作为构造函数参数创建了一个新的 ThreadLocalMap变量,然后赋值给子线程。下面是 createInheritedMap方法和 ThreadLocalMap的构造方法

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                //调用重写的方法
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

在构造函数中将父线程的 inheritableThreadLocals成员变量的值赋值到新的 ThreadLocalMap对象中。返回之后赋值给子线程的inheritableThreadLocals。总之,InheritableThreadLocals类通过重写getMap和createMap两个方法将本地变量保存到了具体线程的inheritableThreadLocals变量中,当线程通过InheritableThreadLocals实例的set或者get方法设置变量的时候,就会创建当前线程的inheritableThreadLocals变量。而父线程创建子线程的时候,ThreadLocalMap中的构造函数会将父线程的inheritableThreadLocals中的变量复制一份到子线程的inheritableThreadLocals变量中。

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

【1】基础概念 : 首先我们先看看 ThreadLocalMap的类图,在前面的介绍中,我们知道 ThreadLocal只是一个工具类,他为用户提供get、set、remove接口操作实际存放本地变量的 threadLocals(调用线程的成员变量),也知道 threadLocals是一个ThreadLocalMap类型的变量,下面我们来看看 ThreadLocalMap这个类。在此之前,我们回忆一下 Java中的四种引用类型链接

ThreadLocal :在 Java中隱匿的魔法之力,Java并发编程(多线程),Java基础,java,开发语言,后端,面试,架构,性能优化,职场和发展

【2】分析 ThreadLocalMap内部实现: 上面我们知道 ThreadLocalMap内部实际上是一个 Entry数组 private Entry[] table ​,我们先看看 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对象就会被回收掉,但是对应的 value不会被回收,这个时候 Map中就可能存在 key为null但是 value不为null的项,这需要实际使用的时候使用完毕及时调用 remove方法避免内存泄漏。

如果使用线程池,由于线程可能并不是真正的关闭(比如newFixedThreadPool会保持线程一只存活)。因此,如果将一些大对象存放到 ThreadLocalMap中,可能会造成内存泄漏。因为线程没有关闭,无法回收,但是这些对象不会再被使用了。如果希望及时回收对象,则可以使用Thread.remove()方法将变量移除。文章来源地址https://www.toymoban.com/news/detail-838043.html

到了这里,关于ThreadLocal :在 Java中隱匿的魔法之力的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【java】开发——《并发编程》

    目录 一.jmm 二.并发了什么 1.只有一个核(单核)并发还有没有意义 2.单核,还有什么可见性问题 3.并发和并行 三.volitaile 1.变量的可见性问题 2.原因是什么 3.本次修改的变量直接刷到主内存 4.声明其他内存对于这个地址的缓存无效 四.happens-befo 1.顺序性问题 五.volitaile+cas 1.原

    2024年02月22日
    浏览(48)
  • Java并发编程:Semaphore

    信号量可以控制线程的并发数量 通常用于那些资源有明确访问数量限制的场景,常用于限流 。 使用 Semaphore 先调用 acquire() 获取,然后通过 try ... finally 保证在 finally 中释放。 Semaphore常用方法说明 acquire() 获取一个令牌,在获取到令牌、或者被其他线程调用中断之前线程一直

    2024年02月12日
    浏览(40)
  • Java并发编程-volatile

    volatile 是 java 虚拟机提供的一种轻量级的同步机制,它有三个重要的特性: 保证可见性 不保证原子性 禁止指令重排 要理解这三个特性,就需要对 JMM (JAVA内存模型)有一定的了解才行。 主要解决的问题: JVM中,每个线程都会存在本地内存,本地内存是公共内存的副本,各

    2024年02月06日
    浏览(41)
  • Java并发编程实战

    2023年06月19日
    浏览(50)
  • Java-并发编程-进阶篇

    在上一篇幅中对并发编程进行了简单介绍:并发与并行,进程与线程,以及并发编程的简单代码 但是在企业中往往并不能解决实际问题,例如: 1.synchronized在企业开发中会大大降低系统的性能,有什么解决方式,或者其他的替代方案 2.当线程被创建并启动以后,它既不

    2024年02月06日
    浏览(63)
  • Java并发编程面试题

    目录 一、线程、进程、程序 二、线程状态  三、线程的七大参数 四、线程有什么优缺点? 五、start 和 run 方法有什么区别? 六、wait 和 sleep的区别? 七、lock与synchronized的区别 八、Volatile是线程安全的吗?底层原理是什么? 九、synchronized作用和底层原理? 十一、Thre

    2024年02月12日
    浏览(58)
  • 【并发编程】深入理解Java并发之synchronized实现原理

    分析: 通过 new MyThread() 创建了一个对象 myThread ,这时候堆中就存在了共享资源 myThread ,然后对 myThread 对象创建两个线程,那么thread1线程和thread2线程就会共享 myThread 。 thread1.start() 和 thead2.start() 开启了两个线程,CPU会随机调度这两个线程。假如 thread1 先获得 synchronized 锁,

    2024年02月04日
    浏览(59)
  • Java并发编程详解:实现高效并发应用的关键技术

    在当前的计算机领域,高效的并发编程对于Java开发人员而言变得越发重要。作为流行的编程语言,Java提供了强大的并发编程支持,使开发人员能够充分发挥多核处理器和线程的潜力,构建高性能、高吞吐量的应用程序。本文将深入探讨Java并发编程的关键技术,包括线程安全

    2024年02月13日
    浏览(50)
  • java并发编程 LinkedBlockingDeque详解

    java 并发编程系列文章目录 首先queue是一种数据结构,一个集合中,先进后出,有两种实现的方式,数组和链表。从尾部追加,从头部获取。Deque是两端都可以添加,且两端都可以获取,所以它的方法会有一系列的Last,Frist语义,添加或获取等操作会指明哪个方向的,这也是

    2024年02月10日
    浏览(52)
  • 01.java并发编程面试宝典

    谈谈什么是线程池 线程池和数据库连接池非常类似,可以统一管理和维护线程,减少没有必要的开销。 因为频繁的开启线程或者停止线程,线程需要从新被 cpu 从就绪到运行状态调度,需要发生上下文切换 实际开发项目中 禁止自己 new 线程。必须使用线程池来维护和创建线

    2024年02月03日
    浏览(105)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包