ThreadLocal源码解析

ThreadLocal

ThreadLocal的作用:
1、是让每个线程都拥有自己的专属本地变量,即实现了线程隔离
2、可以通过ThreadLocal在同一线程的不同组件中传递公共变量

1、成员属性

     /* 用来寻址的hashcode
     * 使用 threadLocalHashCode & (table.length - 1) 计算结果得到的位置就是当前 entry 需要存放的位置。
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * 创建ThreadLocal对象时会使用到该属性:
     * 每创建一个threadLocal对象时,就会使用 nextHashCode 分配一个hash值给这个对象。
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     *每创建一个ThreadLocal对象,ThreadLocal.nextHashCode的值就会增长HASH_INCREMENT(0x61c88647)
     * hash增量为这个数字,带来的好处就是hash分布非常均匀
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * 下一个hashcode
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    /**
     * 初始化一个起始value:
     * 默认返回null,一般情况下都是需要重写这个方法的(例如第2小节的入门案例中就重写了该方法)。
     * 第一次调用get时会调用这个方法,如果之前没调用过set的话
     * @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;
    }


2、get方法

 public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //根据线程获取map
        ThreadLocalMap map = getMap(t);
        //map不为空表示线程的map已经被初始化了
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //执行到这里表示map为空要进行初始化或map还没有对应的Entry
        return setInitialValue();
    }

private T setInitialValue() {
        T value = initialValue();//初始值
        Thread t = Thread.currentThread();//当前线程
        ThreadLocalMap map = getMap(t); //获取map
        //map不为空就设值key为ThreadLocal,value为和ThreadLocal相关的Value
        if (map != null)
            map.set(this, value);
        //map为空就初始化
        else
            createMap(t, value);
        return value;
    }

 void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

整体流程:
1、获取当前线程
2、根据线程获取map
3、根据ThreadLocal获取map里对应的entry
4、entry不为空就返回entry里的值
5、map为空或entry为空就调用setInitialValue()方法初始化map或new一个entry

3、set方法

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

整体流程:
1、获取当前线程
2、根据线程获取map
3、map不为空调用map的set()方法把Threadlocal和value设置进map里
4、map为空就初始化map

ThreadLocalMap

ThreadLocal只是起到了一个中介的作用,真正存储数据的是ThreadLocalMap,ThreadLocal是ThreadLocalMap里的Entry的key,ThreadLocal其实就是一个外壳(相当于一个工具或桥梁把Thread和ThreadLocalMap连接起来)
1、内部类,存放数据的Entry,key是ThreadLocal(弱引用),value是和ThreadLocal关联的值

 static class Entry extends WeakReference<ThreadLocal<?>> {
            //ThreadLocal关联的value,是强引用
            Object value;
            //key是弱引用
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
 }


2、成员属性

          /**
         * table的初始容量,2的n次方
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * 存放数据的table
         */
        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) {
            //i是最后一位,下一位就是0
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * 前一索引, i是第一位,上一位就是最后一位
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }


3、构造函数
ThreadLocalMap是延时初始化,只会才第一次调用set方法或get方法时才会初始化

 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY]; //创建table
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //获取桶位
            table[i] = new Entry(firstKey, firstValue); //把entry放入对应桶位
            size = 1;
            setThreshold(INITIAL_CAPACITY); //设置阈值 初始化后的阈值是16*2/3 = 10
        }


4、getEntry()方法
这个方法是在ThreadLocal里的get()方法被调用的

private Entry getEntry(ThreadLocal<?> key) {
            //获取桶位
            int i = key.threadLocalHashCode & (table.length - 1);
            //桶位对应元素
            Entry e = table[i];
            //桶位有元素并且元素的key就是要查找的key
            if (e != null && e.get() == key)
                return e;
            else
            //桶位没元素或桶位有元素但是元素key和要查找的key不相等
                return getEntryAfterMiss(key, i, e);
        }

 

整体流程:
1、根据ThreadLocal的hashcode获取桶位
2、判断桶位是否有元素
3、如果有元素并且元素的key和要查找的key相等就返回桶位元素
4、如果桶位没元素或桶位有元素但是桶位和要查找的key不相等(这种情况一般是其他元素发生了hash冲突然后把元素放入了该位置),就调用getEntryAfterMiss()方法遍历后面桶位查找元素

5、getEntryAfterMiss()方法

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            // while循环条件:e != null 说明向后查找的范围是有限的,碰到 slot == null 的情况,则搜索结束!
            while (e != null) {
                ThreadLocal<?> k = e.get();
                // k == key条件成立:说明向后查询过程中找到合适的entry了,直接返回entry就ok了。
                if (k == key)
                    return e;
                // 桶位元素的key被回收了
                if (k == null)
                    // 做一次探测式过期数据回收。
                    expungeStaleEntry(i);
                else
                    // 更新index,继续向后搜索
                    i = nextIndex(i, len);
                // 获取下一个桶位中的entry。
                e = tab[i];
            }
            //来到这里表明找不到元素
            return null;
        }

整体流程
1、从i开始遍历后面的桶位,包括i
2、key相等就返回桶位元素
3、key为null就从当前桶位开始进行一次探测式清理,把过期的数据清理掉
4、遍历完后面的桶位还没找到元素就返回null

6、set()方法

 private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1); //获取桶位
            //从i开始往后找,直到找到一个可以使用的桶位
            // 1.k == key说明是替换
            // 2.碰到一个过期的slot ,这个时候可以强行占用
            // 3.查找过程中碰到 slot == null
            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被回收了,替换过期的ThreadLocal
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //来到这里说明遇到一个桶位为空的
            tab[i] = new Entry(key, value);
            int sz = ++size;
            // 做一次启发式清理:
            // 条件一:!cleanSomeSlots(i, sz) 成立,说明启发式清理工作未清理到任何数据..
            // 条件二:sz >= threshold 成立,说明当前table内的entry已经达到扩容阈值了..会触发rehash操作。
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

整体流程
1、根据ThreadLocal的hashcode获取桶位
2、从获取到的桶位开始往后遍历直到找到一个可以使用的桶位
3、key相等就执行更新操作,替换旧值
4、key为null就调用replaceStaleEntry()方法把过期数据和有效数据交换位置
5、3和4都没执行就执行新增操作
6、执行新增操作之后就做一次启发式清理,清理过期元素然后判断是否要扩容

7、expungeStaleEntry()方法
这个方法的作用:
从staleSlot位置开始继续向后查找过期数据,把过期数据都清理掉,直到碰到slot == null(key为null,value也是null)的情况结束,并返回结束时的数组下标位置~
这个方法在getEntryAfterMiss()方法被调用,当遇到key为null的entry时会调用这个方法进行一次过期数据的清理

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            // 因为staleSlot位置的entry是过期的,所以这里直接置为Null
            tab[staleSlot] = null;
            size--; //上面删除一个元素所以数量-1

            // Rehash until we encounter null
            Entry e; //当前遍历节点
            int i; //当前遍历节点的index
            // for循环从staleSlot + 1的位置开始搜索过期数据,直到碰到 slot == null 结束:
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                //桶位元素的key
                ThreadLocal<?> k = e.get();
                //key为null表示ThreadLocal被回收了,整个entry清空
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    // 执行到这里,说明当前遍历的slot中对应的entry 是非过期数据:
                    // 重新计算当前entry对应的 index
                    int h = k.threadLocalHashCode & (len - 1);
                    // 条件成立:说明当前entry存储时 就是发生过hash冲突,然后向后偏移过了,本来应该存在h,因为发生了hash冲突所以后移存在i
                    // 但是h到i这段数据有可能被清空了,所以重新找一个更靠近h的位置
                    if (h != i) {
                        //把当前位置清空
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        //从h开始往后找一个空的
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        // 将当前元素放入到 距离正确位置 更近的位置(有可能就是正确位置)
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

整体流程:
1、先清除staleSlot位置的entry
2、然后从staleSlot+1的位置开始遍历
3、遇到key为null的entry就把该entry清理
4、遇到key不为null的entry,就把该entry往前移,使它更靠近正确的位置,正确的位置即发生hash冲突的位置,正确位置到当前位置这段的数据有可能被清空了,所以要往前移,使它更靠近正确的位置
5、最后返回entry为null的下标

用图来看一下流程:
在这里插入图片描述
当调用getEntryAfterMiss()方法遇到key为null的entry时,就调用expungeStaleEntry()方法,此时staleslot为2,因为2的entry的key是null然后把该位置的entry清空,然后往后遍历发现3的entry不为空,然后重新计算这个entry的桶位,计算得到为1,然后把3的entry清空,从1开始往后找一个空的entry,因为刚才已经把2的entry清空了,所以2的entry是可用的,把原来3的entry放到2,此时的table变成
在这里插入图片描述
遍历4的key为null,把该位置的entry清空,遍历到5,5为null就结束遍历,此时table变成
在这里插入图片描述

8、replaceStaleEntry()方法
这个方法的作用是调用set()方法遇到一个过期的数据,然后把非过期数据和这个过期的数据交换位置,也是非过期数据前移,key和value是调用set()方法的key和value

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
            // slotToExpunge表示开始探测式清理过期数据的开始下标:默认从当前staleSlot开始
            int slotToExpunge = staleSlot;
            //从staleSlot开始往前找过期的ThreadLocal,直到桶位为null
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                // 条件成立:说明向前找到了过期数据,更新 探测清理过期数据的开始下标为 i
                if (e.get() == null)
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            //从staleSlot开始往后找过期的ThreadLocal,直到桶位为null
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                // key是要设置的key,k是遍历到当前桶位的key
                if (k == key) {
                    e.value = value;
                    // 将table[staleSlot]这个过期数据 放到 当前循环到的 table[i] 这个位置
                    tab[i] = tab[staleSlot];
                    //把当前entry放到table[staleSlot]
                    //table[i]和tab[staleSlot]的数据交换了 原来过期数据的桶位变成非过期的 原来非过期的桶位变成过期的 占用步骤
                    //table[staleSlot]在前,当前entry再后,即过期的数据后移了
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    // 如果条件成立:
                    // 1.说明replaceStaleEntry 一开始时 的向前查找过期数据 并未找到过期的entry
                    if (slotToExpunge == staleSlot)
                        // 开始探测式清理过期数据的下标 修改为 当前循环的index。
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                // 条件1:k == null 成立,说明当前遍历的entry是一个过期数据..
                // 条件2:slotToExpunge == staleSlot 成立,一开始时的向前查找过期数据并未找到过期的entry.
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            //来到这里表明是新增操作,原来table没有一样的key
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            // 条件成立:除了当前staleSlot 以外 ,还发现其它的过期slot了.. 所以要开启清理数据的逻辑..
            if (slotToExpunge != staleSlot)
                //expungeStaleEntry(slotToExpunge)先执行第一次清理,返回null
                //到执行cleanSomeSlots这个方法时就是第二次清理了
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

整体流程:
1、从staleSlot开始往前遍历(不包括staleSlot),如果找到一个key为null的元素,就更新slotToExpunge
2、从staleSlot开始往后遍历(不包括staleSlot),如果找到一个key和要设置的key相等,就把遍历位置的元素和过期的元素交换位置,遍历位置元素交换后前移,过期的元素交换后后移
3、如果往后遍历有过期元素,并且往前遍历没找到过期元素,就把当前位置更新为slotToExpunge,只会更新一次
4、往后遍历没找到一个key和要设置的key相等就要新增一个entry,新增在staleSlot处
5、如果除了当前staleSlot 以外 ,还发现其它的过期slot了就要清理其他过期的slot

用图来看一下流程:
在这里插入图片描述

调用set()方法时遍历到2的entry的key为null就把该位置占用,slotToExpunge的初始值是staleSlot即2,向前遍历,遍历到0的entry的key为null,更新slotToExpunge为0
1、往后遍历到3的entry和要设置的key相等,然后把2的entry和3的entry交换位置,此时table变成,此时会再清除一次过期的元素(启发式清理)然后返回
在这里插入图片描述
2、如果遍历到5都没有找到要查找的key,就把staleSlot的value清空然后,在该位置新增一个entry,此时table变成
在这里插入图片描述
然后从slotToExpunge即0开始做一次启发式清理,table变成
在这里插入图片描述


9、cleanSomeSlots()方法
这个方法在set()方法最后被调用,在replaceStaleEntry()方法往后遍历时找到一个key和要设置的key相等调用,位置i一定是没元素的,因为调用expungeStaleEntry()返回的是null
这个方法的作用是把所有过期的元素清理掉,而expungeStaleEntry()方法是清除一部分

private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                // 获取当前i的下一个下标
                i = nextIndex(i, len);
                // 获取table中当前下标为i的元素
                Entry e = tab[i];
                // 条件一:e != null 成立
                // 条件二:e.get() == null 成立,说明当前slot中保存的entry 是一个过期的数据..
                if (e != null && e.get() == null) {
                    // 重新更新n为 table数组长度 len是不变的,n一直在变
                    n = len;
                    removed = true;
                    // 以当前过期的slot为开始节点 做一次 探测式清理工作,把当前过期的slot到后面桶位没元素的这段的过期slot都清理掉
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0); //n除于2
            return removed;
        }
        

整体流程:
1、从i开始向后遍历,不包括i,找到一个过期元素就执行一次expungeStaleEntry()进行探测性清理,并更新n为table的长度,这样的作用是扩大搜索范围
2、如果一直没遇到过期元素并且n为0就停止扫描
3、最后返回是否清理过过期元素

10、扩容

private void rehash() {
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            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;
        }

在扩容前会把过期元素都清理掉,然后再判断是否需要扩容

ThreadLocal为什么要被设计成弱引用

ThreadLocal被设为弱引用的原因,当我们new一个ThreadLocal对象时,有一个强引用指向这个ThreadLocal对象,然后如果把这个强引用设为null之后,如果Entry的ThreadLocal也是强引用的话这个ThreadLocal对象就不会被gc,是要直到线程被gc掉这个ThreadLocal对象才会被gc掉,外部没有强引用访问到ThreadLocal对象这个ThreadLocal对象就没有存在的意义因为只能通过外部的强引用去访问ThreadLocal对象对应的ThreadLocalMap,所以这时ThreadLocal对象不会被gc的话会导致内存泄漏,如果Entry的ThreadLocal为弱引用的话只要外部的强引用被置为null或失效后,那么gc的时候会把ThreadLocal对象回收掉,这样就不会有内存泄漏的问题
ThreadLocal被设为弱引用的原因简单一句话总结就是为了防止内存泄漏
举个例子来说明一下

public class ThreadLocalTest {


    public static class MyRunnable implements Runnable {

        private final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();

        @Override
        public void run() {
            threadLocal.set( (int) (Math.random() * 100D) );

            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }

            System.out.println(Thread.currentThread().getName() + "的专属变量" + threadLocal.get());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable task = new MyRunnable();

        Thread t1 = new Thread(task,"线程1");
        t1.start();
        TimeUnit.MILLISECONDS.sleep(100);

        Thread t2 = new Thread(task,"线程2");
        t2.start();
        TimeUnit.MILLISECONDS.sleep(100);
    }
}

输出:
线程1的专属变量37
线程2的专属变量69

这段代码的threadLocal是ThreadLocal的强引用对象,如果threadLocal设为null之后,这时ThreadLocal对象只剩
Entry里的ThreadLocal弱引用,因为是弱引用,所以gc时ThreadLocal对象会被马上回收
如果Entry里的ThreadLocal是强引用的话那么ThreadLocal对象只会在内存不足或线程被回收的时候才会被回收,这样会导致内存泄漏,因为我们没有了主线程栈的ThreadLocal的强引用就不能通过ThreadLocal对象去访问ThreadLocalMap的Entry,访问不了ThreadLocalMap的Entry,那么ThreadLocal对象就没有存在的意义了所以要回收防止内存泄漏
在这里插入图片描述
因为ThreadLocalMap是Thread的成员变量,所以只能通过Thread访问对应的ThreadLocalMap

总结

在这里插入图片描述
1、一个Thread对应一个ThreadLocalMap,一个ThreadLocalMap对应多个ThreadLocal,所以是通过每个线程都拥有自己的ThreadLocalMap来实现线程隔离的,各个线程在调用同一个ThreadLocal对象的set(value)设置值的时候,是往各自的ThreadLocalMap对象数组中设置值
2、ThreadLocalMap采用的是线性冲突法,即发生hash冲突时元素就往后找
3、ThreadLocal设计成弱引用的原因是为了防止ThreadLocal对象不能被使用时无法被gc而导致内存泄漏
4、ThreadLocal调用set(),get()和remove()方法时都会把table里过期的元素清理掉
5、table的长度为2的n次方的原因和hashmap一样是为了高效取余
6、ThreadLocalMap的Entry的key为ThreadLocal,因为是弱引用,不会导致内存泄漏,但是value是强引用,只要线程不被回收掉就不会被回收,如果使用的是线程池就有可能导致value的内存泄漏,所以最好使用remove()方法把整个entry去掉
7、ThreadLocal并不是为线程保存对象的副本,它仅仅只起到一个索引的作用。它的主要目的是为每一个线程隔离一个类的实例,这个实例的作用范围仅限于线程内
8、ThreadLocal的思想是空间换时间,使每个Thread都拥有自己的ThreadLocalMap,这样虽然使用了更多的内存空间,但是不用考虑因为共享变量而带来的同步问题,因为变量没有共享了,是每个Thread私有的,只会访问到自己的ThreadLocal

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值