zoukankan      html  css  js  c++  java
  • Java并发(二十):线程本地变量ThreadLocal

    ThreadLocal是一个本地线程副本变量工具类。

    主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不同的变量值完成操作的场景。

    读写锁ReentrantReadWriteLock 记录线程持有的读锁数量时使用了ThreadLocal。Java并发(十):读写锁ReentrantReadWriteLock

    一、ThreadLocal的核心机制

    每个Thread线程内部都有一个Map,Tread类的ThreadLocal.ThreadLocalMap属性

    Map里面存储线程本地对象(key也就是当前的ThreadLoacal对象)和线程的变量副本(value)

    Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值

    数据结构:

    二、ThreadLocal源码分析

     ThreadLocal核心方法:

    • get():返回此线程局部变量的当前线程副本中的值。
    • initialValue():返回此线程局部变量的当前线程的“初始值”。
    • remove():移除此线程局部变量当前线程的值。
    • set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。

    内部类 ThreadLocalMap:

          static class Entry extends WeakReference<ThreadLocal<?>> {
              /** The value associated with this ThreadLocal. */
              Object value;
    
              Entry(ThreadLocal<?> k, Object v) {
                  super(k);
                  value = v;
              }
          }

    Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用类型的,Value并非弱引用。

    ThreadLocalMap的set()方法:

    private void set(ThreadLocal<?> key, Object value) {
    
            ThreadLocal.ThreadLocalMap.Entry[] tab = table;
            int len = tab.length;
    
            // 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置
            int i = key.threadLocalHashCode & (len-1);
    
            // 采用“线性探测法”,寻找合适位置
            for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
                e != null;
                e = tab[i = nextIndex(i, len)]) {
    
                ThreadLocal<?> k = e.get();
    
                // key 存在,直接覆盖
                if (k == key) {
                    e.value = value;
                    return;
                }
    
                // key == null,但是存在值(因为此处的e != null),说明之前的ThreadLocal对象已经被回收了
                if (k == null) {
                    // 用新元素替换陈旧的元素
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
    
            // ThreadLocal对应的key实例不存在也没有陈旧元素,new 一个
            tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
    
            int sz = ++size;
    
            // cleanSomeSlots 清楚陈旧的Entry(key == null)
            // 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

    ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

    ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。

    显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。

    所以这里引出的建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。

    get()方法:

    步骤:
    (1)获取当前线程的ThreadLocalMap对象threadLocals
    (2)从map中获取线程存储的K-V Entry节点。
    (3)从Entry节点获取存储的Value副本值返回。
    (4)map为空的话返回初始值null,即线程变量副本为null,在使用时需要注意判断NullPointerException。
        public T get() {
            // 获取当前线程
            Thread t = Thread.currentThread();
    
            // 获取当前线程的成员变量 threadLocal
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                // 从当前线程的ThreadLocalMap获取相对应的Entry
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
    
                    // 获取目标值        
                    T result = (T)e.value;
                    return result;
                }
            }
            return setInitialValue();
        }
    
        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }

    set()方法:

    步骤:
    (1)获取当前线程的成员变量map
    (2)map非空,则重新将ThreadLocal和新的value副本放入到map中。
    (3)map空,则对线程的成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal和value副本放入map中。
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

     initialValue()方法:

    该方法定义为protected级别且返回为null,很明显是要子类实现它的,所以我们在使用ThreadLocal的时候一般都应该覆盖该方法。该方法不能显示调用,只有在第一次调用get()或者set()方法时才会被执行,并且仅执行1次。
     protected T initialValue() {
            return null;
        }

    三、使用场景

    简单使用场景一:

    public class SeqCount {
    
        private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){
            // 实现initialValue()
            public Integer initialValue() {
                return 0;
            }
        };
    
        public int nextSeq(){
            seqCount.set(seqCount.get() + 1);
    
            return seqCount.get();
        }
    
        public static void main(String[] args){
            SeqCount seqCount = new SeqCount();
    
            SeqThread thread1 = new SeqThread(seqCount);
            SeqThread thread2 = new SeqThread(seqCount);
            SeqThread thread3 = new SeqThread(seqCount);
            SeqThread thread4 = new SeqThread(seqCount);
    
            thread1.start();
            thread2.start();
            thread3.start();
            thread4.start();
        }
    
        private static class SeqThread extends Thread{
            private SeqCount seqCount;
    
            SeqThread(SeqCount seqCount){
                this.seqCount = seqCount;
            }
    
            public void run() {
                for(int i = 0 ; i < 3 ; i++){
                    System.out.println(Thread.currentThread().getName() + " seqCount :" + seqCount.nextSeq());
                }
            }
        }
    }

     运行结果:

      Thread-1 seqCount :1
      Thread-3 seqCount :1
      Thread-3 seqCount :2
      Thread-3 seqCount :3
      Thread-0 seqCount :1
      Thread-0 seqCount :2
      Thread-0 seqCount :3
      Thread-2 seqCount :1
      Thread-1 seqCount :2
      Thread-1 seqCount :3
      Thread-2 seqCount :2
      Thread-2 seqCount :3

    类似的ReentrantReadWriteLock中的java.util.concurrent.locks.ReentrantReadWriteLock.Sync.readHolds属性,也使用的了TreadLocal来记录占有该读锁的线程重入次数。可参考:Java并发(十):读写锁ReentrantReadWriteLock

    注意:initialValue()方法返回一个对象时,get()和set()方法操作的其实是同一个对象的属性,不能实现线程隔离。

    使用场景二:session获取场景

    每个线程访问数据库都应当是一个独立的Session会话,如果多个线程共享同一个Session会话,有可能其他线程关闭连接了,当前线程再执行提交时就会出现会话已关闭的异常,导致系统异常。此方式能避免线程争抢Session,提高并发下的安全性。

    //获取Session
    public static Session getCurrentSession(){
        Session session =  threadLocal.get();
        //判断Session是否为空,如果为空,将创建一个session,并设置到本地线程变量中
        try {
            if(session ==null&&!session.isOpen()){
                if(sessionFactory==null){
                    rbuildSessionFactory();// 创建Hibernate的SessionFactory
                }else{
                    session = sessionFactory.openSession();
                }
            }
            threadLocal.set(session);
        } catch (Exception e) {
            // TODO: handle exception
        }
    
        return session;
    }

    四、内存泄漏问题

    ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

    关于GC以及引用状态:JVM垃圾回收机制

    其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

    但是这些被动的预防措施并不能保证不会内存泄漏:

      使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
      分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

    内存泄漏实例分析:ThreadLocal 内存泄露的实例分析 

    解决:

    每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

    在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

    参考资料 / 相关推荐

    线程管理(九)使用本地线程变量

    不共享有时是最好的

    【死磕Java并发】—–深入分析ThreadLocal

    Java并发编程:深入剖析ThreadLocal

    ThreadLocal-面试必问深度解析

    深入分析 ThreadLocal 内存泄漏问题

    ThreadLocal 内存泄露的实例分析 

  • 相关阅读:
    车标知识学习网页开发,与Flask通过base64展示二进制图片 #华为云·寻找黑马程序员#
    大型情感剧集Selenium:3_元素定位 #华为云·寻找黑马程序员#
    大型情感剧集Selenium:2_options设置 #华为云·寻找黑马程序员#
    【nodejs原理&源码赏析(9)】用node-ssh实现轻量级自动化部署
    大型情感剧集Selenium:1_介绍 #华为云·寻找黑马程序员#
    使用Python开发小说下载器,不再为下载小说而发愁 #华为云·寻找黑马程序员#
    #华为云·寻找黑马程序员#【代码重构之路】如何“消除”if/else
    #华为云·寻找黑马程序员#【代码重构之路】如何优雅的关闭外部资源
    走近深度学习,认识MoXing:初识华为云ModelArts的王牌利器 — MoXing
    开启 J2EE(一)—‘全明星队伍’
  • 原文地址:https://www.cnblogs.com/hexinwei1/p/10032907.html
Copyright © 2011-2022 走看看