【线程】ThreadLocal 剖析 (十四)

我的原则:先会用再说,内部慢慢来。
学以致用,根据场景学源码


一、概念

ThreadLocal的作用是提供线程内的局部变量。这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

二、整体架构

2.1 代码架构
2.1.1 Thread.class
public class Thread implements Runnable {
	...
    ThreadLocal.ThreadLocalMap threadLocals = null;

    // 这个下次讲解
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
	...
}

于是,我们可以知道,每个thread的内部,都有一个 threadLocals变量,这是一个 Map,里面就存储着KV对。

2.1.2 ThreadLocal.class
public class ThreadLocal<T> {
		//注意 ThreadLocal 只有这么一个变量!!
	private final int threadLocalHashCode = nextHashCode();  
	// 服务于上面的 threadLocalHashCode
	private static AtomicInteger nextHashCode = new AtomicInteger(); 
	private static int nextHashCode() { //底层是 CAS +1获取hashCode
	    return nextHashCode.getAndAdd(HASH_INCREMENT);
	}

	static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
		@Override
        protected T initialValue() {
            return supplier.get();
        }
	}
	static class ThreadLocalMap {
		static class Entry extends WeakReference<ThreadLocal<?>> {

		}
		private void set(ThreadLocal<?> key, Object value) {}
		private void remove(ThreadLocal<?> key) {}
		private Entry getEntry(ThreadLocal<?> key) {}
	}
	
	//第一次#get的时候,调用 #setInitialValue,再调用 #initialValue
	protected T initialValue() {return null;}
    public void set(T value) {}
    public T get() {}
    private T setInitialValue() {}
    public void remove() {}
	public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {}
	...
}
2.2 UML
  • ThreadLocal

  • ThreadLocal.ThreadLocalMap

  • ThreadLocal.ThreadLocalMap.Entry
    在这里插入图片描述

  • 总结一下:

  1. 每一个thread对象内部有 1 个 ThreadLocal.ThreadLocalMap,
  2. ThreadLocal.ThreadLocalMap 内部放了个 Entry[] 数组,
  3. 每个 Entry 对象存储了一个key和一个value。
  4. 每个 key 都是一个 threadLocal 对象 (balance和cost属于两个 threadLocal对象,对应了2个value,threadLocal对象其实就是一个线程内变量)
  5. 对象 ThreadLocal1 ,ThreadLocal2 ,ThreadLocal3 类似与托管机构。Thread相当与人。
2.3 对象关系图

在这里插入图片描述

=== 点击查看top目录 ===

三、代码 Demo

举个例子:每个人都有一个账户,每次买东西都会进行扣费,每个人的账户存款都不一样。一个人就是一条线程,账户存款就是线程内的局部变量。(如何设置成全局变量,那么就是一个钱包全部人花,这不符合场景。

//每个人都有一个账户,每次买东西都会进行扣费,每个人花的是自己的钱,每个人的账户存款都不一样。一个人就是一条线程,账户存款就是线程内的局部变量。
public class _22_TestThreadLocal {
    public static void main(String[] args) {
        wallet wallet = new wallet();
        // 3.  3个线程共享wallet,各自消费
        new Thread(new TaskDemo(wallet), "A").start();
        new Thread(new TaskDemo(wallet), "B").start();
        new Thread(new TaskDemo(wallet), "C").start();
    }

    private static class TaskDemo implements Runnable { //一个人就像一个线程
        private wallet wallet;

        public TaskDemo(wallet wallet) {
            this.wallet = wallet;
        }

        public void run() {
            try{
                for (int i = 0; i < 3; i++) {
                    wallet.spendMoney();

                    // 4. 每个线程打出3个
                    System.out.println(Thread.currentThread().getName() + " --> balance["
                            + wallet.getBalance() + "] ," + "cost [" + wallet.getCost() + "]");
                }
            }finally {
                wallet.removeAll();
            }

        }
    }

    private static class wallet { //一个人就像一个线程
        // 1.通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
        private static ThreadLocal<Integer> balance = ThreadLocal.withInitial(new Supplier<Integer>() {
            @Override
            public Integer get() {
                return 100;
            }
        }); // 假设初始账户有100块钱

        private static ThreadLocal<Integer> cost = ThreadLocal.withInitial(() -> 0); // 假设初始账户有100块钱
//        private static ThreadLocal<Integer> balance = new ThreadLocal<>();
//        private static ThreadLocal<Integer> cost = new ThreadLocal<>();

        public int getBalance() {
            return balance.get();
        }

        public int getCost() {
            return cost.get();
        }

        // 2。 消费
        public void spendMoney() {
            int balanceNow = balance.get();
            int costNow = cost.get();
            balance.set(balanceNow - 10); // 每次花10块钱
            cost.set(costNow + 10);
        }


        public void removeBalance(){
            balance.remove();
            balance = null;
        }

        public void removeCost(){
            cost.remove();
            cost = null;
        }

        public void removeAll(){
            removeBalance();
            removeCost();
        }
    }
}

输出:

A --> balance[90] ,cost [10]
B --> balance[90] ,cost [10]
A --> balance[80] ,cost [20]
C --> balance[90] ,cost [10]
B --> balance[80] ,cost [20]
C --> balance[80] ,cost [20]
A --> balance[70] ,cost [30]
C --> balance[70] ,cost [30]
B --> balance[70] ,cost [30]

结论:
线程 A,B,C 内部的变量balance ,cost 互不干扰。

=== 点击查看top目录 ===

四、源码分析

threadLocal.class 结构

4.1 withInitial() 方法,初始化
  • 看代码初始化
    传入 Supplier,实例化ThreadLocal对象,用的是ThreadLocal的子类SuppliedThreadLocal。
// 1.通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
private static ThreadLocal<Integer> balance = ThreadLocal.withInitial(new Supplier<Integer>() {
    @Override
    public Integer get() {
        return 100;
    }
}); // 假设初始账户有100块钱
  • withInitial 方法
public static <S> ThreadLocal<S> ThreadLocal#withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

=== 点击查看top目录 ===

4.2 get() 方法,获取
  • demo使用:
public void spendMoney() {
    int balanceNow = balance.get();
    int costNow = cost.get();
    balance.set(balanceNow - 10); // 每次花10块钱
    cost.set(costNow + 10);
}
public T get() {
    Thread t = Thread.currentThread();
4.2.1    ThreadLocalMap map = getMap(t); // 获取这个 thread 的 Map
    if (map != null) { //一开始就是null
4.2.2        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue(); //====  初始化这个 ThreadLocalMap,存储在thread内部,然后再返回对应的V
}
4.2.1 getMap() 方法
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals; // 获取这个 thread 的 Map, 一开始是null
    }
4.2.2 getEntry() 方法
//当前 thread 调用 getEntry 方法,输入 ThreadLocal对象,先得到Entry[] tables,再得到数组中的Entry对象(KV)
private Entry ThreadLocal.ThreadLocalMap#getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key) // e==null说明这个位置已经被清除了。 e.get() != key 说明key
        return e;
    else
4.2.3        return getEntryAfterMiss(key, i, e);
}
4.2.3 getEntryAfterMiss() 方法

走到这里,说明 e == null || e.get() != key

  1. e == null ,该 key 已经被垃圾回收了
  2. e.get() != key,e.get() 很有可能是null,说明是脏数据了
java.lang.ThreadLocal.ThreadLocalMap#getEntryAfterMiss

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) // 如果是 脏Entry,清理掉
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

=== 点击查看top目录 ===

4.3 setInitialValue() 方法,设置初始值 (设置并获取value默认值)
  • setInitialValue
private T setInitialValue() {
    T value = initialValue(); //====  初始化数值,如果没使用 withInitial 方法,那么返回 null
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); //记住 Map 是存储在 thread对象里面的,一开始也是 null
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value); //=== 一开始thread对象里面没这个map,是null,走这里
    return value;
}

=== 点击查看top目录 ===

4.4 initialValue() 方法(map中的value默认值)
  • 父类 ThreadLocal
protected T ThreadLocal#initialValue() {
    return null;
}
  • 子类 SuppliedThreadLocal
protected T SuppliedThreadLocal#initialValue() {
    return supplier.get(); //返回生产者的默认值。
}

=== 点击查看top目录 ===

4.5 createMap() 方法 (给thread对象初始化 threadLocals变量)
void ThreadLocal#createMap(Thread t, T firstValue) {
 	//注意这里传入的是this,也就是最外面的 balance 对象(ThreadLocal的实例化对象)
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

注意 new ThreadLocalMap(this, firstValue),这里传入的是this,也就是最外面的 balance 对象(ThreadLocal的实例化对象)
在这里插入图片描述
=== 点击查看top目录 ===

4.5.1 看下 ThreadLocalMap 的构造方法
java.lang.ThreadLocal.ThreadLocalMap#ThreadLocalMap(java.lang.ThreadLocal<?>, java.lang.Object)

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { 
	// 初始化 ThreadLocal.ThreadLocalMap#table,这是一个数组
    table = new Entry[INITIAL_CAPACITY]; 
     // & 操作,这个实际上是一个取余数操作 。
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 记住,一开始不是往0丢,而是由余数确定。之后,挨个往后放
    table[i] = new Entry(firstKey, firstValue); 
    size = 1; //初始化的时候,长度是1
    setThreshold(INITIAL_CAPACITY); // 设置阈值,这个扩容的时候会用到
}
4.5.1 看下变量 threadLocalHashCode
public class ThreadLocal<T> {
	//注意 ThreadLocal 只有这么一个变量!!
	private final int threadLocalHashCode = nextHashCode();  
	// 服务于上面的 threadLocalHashCode
	private static AtomicInteger nextHashCode = new AtomicInteger(); 
	private static int nextHashCode() { //底层是 CAS +1获取hashCode
	    return nextHashCode.getAndAdd(HASH_INCREMENT);
	}
	...
}

=== 点击查看top目录 ===

4.6 set 方法

=== 总体Set流程图 ===

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

  • ThreadLocal.ThreadLocalMap#set
private void ThreadLocal.ThreadLocalMap#set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table; 
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1); // 取余数

	// 这个for是向右进行扫描,要么替换value,要么去掉脏数据如果满足条件,那么return
    for (Entry e = tab[i];
         e != null; //如果被占用了,那么才进入这个 for 
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) { // 1. 如果 threadlocal 对象一样,那么就重置 value ,返回
            e.value = value;
            return; //直接结束方法
        }

        if (k == null) { // 2. 如果 threadlocal 是 null,那么找到脏数据了 
4.6.1            replaceStaleEntry(key, value, i);
            return;//直接结束方法
        }
    }

    tab[i] = new Entry(key, value);  // 正常是走到这里,设置kv
    int sz = ++size; 
    /*
    	cleanSomeSlots 清楚掉key是null,也就是 threadlocal是null的情况。有这情况返回 true
    	cleanSomeSlots 返回 false,并且 目前的长度+1 超过阈值 threshold了,那么进行 rehash
    	注意: cleanSomeSlots 传入的参数是 sz,也就是table数组内的真实存放Entry长度。 size <= len
     */
4.6.2    if (!cleanSomeSlots(i, sz) && sz >= threshold) 
4.6.3        rehash();
}

=== 点击查看top目录 ===

4.6.1 replaceStaleEntry()方法

// 进入这个方法的前提是:该下标 staleSlot的 key == null,是个脏数据,需要处理
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    //第一个脏entry
    int slotToExpunge = staleSlot;

    // 1. 这个for的目的,就是找到最前面的脏Entry的下标
1.   for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null; 
         i = prevIndex(i, len))
        if (e.get() == null) //如果 key 是 null,那么就是脏数据
            slotToExpunge = i;  //脏的下标就是你啦,不断往前推进

    // Find either the key or trailing null slot of run, whichever
    // occurs first
2.   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.
2.1    if (k == key) {

        	//如果在向后环形查找过程中发现key相同的entry就覆盖,并且和脏entry进行交换
            e.value = value;

2.2            tab[i] = tab[staleSlot];
2.3            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
2.4            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            //搜索脏entry并进行清理
2.5            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.
2.6        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
3.    tab[staleSlot].value = null;
4.    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
5.    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

这个 replaceStaleEntry 方法比较复杂,先分成两种情况。后期节点中,有没有出现 k == key(2.1) 的情况,

  1. 无 k == key 的情况:

    情形A1:假如前无脏数据,后无脏数据。
    情形A2:假如前有脏数据,后无脏数据。
    情形A3:假如前无脏数据,后有脏数据。
    情形A4:假如前有脏数据,后有脏数据。

  2. 假如出现了 k == key 的情况:

    情形B1:假如前无脏数据,后无脏数据。
    情形B2:假如前有脏数据,后无脏数据。
    情形B3:假如前无脏数据,后有脏数据。
    情形B4:假如前有脏数据,后有脏数据。

4.6.1.1 无 k == key 的情况
  • 情形A1:假如前无脏数据,后无脏数据。
	跳过1,
	那么直接走代码 3,4 覆盖 staleSlot 下标的 Entry
	不用进行cleanSomeSlots!!!
  • 情形A2:假如前有脏数据,后无脏数据。
走代码1 slotToExpunge  往前移
那么走完34之后,会走5,进行 cleanSomeSlots

在这里插入图片描述

  • 情形A3:假如前无脏数据,后有脏数据。
跳过1,
那么走代码2,slotToExpunge 往后移
那么走完34之后,会走5,进行 cleanSomeSlots
(2.6 条件满足)

在这里插入图片描述

  • 情形A4:假如前有脏数据,后有脏数据。
走代码1 ,slotToExpunge  往前移
走代码2的时候,由于两个 if 都不满足,
那么走完34之后,会走5,进行 cleanSomeSlots.
(2.6 条件不满足)

==== 情况同A2 ===

4.6.1.2 有 k == key 的情况
  • 情形B1:假如前无脏数据,后无脏数据。
跳过1,
走2.1 ,把 entry 覆盖,交换 entry 引用,脏Entry放到了 i 处(2.2 处)。 脏Entry那个地方指向了tab[i] 的Entry(2.3 处),
2.4条件满足,slotToExpunge 往后推到i 的位置(前面都是不脏的而且 key 不重复的),然后执行 cleanSomeSlots 。
return
(2.4,2.6 条件满足)

在这里插入图片描述

  • 情形B2:假如前有脏数据,后无脏数据。
走代码1 ,slotToExpunge  往前移
走代码2的时候,由于两个 if 都不满足,
那么走完34之后,会走5,进行 cleanSomeSlots.
(2.6 条件不满足)

在这里插入图片描述

  • 情形B3:假如前无脏数据,后有脏数据。
走代码1 ,slotToExpunge  往前移
走代码2的时候,由于两个 if 都不满足,
那么走完34之后,会走5,进行 cleanSomeSlots.
(2.6 条件不满足)

在这里插入图片描述

  • 情形B4:假如前有脏数据,后有脏数据。
走代码1 ,slotToExpunge  往前移
走代码2的时候,由于两个 if 都不满足,
那么走完34之后,会走5,进行 cleanSomeSlots.
(2.6 条件不满足)

=== 清理情形同 B2 ===

4.6.2 cleanSomeSlots() 方法
private boolean ThreadLocal.ThreadLocalMap#cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len); //为什么是nextIndex不是i,因为i刚刚不才插入了一个Entry吗,所以不可能是脏数据
        Entry e = tab[i];
        if (e != null && e.get() == null) { // 1. 首先Entry不是null,其次该Entry的key的null,那么就定义成“脏数据”,进入expungeStaleEntry回收处理
            n = len; //2.重置N,重新再扫描一遍
            removed = true;
            i = expungeStaleEntry(i); // 3.清理脏数据,清除完,返回 Entry==null的下标。
        }
    } while ((n >>>= 1) != 0); //4. >>> 右移动1位,初始状态是 16 (二进制:1000)
    return removed;
}
  1. 如果一切顺利的情况下,不会进入上面的1里面的 if,也就是没有脏数据要处理。那么 n >>>= 1 每次右移动1位,16(二进制1000)移动4次就退出了。 log2(n)
  2. 如果中途遇到脏数据,那么N重置为 tables 的长度,那么再循环 log2(n) 遍。
  3. 关注一下cleanSomeSlots变量N,承接一下上下文。set方法中cleanSomeSlots(i, sz) 传入的参数是 sz,也就是table数组内的真实存放Entry长度。 size <= len. 如果在 cleanSomeSlots 的过程中遇到脏数据,那么 n 变大成 len。那么搜索范围log2(n)也增大

在这里插入图片描述

4.6.3 rehash 方法
private void ThreadLocal.ThreadLocalMap#rehash() {

    expungeStaleEntries();// 清除不干净的Entity,也就是 key == null的 Entity对象

    if (size >= threshold - threshold / 4) //table数组长度大于四分之三的时候就扩容
        resize();
}

=== 点击查看top目录 ===

4.6.4 expungeStaleEntries 方法
// 清理一堆脏数据 (key == null)
 private void ThreadLocal.ThreadLocalMap#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);
    }
}

=== 点击查看top目录 ===

4.6.5 expungeStaleEntry 方法
// 清理单个脏数据 (key == null)
private int ThreadLocal.ThreadLocalMap#expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    // 2. 抹掉下标是 staleSlot 的Entry
    tab[staleSlot].value = null; 
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    //2.往后环形继续查找,直到遇到 table[i]==null时结束
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
     	//3. 如果在向后搜索过程中再次遇到脏entry,同样将其清理掉 ( k == null 说明是脏的entity)
        if (k == null) { 
            e.value = null;
            tab[i] = null;
            size--;
        } else {
        	//处理rehash的情况
            int h = k.threadLocalHashCode & (len - 1);  //取余,len肯定是2的N次方
            if (h != i) { // h!=i ,意思就是目前存储的有问题,i 必须等于 h 才对
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

也就是说该 expungeStaleEntry 方法,清理掉当前脏entry后,并没有闲下来继续向后搜索,若再次遇到脏entry继续将其清理,直到哈希(table[i])为null时退出。table[i] 为null,说明这个位置还没被占用呢

=== 点击查看top目录 ===

4.7 remove 方法
4.7.1 ThreadLocal#remove 方法
public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

将某个线程从 ThreadLocalMap 中去除,底层是调用 ThreadLocal.ThreadLocalMap#remove 方法

4.7.2 ThreadLocal.ThreadLocalMap#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) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
4.7.3 Reference#clear 方法
  • java.lang.ref.Reference#clear
    public void clear() {
        this.referent = null;
    }

指针指向null,让GC好回收刚刚指向的对象。

=== 点击查看top目录 ===

五、番外篇

下一章节:【线程】ThreadLocal 内存泄漏问题(十五)
上一章节:【线程】CountDownLatch 内部原理(十三)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值
>