zoukankan      html  css  js  c++  java
  • ThreadLocal源码分析

    概述

    ThreadLocal提供了一种线程安全的数据访问方式,每个线程中都存在一个共享变量副本,从而实现多线程状态下的线程安全。

    demo

     public static void main(String[] args) {
            final ThreadLocal<Integer> MAIN = ThreadLocal.withInitial(() -> 100);
    
            MAIN.set(200);
    
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + " MAIN:" + MAIN.get());
            }).start();
    
            System.out.println("MAIN:" + MAIN.get());
            
            //一定要注意,当ThreadLocal不再使用时,一定要调用remove方法,以免内存泄漏
            MAIN.remove();
    
            System.out.println("MAIN:" + MAIN.get());
    
        }
    

    运行之后,打印结果如下:

    MAIN:200
    Thread-0 MAIN:100
    

    MAIN是在主线程中set的值,可以在主线程中使用get方法获得,但在线程中调用get方法,结果却还是100,这是为什么呢?这个原因其实也是为什么说ThreadLocal能被称之为线程安全的原因。下面我们就通过源码来一探究竟。

    关键属性

        
        //表示当前ThreadLocal的hashCode,用于计算当前ThreadLocal在ThreadLocalMap中的索引位置
        private final int threadLocalHashCode = nextHashCode();
    
        // static+ AtomInteger 保证了在一台机器中每个ThreadLocal的threadLocalHashCode是唯一的
        // 被static修饰十分关键,因为一个线程在处理业务时,ThreadLocalMap会被set多个ThreadLocal,多个     
       // ThreadLocal就依靠着threadLocalHashCode进行区分
        private static AtomicInteger nextHashCode =
            new AtomicInteger();
    
        // 增量常量
        private static final int HASH_INCREMENT = 0x61c88647;
    
        //计算 ThreadLocal 的 hashCode 值(就是递增)
        private static int nextHashCode() {
            return nextHashCode.getAndAdd(HASH_INCREMENT);
        }
    

    常用方法

    set方法

    每个线程的set方法都是串行的,因而不会有线程安全的问题。

    public void set(T value) {
            //获取当前线程
            Thread t = Thread.currentThread();
            // 获取ThreadLocalMap
            ThreadLocalMap map = getMap(t);
            // 当前的threadLocalMap之前有设置值,则直接进行设置,否则就初始化
            if (map != null)
                map.set(this, value);
            else
                //初始化threadLocalMap
                createMap(t, value);
        }
    
    //获取线程的threadLocalMap属性
    ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    
    //初始化 ThreadLocalMap
     void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    

    get方法

    get方法主要是从ThreadLocalMap中取出当前ThreadLocal存储的值。

    public T get() {
            //获取当前线程
            Thread t = Thread.currentThread();
            //获取ThreadLocalMap
            ThreadLocalMap map = getMap(t);
            //map不为null时
            if (map != null) {
                //从map中取出Entry,由于ThreadLocalMap在set时解决hash冲突的策略不同,get的逻辑也不同
                ThreadLocalMap.Entry e = map.getEntry(this);
                //entry不为空的话,读取当前ThreadLocal中保存的值
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;、
                        //返回值
                    return result;
                }
            }
            //若是map为空 则将当前线程的ThreadLocal初始化 并返回初始值null
            return setInitialValue();
        }
    
    
    private T setInitialValue() {
            //获取初始值
            T value = initialValue();
           // 获取当前线程
            Thread t = Thread.currentThread();
           // 从当前线程中获取ThreadLocalMap
            ThreadLocalMap map = getMap(t);
           // 如果map不为null的话,直接进行set
            if (map != null)
                map.set(this, value);
            else
                //否则初始化ThreadLocalMap
                createMap(t, value);
         //返回值
            return value;
        }
    //直接return null
    protected T initialValue() {
            return null;
        }
    

    remove方法

    由于ThreadLocal在使用不当时可能存在内存泄漏的场景,因而,在使用完ThreadLocal使用完之后,一定要显示的调用remove方法进行清除。

        public void remove() {
             //获取当前线程绑定的ThreadLocalMap
             ThreadLocalMap m = getMap(Thread.currentThread());
            // map不为null的话  
            if (m != null)
                //从map中移除当前threadLocal对应的K-V
                 m.remove(this);
         }
    

    可以看出,无论是set、get还是remove 方法,其底层原理都比较简单,但却都包含一个共性,就是使用到了ThreadLocalMap,那么ThreadLocalMap又是一个什么东东呢?

    ThreadLocalMap

    ThreadLocalMap是ThreadLocal中的一个静态内部类,其本质上是一个简单的Map结构,key是ThreadLocal类型,value是ThreadLocal保存的值,底层是一个Entry类型数组组成的数据结构。Entry类的结构如下所示:

    static class ThreadLocalMap {
    
            // Entry继承自WeakReference 因而Entry数组中每个Entry节点也是一个弱引用,当没有引用指向时,
            //会被回收
    static class Entry extends WeakReference<ThreadLocal<?>> {
                // 当前ThreadLocal关联的值
                Object value;
        
                // WeakReference的引用 referent就是ThreadLocal
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    
            //初始容量大小
            private static final int INITIAL_CAPACITY = 16;
    
            // Entry数组
            private Entry[] table;
    
            //Entry数组大小
            private int size = 0;
    
            //阈值
            private int threshold; // Default to 0
    
            //设置阈值方法  可以看出是容量的2/3
            private void setThreshold(int len) {
                threshold = len * 2 / 3;
            }
        
           //计算索引的下一个位置
            private static int nextIndex(int i, int len) {
                return ((i + 1 < len) ? i + 1 : 0);
            }
        
           //计算索引的上一个位置
            private static int prevIndex(int i, int len) {
                return ((i - 1 >= 0) ? i - 1 : len - 1);
            }
        
        //构造方法
         ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
                table = new Entry[INITIAL_CAPACITY];
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
                table[i] = new Entry(firstKey, firstValue);
                size = 1;
                setThreshold(INITIAL_CAPACITY);
            }
    
    .............
        
    }
    

    ThreadLocal实现线程隔离的原理

    ThreadLocal是线程安全的,主要是因为ThreadLocalMap是线程Thread的一个属性,如下所示:

    threadLocals和inheritableThreadLocals分别是线程的两个属性,因而每个线程的ThreadLocals都是隔离独享的。在Thread的init方法中,父线程在创建子线程的情况下,会拷贝inheritableThreadLocals的值,但不会拷贝threadLocals的值,如下所示:

    // Thread中的init方法
    private void init(ThreadGroup g, Runnable target, String name,
                          long stackSize, AccessControlContext acc) {
        ...
            //当父线程的inheritableThreadLocals的值不为空时
            // 会把inheritable里面的值全部传递给子线程
            if (parent.inheritableThreadLocals != null)
                this.inheritableThreadLocals =
                   //
                    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
         ...
        }
    
    
    //ThreadLocal中的createInheritedMap方法
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
            return new ThreadLocalMap(parentMap);
        }
    

    在线程创建时,会把父线程的inheritableThreadLocals属性值进行拷贝。

    set方法

      private void set(ThreadLocal<?> key, Object value) {
               //保存entry数组
                Entry[] tab = table;
               // 获取数组长度
                int len = tab.length;
               // 计算索引下标位置
                int i = key.threadLocalHashCode & (len-1);
                
          // 整体策略:查看 i 索引位置有没有值,有值的话,索引位置 + 1,直到找到没有值的位置
        // 这种解决 hash 冲突的策略,也导致了其在 get 时查找策略有所不同,体现在 getEntryAfterMiss 中
                for (Entry e = tab[i];
                     e != null;
                     // nextIndex 就是让在不超过数组长度的基础上,把数组的索引位置 + 1
                     e = tab[i = nextIndex(i, len)]) {
                    //获取threadLocal
                    ThreadLocal<?> k = e.get();
                    
                    //如果二者相等 直接替换并返回
                    if (k == key) {
                        e.value = value;
                        return;
                    }
                    
                    //如果为空 说明当前的threadLocal被清理了,直接替换并返回
                    if (k == null) {
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
          
                //当前i位置没有值的话,直接生成一个Entry
                tab[i] = new Entry(key, value);
                // 维护size
                int sz = ++size;
                // 当数组大小大于等于扩容阈值(数组大小的三分之二)时,进行扩容
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }
    

    需要注意几点的是:

    • 通过递增的AtomicInteger作为ThreadLocal的hashCode的;
    • 通过计算hashCode计算的索引位置i处,如果已经有值的话,会从i开始,通过+1,不断往后寻找,直到找到索引位置为空的地方,把当前ThreadLocal作为key放进去;

    getEntry方法

    private Entry getEntry(ThreadLocal<?> key) {
                // 计算索引值
                int i = key.threadLocalHashCode & (table.length - 1);
                // 获取索引处的entry
                Entry e = table[i];
               // e 不为空,并且 e 的 ThreadLocal 的内存地址和 key 相同,直接返回,否则就是没有找到,
               //继续通过 getEntryAfterMiss 方法找
                if (e != null && e.get() == key)
                    return e;
                else
                    // 这个取数据的逻辑,是因为 set 时数组索引位置冲突造成的
                    return getEntryAfterMiss(key, i, e);
            }
    
    
      private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
               //暂存entry数组
                Entry[] tab = table;
               // 获取数组长度
                int len = tab.length;
          
               //遍历数组
                while (e != null) {
                    // 内存地址一样,表示找到了 直接返回即可
                    ThreadLocal<?> k = e.get();
                    if (k == key)
                        return e;
                    //如果为null的话 删除没用的key
                    if (k == null)
                        expungeStaleEntry(i);
                    else
                        //否计算出下一个索引的位置
                        i = nextIndex(i, len);
                    //继续下一次遍历
                    e = tab[i];
                }
              //如果最后entry数组遍历结束都没有找到,直接返回null
                return null;
            }
    

    resize方法

            //扩容
            private void resize() {
                //暂存旧的entry数组
                Entry[] oldTab = table;
                //旧数组的容量
                int oldLen = oldTab.length;
                //新数组的容量
                int newLen = oldLen * 2;
                //初始化一个空的新数组
                Entry[] newTab = new Entry[newLen];
                //记录数量
                int count = 0;
                
                //开始复制
                for (int j = 0; j < oldLen; ++j) {
                    //旧数组的一个节点
                    Entry e = oldTab[j];
                    //不为null的话开始进行复制
                    if (e != null) {
                        //获取threadLocal
                        ThreadLocal<?> k = e.get();
                        //为null的话直接进行清除
                        if (k == null) {
                            e.value = null; // Help the GC
                        } else {
                            //计算threadLocal在新entry中的索引位置
                            int h = k.threadLocalHashCode & (newLen - 1);
                            //如果该位置以及有值了,那么就寻找下一个索引位置,直到为空
                            while (newTab[h] != null)
                                h = nextIndex(h, newLen);
                            //将值拷贝到新数组的位置
                            newTab[h] = e;
                            //更新数量
                            count++;
                        }
                    }
                }
                
                //重新设置扩容时的阈值,新数组长度的2/3
                setThreshold(newLen);
                //维护size大小
                size = count;
                //将entry数组的引用指向新数组
                table = newTab;
            }
    

    扩容时的逻辑也比较清晰:

    • 扩容时新数组大小为原来的两倍;
    • 扩容时没有线程安全问题,因为ThreadLocalMap是线程本身的一个属性,一个线程同一时刻只能对ThreadLocalMap进行操作,因为同一个线程执行业务逻辑时必然是串行的,那么操作ThreadLocalMap必然也是串行的;

    ThreadLocal内存泄漏原因探究

    在demo演示中我们提到过,在使用ThreadLocal的过程中,如果使用不当,最后可能会导致内存泄漏的问题,那么原因是什么呢?

    先来明确一下内存泄漏的概念:

    内存泄漏主要有两种情况,一是堆中申请的空间没有被释放;二是对象已不再被使用,但仍在内存中保留着。
    

    先来看一下ThreadLocal和当前Thread在堆栈中的布局图吧。

    注:上面的连接线中,实线代表强引用,虚线代表弱引用。

    上面我们已经说过了,ThreadLocalMap中的Entry静态类继承了WeakReference类,它的Key是ThreadLocal对象的弱引用。那什么是弱引用呢?根据《深入理解Java虚拟机 第二版》中的定义:

    弱引用是用来描述非必需对象的,被弱引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾回收器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
    

    一般情况下,当我们不再使用threadLocal变量时,会手动将该变量置为null,这样堆中的threadLocal实例对象将不会再被任何强引用所指向,这样垃圾回收器就可以对其进行回收。此时,根据上图我们可知,在垃圾回收之后,ThreadLocalMap中的Entry的key已经变成了null。但是,如果此时线程还存活着继续运行,则key为null,但value指向Object(存在着强引用关系)的Entry对象仍然不会被回收,此时就会发生内存泄漏。当然,如果线程在完成任务之后就结束了生命周期,那么随后ThreadLocalMap和Entry也会随之消亡。但如果使用的是线程池,线程在完成任务之后会回放到线程池中从而继续被复用,那么此时value就会一直存在,导致内存泄漏。

    那么问题来了,既然弱引用可能导致内存泄漏,那么改为强引用呢?

    还是跟上面一样,一起来分析一下。当手动将threadLocal置为null时,虽然threadLocal Ref—ThreadLocal实例之间没有引用关系,但Entry中key与ThreadLocal之间仍然存在着强引用关系,就会产生key和value都不为null的Entry对象,但是ThreadLocal我们明明已经不需要了,且只有线程一直运行下去,那么threadLocal实例还是无法被回收,这样还是会发生内存泄漏

    因而,虽然弱引用同样也会导致内存泄漏的问题,但ThreadLocal的set、get以及remove操作都会清除ThreadLocalMap中Entry数组中key为null的Entry,从而降低出现内存泄漏的风险。

  • 相关阅读:
    多线程、线程同步、Lock接口
    线程池、匿名内部类、创建线程
    commons-IO、多线程
    PHP的一个坑
    jquery 实现导航栏滑动效果
    浏览器加载模式:window.onload和$(document).ready()的区别(详解)
    jQuery绑定事件-多种方式实现
    baidu-fex 精彩文章
    jquery1.11 操作checkbox:全选、取消全选、获取选择元素、获取取消选择元素(总结)
    倒计时时钟-原创
  • 原文地址:https://www.cnblogs.com/reecelin/p/13341320.html
Copyright © 2011-2022 走看看