zoukankan      html  css  js  c++  java
  • 多线程学习笔记九之ThreadLocal

    多线程学习笔记九之ThreadLocal

    简介

      ThreadLocal顾名思义理解为线程本地变量,这个变量只在这个线程内,对于其他的线程是隔离的,JDK中对ThreadLocal的介绍:

    This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its{@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).

    大意是ThreadLocal提供了线程局部变量,只能通过ThreadLocal的set方法和get方法来存储和获得变量。

    类结构

      ThreadLocal类结构如下:

    可以看到ThreadLocal有内部类ThradLocalMap,ThreadLocal存储线程局部对象就是利用了ThreadLocalMap数据结构,在下面的源码分析也会先从这里开始。

    源码分析

    ThreadLocalMap

      ThreadLocalMap静态内部类Entry是存储键值对的基础,Entry类继承自WeakReference(为什么用弱引用在后面解释),通过Entry的构造方法表明键值对的键只能是ThreadLocal对象,值是Object类型,也就是我们存储的线程局部对象,通过super调用父类WeakReference构造函数将ThreadLocal<?>对象转换成弱引用对象
      ThreadMap存储键值对的原理与HashMap是类似的,HashMap依靠的是数组+红黑树数据结构和哈希值映射,ThreadMap依靠Entry数组+散列映射,ThreadLocalMap使用了Entry数组来保存键值对,Entry数组的初始长度为16,键值对到Entry数组的映射依靠的是int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);,通过ThreadLocal对象的threadLocalHashCode与(INITIAL_CAPACITY - 1)按位相与将键值对均匀散列到Entry数组上。

        static class 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数组大小
            private static final int INITIAL_CAPACITY = 16;
    
            //Entry数组
            private Entry[] table;
    
            //ThreadLocalMap实际存储键值对的个数
            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);
            }
    
            /**
             * Decrement i modulo len.
             */
            private static int prevIndex(int i, int len) {
                return ((i - 1 >= 0) ? i - 1 : len - 1);
            }
            
    		//构造一个ThreadLocalMap对象,并把传入的第一个键值对存储
            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作为做为键值对的键通过常量threadLocalHashCode映射到Entry数组,threadLocalHashCode初始化时会调用nextHashCode()方法,就是在nextHashCode的基础上加上0x61c88647,实际上每个ThreadLocal对象的threadLocalHashCode值相差0x61c88647,这样生成出来的Hash值可以较为均匀的散列到2的幂次方长度的数组中,具体可见这篇文章为什么使用0x61c88647
      由于采用的是散列算法,就需要考虑Hash冲突的情况,HashMap解决Hash冲突的方法是链表+红黑树,ThreadLocalMap解决方法是linear-probe(线性探测),简单来说如果散列对应的位置已经有键值对占据了,就把散列位置加/减一找到符合条件的位置放置键值对。

        // final常量,一旦确定不再改变
        private final int threadLocalHashCode = nextHashCode();
    
        /**
         * The next hash code to be given out. Updated atomically. Starts at
         * zero.
         */
        private static AtomicInteger nextHashCode =
            new AtomicInteger();
    
        /**
         * The difference between successively generated hash codes - turns
         * implicit sequential thread-local IDs into near-optimally spread
         * multiplicative hash values for power-of-two-sized tables.
         */
        private static final int HASH_INCREMENT = 0x61c88647;
    
        /**
         * Returns the next hash code.
         */
        private static int nextHashCode() {
            return nextHashCode.getAndAdd(HASH_INCREMENT);
        }
    
    	//构造方法
        public ThreadLocal() {
        }
    

    set(T value)

      简要介绍完了内部类ThreadLocalMap后,set方法属于ThreadLocal,首先获得与线程Thread绑定的ThreadLocalMap对象,再将ThreadLocal和传入的value封装为Entry键值对存入ThreadLocalMap中。注意,ThreadLocalMap对象是在线程Thread中声明的:
    ThreadLocal.ThreadLocalMap threadLocals = null;

        public void set(T value) {
    		//获得当前线程对象
            Thread t = Thread.currentThread();
    		//获得线程对象的ThreadLocalMap
            ThreadLocalMap map = getMap(t);
    		// 如果map存在,则将键值对存到map里面去
            if (map != null)
                map.set(this, value);
    		//如果不存在,调用ThreadLocalMap构造方法存储键值对
            else
                createMap(t, value);
        }
    
    	//返回线程t中声明的Thread
        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    
        void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    
    • set(ThreadLocal<?> key, Object value)
        在ThreadLocalMap存在的情况下,调用ThreadLocal类的set方法存储键值对,set方法需要考虑散列的位置已经有键值对:如果已经存在的键值对的键当存入的键,覆盖键值对的值;如果键值对的键ThreadLocal对象已经被回收,调用replaceStaleEntry方法删除table中所有陈旧的元素(即entry的引用为null)并插入新元素。
        private void set(ThreadLocal<?> key, Object value) {        
            Entry[] tab = table;
            int len = tab.length;
    		//利用ThreadLocal的threadLocalHahsCode值散列
            int i = key.threadLocalHashCode & (len-1);
    
    		//如果散列的位置不为空,判断是否是哈希冲突
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
    			
    			//如果此位置的键值对的键与传入的键相同,覆盖键值对的值
                if (k == key) {
                    e.value = value;
                    return;
                }
    
    			//键值对的键为空,说明键ThreadLocal对象被回收,用新的键值对代替过时的键值对
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
    
    		//散列位置为空,直接存储键值对
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
    

    get()

      获得当前线程中保存的以ThreadLocal对象为键的键值对的值。首先获取当前线程关联的ThreadLocalMap,再获得以当前ThreadLocal对象为键的键值对,map为空的话返回初始值null,即线程局部变量为null,

        public T get() {
    		//获取与当前线程绑定的ThreadLocalMap
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
    		//map不为空,获取键值对对象
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            return setInitialValue();
        }
    
        private Entry getEntry(ThreadLocal<?> key) {
    		//散列
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
    		//判断散列位置的键值对是否符合条件:e.get()==key
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
    
    	//线性探测寻找key对应的键值对
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
    
            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
    
    

    remove()

      从ThreadLocalMap中移除键值对,一般在get方法取出保存的线程局部变量后调用remove方法防止内存泄露。

        public void remove() {
            ThreadLocalMap m = getMap(Thread.currentThread());
            if (m != null)
                m.remove(this);
        }
    

    为什么ThreadLocalMap的键是WeakReferrence?

      键值对对象Enry的键是ThreadLocal对象,但是使用WeakReferrence虚引用包装了的,虚引用相对于我们经常使用的String str = "abc"这种强引用来说对GC回收对象的影响较小,以下是虚引用的介绍:

    WeakReference是Java语言规范中为了区别直接的对象引用(程序中通过构造函数声明出来的对象引用)而定义的另外一种引用关系。WeakReference标志性的特点是:reference实例不会影响到被应用对象的GC回收行为(即只要对象被除WeakReference对象之外所有的对象解除引用后,该对象便可以被GC回收),只不过在被对象回收之后,reference实例想获得被应用的对象时程序会返回null。

      如果Entry的键使用强引用,那么我们存入的键值对即使线程之后不再使用也不会被回收,生命周期将变得和线程的生命周期一样。而使用了虚引用之后,作为键的虚引用并不影响ThreadLocal对象被GC回收,当ThreadLocal对象被回收后,键值对就会被标记为stale entry(过期的键值对),再下一次调用set/get/remove方法后会进行  ThreadLocalMap层面对过期键值对进行回收,防止发生内存泄漏。
    注意:当我们使用了set方法存入局部变量后,如果不进行get/remove,那么过期的键值对无法被回收,所以建议在get取出存储变量后手动remove,可以有效防止内存泄漏。

    总结

      ThreadLocal实现了存储线程局部变量,ThreadLocal的实现并不是HashMap<Thread,Object>以线程对象为键,而是在线程内部关联了一个ThreadLocalMap用于存储键值对,键值对的键是ThreadLocal对象,所以ThreadLocal对象本身是不存储内容的,而是作为键与存储内容构成键值对。

  • 相关阅读:
    在Ubuntu上安装Mono
    CSS 外边距(margin)重叠及防止方法
    如何通过 iframe 共享 jQuery $.data?
    给ajax表单提交数据前面加上实体名称
    发布web应用程序是出现unsafe code
    ASP.NET MVC 基于页面的权限管理
    jquery旋转图片
    artTemplate子模板include
    JS调试必备的5个debug技巧_javascript技巧
    jQuery中position()与offset()区别
  • 原文地址:https://www.cnblogs.com/rain4j/p/10525421.html
Copyright © 2011-2022 走看看