• Java并发编程——ThreadLocal源码分析及知识点总结


    ThreadLocal简介

    ThreadLocal是为了在多线程下,实现对于一个变量访问的安全性。不同于加锁的可见性方式,ThreadLocal提供给每个线程有一个自己的变量,和其他线程互不干扰,所以,变量也是不共享的,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程不安全问题。

    每个线程使用ThreadLocal的时候,其实就是在使用自身线程对象的ThreadLocalMap字段,所以互不干涉。

    ThreadLocal的使用

    常用基本的使用API有:

    • get()
      获取当前线程下的threadLocal值。
    • set()
      设置当前线程下的ThreadLocal值。
    • remove()
      删除当前线程下的ThreadLocal值。

    看个例子:

    	// 设置ThreadLocal中存入的类型,创建一个实例对象
    	static ThreadLocal<String> localVar = new ThreadLocal<>();
    	
        public static void main(String[] args) {
            Thread t1  = new Thread(() -> {
                // 设置线程1中本地变量的值
                localVar.set("localVar1");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 打印本地变量
                System.out.println("thread1:" + localVar.get());
    			// 删除本地变量
    			localVar.remove();
                // 打印本地变量
                System.out.println("after remove : " + localVar.get());
            });
    
            Thread t2  = new Thread(() -> {
                // 设置线程2中本地变量的值
                localVar.set("localVar2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 打印本地变量
                System.out.println("thread2:" + localVar.get());
    			// 删除本地变量
    			localVar.remove();
                // 打印本地变量
                System.out.println("after remove : " + localVar.get());
            });
    
            t1.start();
            t2.start();
        }
    

    执行结果:
    在这里插入图片描述
    可以看出每个线程都有自己的变量,做了延时之后,两个线程中的变量也是没有任何干扰的,被删除之后,本地变量指向的对象就是空的。

    ThreadLocal实现原理

    源码分析

    下面分析JDK中ThreadLocal的源码:

    public class ThreadLocal<T> {
        private final int threadLocalHashCode = nextHashCode();
    
    	// 通过一个原子类保存下一个线程使用时的哈希值
        private static AtomicInteger nextHashCode =
            new AtomicInteger();
    
    	// 哈希值累加算子
        private static final int HASH_INCREMENT = 0x61c88647;
    
    	// 计算下一个哈希值
        private static int nextHashCode() {
            return nextHashCode.getAndAdd(HASH_INCREMENT);
        }
    
    	// 初始化键值对时候使用,给值赋为null
        protected T initialValue() {
            return null;
        }
    
    	// 将ThreadLocal转化为一个SuppliedThreadLocal
        public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
            return new SuppliedThreadLocal<>(supplier);
        }
    
    	// 默认构造函数
        public ThreadLocal() {
        }
    	
    	// 获取当前线程保存的值
        public T get() {
        	// 获得当前线程
            Thread t = Thread.currentThread();
            // 获取当前线程的ThreadLocalMap
            ThreadLocalMap map = getMap(t);
            if (map != null) {
            	// 获取当前线程ThreadLocalMap中这个ThreadLocal的键值对
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    // 获取键值对的值
                    T result = (T)e.value;
                    return result;
                }
            }
            // 如果当前线程没有初始化ThreadLocalMap,那就初始化一个新的map
            // 或者当前ThreadLocal第一次被当前线程调用,那就初始化一个新的键值对
            return setInitialValue();
        }
    
    	// 初始化一个ThreadLocalMap
        private T setInitialValue() {
        	// 设置初始值,初始值是null
            T value = initialValue();
            Thread t = Thread.currentThread();
            // 获取当前线程的ThreadLocalMap
            ThreadLocalMap map = getMap(t);
            if (map != null)
            	// 添加键值对
                map.set(this, value);
            else
            	// 如果当前线程还没有被创建过
                createMap(t, value);
            return value;
        }
    
    	// 设置当前线程对应的ThreadLocal值
        public void set(T value) {
        	// 获取当前线程
            Thread t = Thread.currentThread();
            // 获取当前线程的ThreadLocalMap 字段
            ThreadLocalMap map = getMap(t);
            
            if (map != null)
            	// ThreadLocalMap 中设置当前ThreadLocal键对应的值
                map.set(this, value);
            else
            	// map没有被初始化,实例化一个
                createMap(t, value);
        }
    
    	// 删除当前线程对应的ThreadLocal的值
         public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null)
                 m.remove(this);
         }
    
    	// 获取当前线程的ThreadLocalMap 
        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    
    	// 给当前线程初始化一个ThreadLocalMap 
        void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    
    	// 工厂方法创建一个继承某个ThreadLocalMap 的ThreadLocalMap 
        static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
            return new ThreadLocalMap(parentMap);
        }
    
        /**
         * Method childValue is visibly defined in subclass
         * InheritableThreadLocal, but is internally defined here for the
         * sake of providing createInheritedMap factory method without
         * needing to subclass the map class in InheritableThreadLocal.
         * This technique is preferable to the alternative of embedding
         * instanceof tests in methods.
         */
        T childValue(T parentValue) {
            throw new UnsupportedOperationException();
        }
    
    	// 特殊属性的ThreadLocal,用于配置ThreadLocal的初始值
        static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
    
            private final Supplier<? extends T> supplier;
    
            SuppliedThreadLocal(Supplier<? extends T> supplier) {
                this.supplier = Objects.requireNonNull(supplier);
            }
    
    		// 覆盖了设置初始值的方法
            @Override
            protected T initialValue() {
                return supplier.get();
            }
        }
    
    	// ThreadLocal存储值的映射类定义,每个线程中都存在这个类的字段
        static class ThreadLocalMap {
    
    		// 键值对定义
            static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;
    			// 键就是ThreadLocal,值可以是任何对象
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    
    		// 初始容量 
            private static final int INITIAL_CAPACITY = 16;
    
    		// map中的数组,存储键值对
            private Entry[] table;
    		
    		// 当前map中存储的键值对的数量
            private int size = 0;
    
    		// 负载值
            private int threshold; // Default to 0
    
    		// 设置负载
            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);
            }
    
    		// 从一个ThreadLocalMap搬运所有键值对到当前的ThreadLocalMap 中
            private ThreadLocalMap(ThreadLocalMap parentMap) {
            	// 获取需要赋值的ThreadLocalMap中的数组
                Entry[] parentTable = parentMap.table;
                // 获取数组的长度
                int len = parentTable.length;
                // 设置负载
                setThreshold(len);
                // 初始一个同等大小的数组
                table = new Entry[len];
    			// 遍历原数组复制键值对
                for (int j = 0; j < len; j++) {
                    Entry e = parentTable[j];
                    if (e != null) {
                        @SuppressWarnings("unchecked")
                        ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                        if (key != null) {
                            Object value = key.childValue(e.value);
                            Entry c = new Entry(key, value);
                            int h = key.threadLocalHashCode & (len - 1);
                            // 利用开放定址法,找到下一个不会产生哈希冲突的槽位 
                            while (table[h] != null)
                                h = nextIndex(h, len);
                            table[h] = c;
                            size++;
                        }
                    }
                }
            }
    
    		// 通过键获取键值对
            private Entry getEntry(ThreadLocal<?> key) {
            	// 通过键的哈希值计算当前键值对应的数字下标
                int i = key.threadLocalHashCode & (table.length - 1);
                Entry e = table[i];
                if (e != null && e.get() == key)
                    return e;
                else
                	// 如果当前位置不存在值或者有其他的键值对,说明可能产生了哈希冲突
                    return getEntryAfterMiss(key, i, e);
            }
    
    		// 键值对为空或者产生哈希冲突的解决函数
            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;
                    // key为null 而键值对不为null,说明key对应的ThreadLocal已经被垃圾回收了
                    if (k == null)
                    	// 清理无效的键值对
                        expungeStaleEntry(i);
                    else
                    	// 当前key不对,获取下一个槽位的下标
                        i = nextIndex(i, len);
                    e = tab[i];
                }
                return null;
            }
    		
    		// 设置对应键的值
            private void set(ThreadLocal<?> key, Object value) {
    
                Entry[] tab = table;
                int len = tab.length;
                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;
                    }
    				// key为null 而键值对不为null,说明key对应的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();
            }
    
    		// 删除对应位置的键值对
            private void remove(ThreadLocal<?> key) {
                Entry[] tab = table;
                int len = tab.length;
                int i = key.threadLocalHashCode & (len-1);
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) {
                    if (e.get() == key) {
                    	// 显式地将键值对的引用指向null,gc
                        e.clear();
                        // 清理无效的键值对
                        expungeStaleEntry(i);
                        return;
                    }
                }
            }
    
    		// 在staleSlot槽位发现无效的键,所做的替换等操作
            private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                           int staleSlot) {
                Entry[] tab = table;
                int len = tab.length;
                Entry e;
    
                //  向前扫描,找到第一个空的槽位
                int slotToExpunge = staleSlot;
                for (int i = prevIndex(staleSlot, len);
                     (e = tab[i]) != null;
                     i = prevIndex(i, len))
                    if (e.get() == null)
                        slotToExpunge = i;
    
                // 向后扫描
                for (int i = nextIndex(staleSlot, len);
                     (e = tab[i]) != null;
                     i = nextIndex(i, len)) {
                    ThreadLocal<?> k = e.get();
    
                    // 找到了key
                    if (k == key) {
                    	// 将其与无效的槽位进行交换
                    	// 更新对应槽位的值
                        e.value = value;
    
                        tab[i] = tab[staleSlot];
                        tab[staleSlot] = e;
    
                        // 
                        if (slotToExpunge == staleSlot)
                            slotToExpunge = i;
                        // 做一次启发式的清理
                        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                        return;
                    }
    
                    // 如果当前槽位已经无效,而且向前扫描中没有发现无效的槽位,旧更新当前位置
                    if (k == null && slotToExpunge == staleSlot)
                        slotToExpunge = i;
                }
    
                // 如果key不存在,就放一个新的在原地
                tab[staleSlot].value = null;
                tab[staleSlot] = new Entry(key, value);
    
                // 在探测过程没有发现任何无效的槽位,则做一次清理
                if (slotToExpunge != staleSlot)
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            }
    
    		// 清理函数,从staleSlot开始遍历,将无效的键值对清理
            private int expungeStaleEntry(int staleSlot) {
                Entry[] tab = table;
                int len = tab.length;
    
                // 断开当前键值对的引用,gc
                tab[staleSlot].value = null;
                tab[staleSlot] = null;
                size--;
    
                Entry e;
                int i;
                // 向后遍历,直到键值对不为空
                for (i = nextIndex(staleSlot, len);
                     (e = tab[i]) != null;
                     i = nextIndex(i, len)) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                    	// 清理掉当前无效的键值对
                        e.value = null;
                        tab[i] = null;
                        size--;
                    } else {
                    	// 说白了就是将空位后面的键值对放到正确的槽位上,把空位填上
                        int h = k.threadLocalHashCode & (len - 1);
                        if (h != i) {
    						// 当前位置不是他的最初哈希位置
                            tab[i] = null;
    
                            // 探测最初哈希位置后面的一个空位
                            while (tab[h] != null)
                                h = nextIndex(h, len);
                            tab[h] = e;
                        }
                    }
                }
                return i;
            }
    
            // 启发式的清理槽位,i对应的键值对是无效的,n用于控制扫描次数
            private boolean cleanSomeSlots(int i, int n) {
                boolean removed = false;
                Entry[] tab = table;
                int len = tab.length;
                do {
                    i = nextIndex(i, len);
                    Entry e = tab[i];
                    if (e != null && e.get() == null) {
                        n = len;
                        removed = true;
                        i = expungeStaleEntry(i);
                    }
                } while ( (n >>>= 1) != 0);
                return removed;
            }
    
         
            private void rehash() {
            	// 先做一次全表清理
                expungeStaleEntries();
    
                // 大于总表长度的一半就会进行扩容
                if (size >= threshold - threshold / 4)
                    resize();
            }
    
            // 扩容,每次扩大两倍
            private void resize() {
                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];
                    if (e != null) {
                        ThreadLocal<?> k = e.get();
                        if (k == null) {
                        	// 不会复制已经失效的键值对
                            e.value = null; // Help the GC
                        } else {
                        	// 线性探测来存放键值对
                            int h = k.threadLocalHashCode & (newLen - 1);
                            while (newTab[h] != null)
                                h = nextIndex(h, newLen);
                            newTab[h] = e;
                            count++;
                        }
                    }
                }
    
                setThreshold(newLen);
                size = count;
                table = newTab;
            }
    
            // 清理全表的无效键值对
            private void expungeStaleEntries() {
                Entry[] tab = table;
                int len = tab.length;
                for (int j = 0; j < len; j++) {
                    Entry e = tab[j];
                    if (e != null && e.get() == null)
                        expungeStaleEntry(j);
                }
            }
        }
    }
    
    

    源码总结

    • ThreadLocal是通过ThreadLocalMap类结构来存储数据的。每个线程上都可以保存一个自己的ThreadLocalMap,这样就实现了线程的隔离,它就是一个哈希表,key就是ThreadLocal,值就是ThreadLocal存储的数据。所以,ThreadLocal是将数据存储在了线程对象中,使用ThreadLocal存储数据的时候,都是被间接调用了线程本身的ThreadLocalMap。
    • ThreadLocalMap不同于HashMap的实现,它是采用开放寻址法来实现哈希冲突的。相同的是,默认初始容量是16,每次扩容的大小都是原先的两倍,这样就可以通过位与的方式来取余。ThreadLocalMap的负载因子是2/3。
    • ThreadLocalMap中和WeakHashMap一样,键值对采用的是弱引用,当ThreadLocal在外面没有被引用的时候,ThreadLocal也就没有存在的必要,就可以被垃圾回收了。如果这里是强引用,只要线程存在,就永远不会被回收。

    set()方法逻辑:

    • 线性探测的过程中,如果遇到的key都是有效的,并找到了对应key,那就直接替换value;
    • 如果发现某个槽中被回收了的key,就调用replaceStaleEntry,最终会把某个键值对放到这个槽上,并且会尽可能清理存在无效的key的槽。
      • 在replaceStaleEntry的过程中,如果找到了key,就会将这个key的键值对转移到第一个无效的槽位上;
      • 如果没有找到key,那么久直接在最初哪个无效键的槽位上放上新的键值对。
    • 如果探测过程中没有匹配到key,那么就会来连续段的末尾放上新键值对。然后做一次启发式的清理,如果没有清理掉一些key,而且当前的大小超出了负载,就会做一次rehash,其中包括全表清理和扩容。其中如果全表清理之后大小超过了threshold - threshold / 4,则进行扩容。

    get()方法逻辑:

    • 采用开放地址法的线性探测法,计算哈希值再取余之后,一个一个往后查询;
    • 如果当前下标的key就是对应的ThreadLocal,那就直接返回结果;
    • 调用getEntryAfterMiss进行线性探测,如果遇到被回收的键,就调用expungeStaleEntry进行清理,找了到key就返回结果,没有找到就返会null。

    为什么ThreadLocalMap要用开放寻址法?

    在开放寻址法中,所有的数据都存储在一个数组中,比起链表法来说,冲突的代价更高。
    所以,使用开放寻址法解决冲突的散列表,装载因子的上限不能太大。这也导致这种方法比链表法更浪费内存空间。
    但是,链表法指针需要额外的空间,故当结点规模较小时,开放寻址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放寻址法中的冲突,从而提高平均查找速度。

    ThreadLocal使用时的内存泄露问题

    如果一个ThreadLocal对象被回收了,但是ThreadLocalMap中的value对于【当前线程->当前线程的threadLocals(ThreadLocal.ThreadLocalMap对象)->Entry数组->某个entry.value】这样一条强引用链是可达的,因此value不会被回收。
    虽然从ThreadLocalMap的源码来看,它具有一套自我清理的机制,存在于get和set操作中,但是,如果线程一直没有被销毁,而且所有线程中也没有使用ThreadLocal,那么ThreadLocalMap中存储的value就不会被清理,也就可能造成内存泄漏的问题。

    解决办法:

    • 我们在使用ThreadLocal的时候,应当考虑合适调用ThreadLocal的remove方法,显式地清理无效地键值对,使得value被gc。
    • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。

    例如一个比较熟悉的场景就是对于一个请求一个线程的server如tomcat,在代码中对web api作一个切面,存放一些如用户名等用户信息,在连接点方法结束后,再显式调用remove。

    ThreadLocal的应用场景

    1. ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。
    2. ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。

    具体场景:

    • 经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection。
    • Session管理问题,将Session存入到ThreadLocal ,这样可以让当前线程中方便获取Session,不需要传来传去。
  • 相关阅读:
    hdu 1301 prime算法
    hdu 4763 kmp算法
    linux上安装程序出现的问题汇总
    linux之下载工具wget
    python之os模块
    管道和xargs的区别
    linux下查找文件或目录(which,whereis,locate,find)
    blast+学习之search tools
    linux的文件,目录操作命令(mv,rm,cp)
    PHPCMS V9 简单的二次开发
  • 原文地址:https://www.cnblogs.com/lippon/p/14205364.html
Copyright © 2020-2023  润新知