JUC并发编程——ThreadLocal

简介: JUC并发编程——ThreadLocal

正文


一、什么是ThreadLocal


ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。当线程结束后,每个线程所拥有的那个本地值会被释放。在多线程并发操作“线程本地变量"的时候,线程操作自己的变量副本,从而规避了线程安全问题,是一种空间换时间的思想。


二、ThreadLocal的使用


public class ThreadLocalDemo {
//    private  ThreadLocal<String> threadLocal= ThreadLocal.withInitial(()->{
//        return "test threadlocal"; //在定义ThreadLocal的时候设置一个获取初始值的回调函数。
//    });
    private  static ThreadLocal<String> threadLocal=new ThreadLocal<>();
    public static void main(String[] args) throws InterruptedException {
        ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
        Thread thread = new Thread(() -> {
            threadLocal.set("test");
            System.out.println(threadLocal.get());
        }, "t1");
        thread.start();
        thread.join();
        String s = threadLocal.get();
        System.out.println("主线程获取不到在thread线程设置的值:"+s);
    }
}


使用场景


线程隔离,ThreadLocal中的数据只属于当前线程,其本地值对其他线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。例如用户会话信息,数据库连接,Session数据管理。

跨函数传递数据,在同一个线程跨类、跨方法传递数据时,如果不用ThreadLocal,就需要靠方法的返回值或者参数传参,这样就增加了代码的耦合度。而ThreadLocal在当前线程设置值以后,在此线程中任何地方都能直接获取到值。避免通过参数传递数据降低代码耦合度。例如spring的编程式事务。


三、ThreadLocal源码分析


set()方法解读


public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null) {
            //value被绑定到threadLocal实例
            map.set(this, value);
        } else {
            //没有ThreadLocalMap则创建一个ThreadLocalMap实例,关联到thread实例
            createMap(t, value);
        }
    }


结:set()步骤


获得当前线程,然后获得当前线程的ThreadLocalMap成员,暂存于map变量。

如果map不为空,就将value设置到map中,当前的ThreadLocal作为key。

如果map为空,为该线程创建map,然后设置第一个“key-value对”,key为当前ThreadLocal的实例,value为set()方法参数的值。


get()方法解读


 public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取ThreadLocalMap
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null) {
            //如果不为空,以threadlocal实例为key获取值
            ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                //值不为空 返回值
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果为空 初始化一个值
        return setInitialValue();
    }
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);//绑定值
        } else {
            createMap(t, value);//创建threallocalmap
        }
        if (this instanceof TerminatingThreadLocal) {
            //当线程终止并且已在终止线程中初始化时(即使已使用空值初始化)会收到通知。
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }


小结:get()大致步骤


首先获取当前线程对象t, 然后从线程t中获取到ThreadLocalMap的成员属性threadLocals

如果当前线程的threadLocals已经初始化(即不为null) 并且存在以当前ThreadLocal对象为Key的值, 则直接返回当前线程要获取的对象;

如果当前线程的threadLocals已经初始化(即不为null)但是不存在以当前ThreadLocal对象为Key的的对象, 那么重新创建一个对象, 并且添加到当前线程的threadLocals Map中,并返回

如果当前线程的threadLocals属性还没有被初始化, 则重新创建一个ThreadLocalMap对象, 并且创建一个对象并添加到ThreadLocalMap对象中并返回。

ThreadLocal实现线程隔离的原理其实就是用了Map的数据结构给当前线程缓存了变量的值, 要使用的时候就从本线程的threadLocals对象中获取就可以了, key就是当前线程。当然在当前线程下获取当前线程里面的Map里面的对象并操作肯定没有线程并发问题了, 当然能做到变量的线程间隔离了。


remove()方法解读


remove()方法用于在当前线程的ThreadLocalMap中移除线程本地变量所对应的值


 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             m.remove(this);
         }
     }
  private void remove(ThreadLocal<?> key) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length; //entry的长度
        int i = key.threadLocalHashCode & (len-1);//key在数组上的槽点
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.refersTo(key)) { 
                //如果e等于key值就删除该值,这是一个native方法,方便垃圾回收
                e.clear();
                expungeStaleEntry(i);
                return;
            }
        }
    }


ThreadLocalMap解读


ThreadLocalMap内部静态类Entry实现
内部使用table数组存储Entry,默认大小INITIAL_CAPACITY(16)
参数说明:
size:table中元素的数量。
threshold:table大小的2/3,当size >= threshold时,遍历table并删除key为null的元素,
如果删除后size >= threshold*3/4时,需要对table进行扩容。
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
      Object value;
      Entry(ThreadLocal<?> k, Object v) {
         super(k);
         value = v;
           }
  }


可以看到


它并没有实现Map接口;

它没有public的方法, 最多有一个default的构造方法, 因为这个ThreadLocalMap的方法仅仅在ThreadLocal类中调用, 属于静态内部类;

ThreadLocalMap的Entry实现继承了WeakReference<ThreadLocal<?>>

该方法仅仅用了一个Entry数组来存储Key, Value; Entry并不是链表形式, 而是每个bucket里面仅仅放一个Entry。ThreadLocalMap 并不是我们所说的传统的map结构。

Entry对ThreadLocal使用了弱引用(WeakReference,弱引用指向的对象只能生存到下一次垃圾回收之前,也就是说当发生GC回收时,不管内存够不够弱引用的对象都会被回收)。


为什么要使用弱引用呢?如下代码


    public  void  a (){
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("test");//设置值
        threadLocal.get();//获取值
        //结束
    }


111.png


线程执行方法a()时,会创建一个ThreadLocal实例,这个是强引用,在调用set("test")之后,ThreadLocalMap会新建一个Entry实例,这个key是以弱引用的方式指向ThreadLocal的,执行完方法之后,栈帧被销毁,强引用的值也就没有了,但是ThreadLocal的实例还有Entry对应的引用,如果是强引用那么ThreadLocal和value值都不能会GC回收。从而会导致内存泄露问题。


set()方法解读


  private void set(ThreadLocal<?> key, Object value) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.refersTo(key)) {
//如果key等于条目的值,那就直接替换掉旧值
                e.value = value;
                return;
            }
            if (e.refersTo(null)) {
//如果不相等,调用replaceStaleEntry方法创建新值
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
//如果清理完无用条目(ThreadLocal被回收的条目)、并且数组中的数据大小 >= 阈值的时候对当前的Table进行重新哈希 
            rehash();
    }


getEntry()方法解读


    private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.refersTo(key))
//如果相等直接返回值
                return e;
            else
//如果没有匹配的值,就去后面的条目中查找
                return getEntryAfterMiss(key, i, e);
        }

四、ThreadLocal导致内存泄漏


内存泄漏就是不再使用的内存不能得到回收,引发的最终结果是内存溢出。


在什么情况下ThreadLocal会引发内存泄漏呢?


1、如果一个线程长时间运行而不被销毁,比如线程池。因为对于线程池里面不会销毁的线程, 里面总会存在着threadlocal的强引用, 因为final static 修饰的 ThreadLocal 并不会释放, 而ThreadLocalMap 对于 Key 虽然是弱引用, 但是强引用不会释放, 弱引用当然也会一直有值, 同时创建的value对象也不会释放, 就造成了内存泄露。


2、ThreadLocal应用被设置为null之后,且后续在同一Thread实例执行期间,没有发生对其他threadLocal实例的get(),set(),remove()操作(ThreadLocalMap 在执行这些方法时会清空key为null的Entry)。


如何避免ThreadLocal导致的内存泄漏?


1、尽量使用private static final修饰ThreadLocal实例,使用private和final主要是尽可能不让其他的类修改ThreadLocal的引用,static保证ThreadLocal实例的全局唯一。


2、ThreadLocal 使用完之后务必调用remove()方法。


参考


《JAVA高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计》-尼恩编著


Java 并发 - ThreadLocal详解 | Java 全栈知识体系

相关文章
|
6月前
|
安全 Java 编译器
高并发编程之什么是 JUC
高并发编程之什么是 JUC
51 1
|
6月前
|
存储 Java
JUC并发编程之深入理解ThreadLocal
ThreadLocal是Java标准库提供的一个工具类,位于java.lang包下。它允许你创建一个线程局部变量,每个线程都可以独立地访问自己的变量副本,互不干扰。这在某些场景下非常有用,比如在多线程环境下,每个线程需要维护自己的状态信息,但又不想通过方法参数传递的方式来实现。
|
2月前
|
算法 安全 Java
JAVA并发编程系列(12)ThreadLocal就是这么简单|建议收藏
很多人都以为TreadLocal很难很深奥,尤其被问到ThreadLocal数据结构、以及如何发生的内存泄漏问题,候选人容易谈虎色变。 日常大家用这个的很少,甚至很多近10年资深研发人员,都没有用过ThreadLocal。本文由浅入深、并且才有通俗易懂方式全面分析ThreadLocal的应用场景、数据结构、内存泄漏问题。降低大家学习啃骨头的心理压力,希望可以帮助大家彻底掌握并应用这个核心技术到工作当中。
|
5月前
|
安全 算法 Java
|
6月前
|
安全 Java
JUC并发编程之原子类
并发编程是现代计算机应用中不可或缺的一部分,而在并发编程中,处理共享资源的并发访问是一个重要的问题。为了避免多线程访问共享资源时出现竞态条件(Race Condition)等问题,Java提供了一组原子类(Atomic Classes)来支持线程安全的操作。
|
6月前
|
缓存 安全 Java
JUC并发编程之volatile详解
Java内存模型是Java虚拟机(JVM)规范中定义的一组规则,用于屏蔽各种硬件和操作系统的内存访问差异,保证多线程情况下程序的正确执行。Java内存模型规定了线程之间如何交互以及线程和内存之间的关系。它主要解决的问题是可见性、原子性和有序性。
|
安全 Java 调度
JUC并发编程(上)
JUC并发编程(上)
66 0
|
并行计算 Java 应用服务中间件
JUC并发编程超详细详解篇(一)
JUC并发编程超详细详解篇
1624 1
JUC并发编程超详细详解篇(一)
|
存储 缓存 监控
JUC并发编程(下)
JUC并发编程(下)
39 0
|
安全 Java 编译器
JUC 并发编程
JUC 并发编程