ThreadLocal解析

前言

ThreadLocal看类名就是线程本地变量的意思。从使用上来说,如果定义了一个ThreadLocal,那么各个线程针对这个ThreadLocal进行get/set都是线程独立的,也就是说,是线程隔离的本地变量。

Thread类中有一个ThreadLocalMap类型的成员,是一种hashmap,它以ThreadLocal作为key。所以,通过Thread对象和ThreadLocal对象二者,才可以唯一确定到一个value上去。线程隔离的关键,正是因为这种对应关系用到了Thread对象。

例子:

	Thread t = Thread.currentThread();
		//现根据当前线程确定map变量
        ThreadLocalMap map = getMap(t);
	//再根据当前ThreadLocalMap确定唯一的Entry
	ThreadLocalMap.Entry e = map.getEntry(this);
	e.value

1、类定义

1.1、成员变量

	 //用于取模计算数组的下标 如    threadLocalHashCode & length,散列法取数组下标,避免hash冲突
	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);
    }
  • 每个ThreadLocal对象都有一个int值的成员变量,作为它在ThreadLocalMaps里的哈希值。

  • 第一次构造ThreadLocal对象时,它的哈希值为0。此后,每个新构造出来的ThreadLocal对象都新增一个模数0x61c88647。

  • 使用了AtomicInteger,就算两个线程同时构造了ThreadLocal对象,也能保证这个int成员变量各自线程的不同。

  • 模数0x61c88647在2的幂为容量的哈希表上,能够完美散列,没有一个元素会哈希冲突

2、方法解析

2.1、get方法解析

public T get() {
    	1、获取当前线程
        Thread t = Thread.currentThread();
    	2、根据当前线程获取ThreadLocalMap变量
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            3、调用内部类ThreadLocalMap方法获取Entry,后续讲解
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                3.1、获取value的值
                T result = (T)e.value;
                return result;
            }
        }
    	4、设置初始值
        return setInitialValue();
    }
***********************************************************************
    private T setInitialValue() {
        1、根据SuppliedThreadLocal函数时接口获取值
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            2、设置初始值,后续讲解
            map.set(this, value);
        else
            3、创建ThreadLocalMap
            createMap(t, value);
        return value;
    }

***********************************************************************
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  • 刚开始就直接找到了调用当前函数的线程是哪个(Thread t = Thread.currentThread();),然后调用getMap,最后居然直接去取Thread对象的一个ThreadLocal.ThreadLocalMap类型的threadLocals成员.

  • 上面这点就直接解释了,为什么ThreadLocal可以实现线程隔离的线程私有变量。因为它把这个Map对象直接作为了Thread对象的成员,这样,每个运行的线程都对应到一个唯一的Thread对象,而每个Thread对象都保存着各自的ThreadLocal.ThreadLocalMap类型的成员变量。

2.2、set和remove方法解析

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

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

比较简单,不做解析,重点关注后续的ThreadLocalMap中的方法。

2.3、父子线程

    /**
     * 工厂方法,当父线程创建子线程,通过此方法构造出一个ThreadLocalMap赋值给
     * 子线程的inheritableThreadLocals成员。
     * 这个方法是父线程通过new Thread间接调用到的。
     *
     * @param  parentMap 父线程(当前线程)的inheritableThreadLocals成员
     * @return 父线程的inheritableThreadLocals成员,浅复制而来的一个ThreadLocalMap
     */
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

    /**
     * 默认实现是抛异常,InheritableThreadLocal子类实现了此方法,逻辑可为:
     * 可根据父线程的value来设置子线程的value。
     */
    T childValue(T parentValue) {
        throw new UnsupportedOperationException();
    }

3、ThreadLocalMap静态内部类解析

3.1、成员变量

//初始化容量,默认16
private static final int INITIAL_CAPACITY = 16;
//Entry数组
private Entry[] table;
//数组元素个数
private int size = 0;
//扩容阈值
private int threshold;
//计算扩容阈值
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}
//自增方法,若大于length则从头自增,用于开放寻址
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}
//自减方法,若小于0,则从length-1开始自减
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

3.2、构造方法

3.2.1、构造方法一

        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            1、初始化数组长度 16
            table = new Entry[INITIAL_CAPACITY];
            2、计算hash值取模存放在数组的位置,散列法
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            3、新建Entry
            table[i] = new Entry(firstKey, firstValue);
            4、设置数组中包含元素的个数为1
            size = 1;
            5、设置初始阈值  16*2/3
            setThreshold(INITIAL_CAPACITY);
        }

3.2.2、构造方法二

父线程Thread中的inheritableThreadLocals成员,赋值到子线程的inheritableThreadLocals

private ThreadLocalMap(ThreadLocalMap parentMap) {
    	   1、其实就是转移值,不做解析
            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++;
                    }
                }
            }
        }

3.3、getEntry方法解析

private Entry getEntry(ThreadLocal<?> key) {
    1、根据当前key取模,计算下标位置
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        2、没找到key说明再存入的时候可能发生了hash冲突
            //1.e不为null,但e的key与传入key不同
            //2.e为null,调用下面函数将直接返回null。
        return getEntryAfterMiss(key, i, e);
}

注意注释我写了,如果e(取模下标的entry)为null,调用下面getEntryAfterMiss函数将直接返回null。为啥这么自信呢,哈希值取模下标的entry没找到,就认为目标key肯定不在map里了。显然,ThreadLocalMap的函数逻辑是保证了这样一点:多个哈希值不同但取模下标相同的ThreadLocal,在你操作完毕后,或者说操作开始前,它们肯定从这个取模下标开始放置的(后面的由于冲突,会依次放在后面索引),在移除的时候后面的值会重新根据hash取模进行计算下标位置,所以说该下标位置不可能为null。

名词解释:

  • 数组元素为null,就是null node

  • 数组元素不为null,但是key为null,为stale entry

  • 循环通常从一个索引向后搜索,直到遇到第一个null entry(一般是指循环处理前,就是null的entry),结束循环。将这个索引直到第一个null entry称为连续段。

    重点

    取模之后的连续段若找不到key,则在其他位置一定也找不到该key,

3.3、getEntryAfterMiss方法解析

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
		 	1、循环遍历,向后搜索连续段,之前set的时候也是set到第一个不为null的值,
                所以只需要遍历到第一个为null的位置即可
            while (e != null) {
                ThreadLocal<?> k = e.get();
                2、key相等,直接返回value
                if (k == key)
                    return e;
                3、key为null,说明该Entry是 stale node,需要清除
                if (k == null)
                    expungeStaleEntry(i);
                else
                4、循环自增,前面解释过
                    i = nextIndex(i, len);
                5、获取下一个i下标的Entry
                e = tab[i];
            }
            return null;
        }

3.3、expungeStaleEntry方法解析

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
			1、当前位置直接设置的value和Entry直接设置为null,size--
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            Entry e;
            int i;
    		2、遍历连续段,直到下一个为null的位置
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                2.1、key为null,说明该位置还是stale node 同上1位置处理
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    3、key不为null,重新计算下标位置,让key节点更靠近取模的位置
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        3.1、h!=i,说明node下标不在取模的位置上, tab[i]直接设置为null
                        tab[i] = null;
                        3.2、从h出发找到后面下一个为null的位置
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
    		4、返回连续段后为null位置的下标即 tab[i] == null
            return i;
        }

expungeStaleEntry函数,看名字就知道是用来清理stale entry的。而且get、set、remove、resize都可能调用到它,实际上它也确实是用来处理stale entry的主要函数。

  • 循环从来向后搜索连续段,当遇到null entry时(是指循环开始前就为null的entry),停止。

  • 每次循环中,遇到stale entry,清空它。

  • 每次循环中,遇到取模下标不是其实际下标的entry,为其rehash,以使得它移动到更靠近取模下标的位置上去。

3.4、set方法解析

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

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
		1、遍历连续段,直到为null的位置
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
			2、找到相同的key,直接修改value
                if (k == key) {
                    e.value = value;
                    return;
                }
			3、key为null,该节点为 stale节点
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
		4、到这儿说明没有找到key的节点,插入到i的位置
            tab[i] = new Entry(key, value);
            int sz = ++size;
			//cleanSomeSlots返回真说明有stale entry被清空了,size肯定减小了;
            //只有当 cleanSomeSlots返回假 且到达阈值时,才肯定需要rehash
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

replaceStaleEntry函数中,无论怎样,都会把传入的key和value塞到staleSlot所在的下标上去,只不过有两种情况都会达到这个目标,具体看注释。另外,该函数会把staleSlot参数所在的run里的所有stale entry都给清理掉

3.5、replaceStaleEntry方法解析

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
			1、从该位置进行删除statle node节点
            int slotToExpunge = staleSlot;
    		2、向前遍历连续段,slotToExpunge为最前面statle节点的下标或自己本身
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;
			3、向后遍历连续段
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
			3.1、找到了相同的key,交换i与staleSlot的元素
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;
					3.2、如果相同,说明连续段前面没有stale
                        若不相同有两种可能:
                        	(1)slotToExpunge为最前面stale的下标
                        	(2)slotToExpunge为staleSlot后面的第一个位置
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    3.3、清除stale节点
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }
				3.4、进入这儿,说明staleSlot前面没有stale节点,
                    则让slotToExpunge为i,且i为staleSlot后面的第一个位置,即staleSlot+10。
                    因为staleSlot一定不为null或stale node,所以使expungeStaleEntry直接忽略该节点
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }
			4、没有找到相同的key,说明key不存在在数组中,直接插入到staleSlot位置
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);
			5、解析同3.2,其实就是expungeStaleEntry方法的起始下标位置
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

主要做了三件事:

  • 把传入的key和value插入到staleSlot下标位置

  • 计算expungeStaleEntry方法的初始下标位置,要么为连续段最前面的stale节点,要么为staleSlot后面第一个节点

  • cleanSomeSlots清除stale节点

3.6、cleanSomeSlots方法解析

        private boolean cleanSomeSlots(int i, int n) {
            1、设置清空标志
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            2、循环清除,最少循环log2(n)do {
                2.1、后面的下标位置i
                i = nextIndex(i, len);
                Entry e = tab[i];
                2.2、如果e为stale node,则进行清除
                if (e != null && e.get() == null) {
                  2.3、重新赋值n,如果循环了log2(n)都没有进入到该位置,也就结束循环了,所以最少循环log2(n)次
                    n = len;
                    removed = true;
                    2.4、清除stale,返回的i使为null的下标,所以i可能会跳跃
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0); 3、无符号右移,若不进入上述判断执行log2(n)4、返回清除标志
            return removed;
        }
  • 执行循环,如果循环中一次都没有发现stale entry,那么只会循环log2(n)次,因为n一直在无符号右移。

  • 如果循环中发现了一次stale entry,那么不管之前执行了多少次循环,之后也至少执行log2(n)次循环(n会更新为容量)。由于expungeStaleEntry的执行,i 可能会跳跃到 i 之后连续段的第一个null entry,然后下一次循环i 再移动

3.7、remove方法解析

        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) {
                    1、找到key,先清除key,将节点变成stalenode
                    e.clear();
                    2、然后执行expungeStaleEntry
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值