【并发编程】ThreadLocal详解

简介: 【并发编程】ThreadLocal详解

1.ThreadLocal简介

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。

ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。

3ecbfc6095804e6da5b311526874b3da.jpg


2.ThreadLocal的简单使用

  • 启用两个线程分别对自己工作空间的num进行++
public class ThreadLocalDemo {
    private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()->0);
    public void add(){
        threadLocal.set(threadLocal.get()+1);
    }
    public Integer get(){
        return threadLocal.get();
    }
    public static void main(String[] args) {
        ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
        for (int i = 0; i < 2; i++) {
            new Thread(()->{
                while (true){
                    try {
                        Thread.sleep(5000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    threadLocalDemo.add();
                    System.out.println(Thread.currentThread().getName()+":线程当前获取num值为:"+threadLocalDemo.get());
                }
            }).start();
        }
    }
}
  • 运行结果

3dff6d7583944b81a3d9c59bb6596fc6.jpg

3.ThreadLocal的实现原理

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

只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们。除此之外,每个线程的本地变量不是存放在ThreadLocal实例中,而是存放在调用线程的Thread实例的threadLocals变量里面,也就是说ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载线程本地变量的容器,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法的时候,从它的threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在它的threadLocals中,所以不使用本地变量的时候调用remove()方法,将threadLocals中删除不使用的变量。

类关系图


139331c3b91045f2a42140a7a4a4f586.jpg

  • set方法源码分析
public void set(T value){
    //先获取当前线程实例(调用者线程)
    Thread t = Thread.currentThread();
    //先通过当前线程实例作为key去获取ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    //如果获取不是null,直接set值,key为当前定义的ThreadLocal变量的this引用。
    if(map !=null )
        map.set(this,value);
    //如果为null说明是首次添加,要先创建出对应的map
    else
        createMap(t,value);
}

getMap(Thread t)方法获取的是当前线程对应的threadLocals。

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

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

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

get方法源码分析

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

public T get(){
    //获取当前线程实例
    Thread t = Thread.currentThread();
    //获取当前实例的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    //判断map是否为空
    if(map != null){
        //获取map中的value返回
        ThreadLocalMap.Entry e = map.getEntry(this);
        if(e != null){
            T result = (T)e.value;
            return result;
        }
    }
    //如果map为空则调用setInitialValue进行初始化,设置threadLocals变量value设为null
    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
    if(map != null)
        //这块value 的值为null
        map.set(this,value);
    else
        //这块value的值也为null
        createMap(t,value);
    return value;
}
  • remove方法源码分析
    remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量。
public void remove(){
    //获取当前线程绑定的threadLocals
    ThreadLocalMap map = getMap(Thread.currentThread());
    //如果map不为null,就移除当前线程中指定的ThreadLocal实例的本地变量
    if(m != null){
        m.remove(this);
    }
}
  • Threadlocal的内部结构图

c353c9b61201462c81fe0caa20f70bb5.jpg

  • 从上面的我们可以看出:
  • 每一个Thread线程内部都有一个Map类型为ThreadLocalMap。

Map里面存储线程本地对象(key)和线程的变量副本(value)。

Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。对于不同的线程,每次获取副本值时,别的线程并不能获取当前西安城的副本值,于是形成了副本的隔离互相不干扰。

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,Map内部节点对象Entry也独立实现。

每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能够保存多个副本,就要多创建几个ThreadLocal实例。

Threadlocal里存储的可以是long,也可以是其他对象,同一个线程里不断set会不断覆盖。不同线程间数据互不影响。

ThreadLocalMap源码中的set方法



b7f9ace9f99f489cb9be42dab88a01f0.jpg

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


2f14fa25aff64fb1a09fbb0e95f3261a.jpg

4.ThreadLocal不支持继承性

  • 同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的
public class ThreadLocalDemo {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static void main(String[] args) {
        threadLocal.set("mainValue");
        new Thread(()->{
            System.out.println("子线程拿到threadLocal中的值:"+threadLocal.get());
        }).start();
        System.out.println("main线程拿到threadLocal中的值:"+threadLocal.get());
    }
}

f82ed3f029184592940bebb57875d654.jpg

5.InheritableThreadLocal支持继承性

  • 在上面说到的ThreadLocal类是不能提供子线程访问父线程的本地变量的,而
  • InheritableThreadLocal类则可以做到这个功能.
  • 同样是上面的代码,将ThreadLocal改成InheritableThreadLocal即可


deb2a9482f4b4f1ba56d910e951b32b2.jpg

6.从ThreadLocalMap看ThreadLocal使用不当的内存泄漏问题

  • 首先我们先来看一下Java的四种引用用类型。
  • 强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。
  • 软引用:如果一个对象持有软引用,那么在JVM发生OOM之前,是不会GC这个对象的,只有到JVM内存内存不足的时候才会GC这个对象,软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中。
  • 弱引用:弱引用对象指挥生存到下一次GC之前,只要发生GC无论内存够不够,都会把弱引用对象GC掉。
  • 虚引用:虚引用是所有引用中最弱的一个,其存在就是将关联虚引用的对象在被GC掉之后收到一个通知。


  • 分析ThreadLocalMap内部实现
  • 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;
 }

Entry的源码中我们可以看出Entry继承WeakReference<ThreadLocal<?>>类,当前的ThreadLocal的应用key被传递到WeakReference<ThreadLocal<?>>的构造函数中,所以ThreadLocalMap中的key为ThreadLocal的弱引用,value就是通过ThreadLocal设置的值,,如果当前线程一直没由调用该ThreadLocal 的remove方法,这时还有别的地方对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,由于ThreadLocalMap中key为ThreadlLocal的弱引用,当下一次GC的时候,key会被释放,但是value不会,这时就会造成key为空,但是value不为空的情况,造成内存泄露,所以实际开发中应尽量漏掉remove方法。


相关文章
|
6月前
|
存储 Java
JUC并发编程之深入理解ThreadLocal
ThreadLocal是Java标准库提供的一个工具类,位于java.lang包下。它允许你创建一个线程局部变量,每个线程都可以独立地访问自己的变量副本,互不干扰。这在某些场景下非常有用,比如在多线程环境下,每个线程需要维护自己的状态信息,但又不想通过方法参数传递的方式来实现。
|
2月前
|
算法 安全 Java
JAVA并发编程系列(12)ThreadLocal就是这么简单|建议收藏
很多人都以为TreadLocal很难很深奥,尤其被问到ThreadLocal数据结构、以及如何发生的内存泄漏问题,候选人容易谈虎色变。 日常大家用这个的很少,甚至很多近10年资深研发人员,都没有用过ThreadLocal。本文由浅入深、并且才有通俗易懂方式全面分析ThreadLocal的应用场景、数据结构、内存泄漏问题。降低大家学习啃骨头的心理压力,希望可以帮助大家彻底掌握并应用这个核心技术到工作当中。
|
4月前
|
存储 安全 Java
多线程线程安全问题之ThreadLocal是什么,它通常用于什么场景
多线程线程安全问题之ThreadLocal是什么,它通常用于什么场景
|
5月前
|
存储 安全 Java
Java多线程中线程安全问题
Java多线程中的线程安全问题主要涉及多线程环境下对共享资源的访问可能导致的数据损坏或不一致。线程安全的核心在于确保在多线程调度顺序不确定的情况下,代码的执行结果始终正确。常见原因包括线程调度随机性、共享数据修改以及原子性问题。解决线程安全问题通常需要采用同步机制,如使用synchronized关键字或Lock接口,以确保同一时间只有一个线程能够访问特定资源,从而保持数据的一致性和正确性。
5684 1
|
存储 安全 Java
【JUC基础】14. ThreadLocal
一般提到多线程并发总是要说资源竞争,线程安全。而通常保证线程安全的其中一种方式便是控制资源的访问,也就是加锁。其实还有另一种方式,那么便是增加资源来保证所有对象不竞争少数资源。比如,有100个人需要填写信息表,如果只有一只笔,那么要么变成串行,一个一个填写,要么就是我写一半你写一半。那么如果准备100只笔,100个人每个人都有一只笔能够填写信息表,那么就不会出现竞争的情况,也就能顺利的保证信息表的填写。这支笔也就是我们今天要说的ThreadLocal。
|
安全 Java 调度
【并发编程】线程安全性问题
【并发编程】线程安全性问题
10171 0
|
缓存 安全 Java
Java多线程之线程安全问题
Java多线程之线程安全问题
190 0
Java多线程之线程安全问题
|
Java 关系型数据库 MySQL
Java并发:ThreadLocal详解
Java并发:ThreadLocal详解
239 1
|
存储 缓存 Java
JUC并发编程——ThreadLocal
JUC并发编程——ThreadLocal
145 0
JUC并发编程——ThreadLocal
|
SQL Java 数据安全/隐私保护
JUC(三)ThreadLocal
JUC(三)ThreadLocal
JUC(三)ThreadLocal