ThreadLocal源码解析

1. 简介

ThreadLocal用于提供线程独有的变量,只有当前自身线程可以访问,而其他线程不能访问,极大的方便了一些逻辑的实现。

ThreadLocal(不同的线程有不同的数据,不存在并发问题)和Synchronized都可以解决并发问题,但是ThreadLocal是用于数据隔离的,而Synchronized是用于数据共享的。

2. 基本使用

package ThreadLocal;

/**
 * 让不同线程在设置完之后再输出,保证从输出开始之后都不会有设置操作
 * 如果不同的线程输出的结果不同,则说明ThreadLocal确实实现了线程
 * 间的数据隔离
 */
public class Demo01 {
    static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    // 获取当前线程的值
    public static void getThread(){
        System.out.println(threadLocal.get());
        threadLocal.remove();
    }
    // 设置不同线程的值
    public static void setThread(){
        threadLocal.set(Thread.currentThread().getName());
    }

    public static void main(String[] args) {

        new Thread(() -> {
            Demo01.setThread();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Demo01.getThread();
        }, "A").start();

        new Thread(() -> {
            Demo01.setThread();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Demo01.getThread();
        }, "B").start();
    }
}

结果:

在这里插入图片描述

从上面的结果确实可以看出ThreadLocal实现了线程之间数据的隔离。接下来我们就来说说ThreadLocal是怎样实现的吧!

3. ThreadLocal原理说明

每个Thread内部都有一个Map(也就是下面要讲的ThreadLocalMap),每当调用ThreadLocal对象的get()方法时,相当于往ThreadThreadLocalMap里面传入了一个键值对(key-value),key就是ThreadLocal对象,value就是我们要存储在线程中的

4. ThreadLocal源码解析

4.1 成员变量

/*
   threadLocalHashCode用于线程的ThreadLocalMap对象的桶位寻址,
   在第一次在某个ThreadLocal对象上调用get()方法时,会给当前线程分配一个
   value,当前ThreadLocal对象会和这个value包装成一个Entry,存储在Thread对象的
   ThreadLocalMap(Map)对象中,threadLocalHashCode就是ThreadLocal对象的
   哈希值,在寻址时通过threadLocalHashCode&(table.length - 1)方法寻址。
   注:table就是ThreadLocalMap底层的Entry数组
*/

// 这个是ThreadLocal对象的哈希值,通过调用nextHashCode()方法获取
private final int threadLocalHashCode = nextHashCode();

// 存储哈希值的变量,属性是AtomicInteger原子类,是一个静态变量
private static AtomicInteger nextHashCode = new AtomicInteger();

// 每一次nextHashCode增加的数值大小,表示哈希值的增量
// 每创建一个ThreadLocal对象,nextHashCode就会增长HASH_INCREMENT
// HASH_INCREMENT是一个黄金分割数,哈希增量为这个值
// 可以使Map中的数分布均匀
private static final int HASH_INCREMENT = 0x61c88647;

// 获取哈希值
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

// 初始化一个起始value,,一般这个方法都是会重写的
protected T initialValue() {
        return null;
    }

4.2 构造方法

// 只有一个空参构造方法
public ThreadLocal() {
}

4.3 成员方法

4.3.1 get()及相关的方法

获取当前线程内部ThreadLocalMap中key为ThreadLocal对象的value

public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程内部的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 如果当前线程已经初始化了自己的ThreadLocalMap对象,就尝试在里面寻找值
    if (map != null) {
        // 获取ThreadLocalMap中key为ThreadLocal对象的键值对
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果键值对不等于null,直接返回值
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 执行到这里有几种情况
    // 1. 当前线程没有初始化过自己的ThreadLocalMap对象
    // 2. 当前线程初始化了自己的ThreadLocalMap对象,但是没有生成当前线程与ThreadLocal相关联的变量
    // setInitialValue() 会先检查当前Thread对象是否初始化了自己的ThreadLocalMap,如果没有初始化
    // 就调用createMap(t, value)初始化并在createMap方法内部添加线程与ThreadLoca相关联的值
    // 如果已经初始化,就调用ThreadLocalMap对象的set方法设置值
    return setInitialValue();
}

private T setInitialValue() {
    	// 获取当前初始化的值
        T value = initialValue();
    	// 获取当前线程
        Thread t = Thread.currentThread();
    	// 获取当前线程的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
    	// 如果已经初始化,就调用ThreadLocalMap的set方法往里面设置值
        if (map != null)
            map.set(this, value);
        else
        // 如果没有初始化,就调用createMap(t, value)方法在里面抵用ThreadLocalMap的
    	// 构造方法(构造方法会初始化并设置值)
            createMap(t, value);
    	// 返回值
        return value;
    }

// 获取线程的ThreadLocalMap对象,也就是threadLocals属性
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
// 调用ThreadLocalMap的构造方法创建并在里面设置一个值
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

4.3.2 set(T value)

设置线程与ThreadLocal对象相关联的值,对于同一个ThreadLocal对象和同一个Thread来说,如果多次调用,会覆盖掉以前的值,保留最后一次的值

public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocal对象
    ThreadLocalMap map = getMap(t);
    // 如果当前线程的ThreadLocalMap对象不为空,就调用ThreadLocal对象的set方法设置一个值
    if (map != null)
        map.set(this, value);
    else
    // 如果当前线程的ThreadLocalMap对象为空,就调用createMap(t, value)方法(createMap对象内部会初始化ThreadLocalMap
    // 并设置一个值)
        createMap(t, value);
}

4.3.3 remove()方法

移除当前线程与当前ThreadLocal对象相关联的值

public void remove() {
    // 获取当前线程的ThreadLocalMap对象
    ThreadLocalMap m = getMap(Thread.currentThread());
    // 如果ThreadLocalMap对象不为空,调用ThreadLocalMap对象的remove方法移除
    if (m != null)
        m.remove(this);
}

ThreadLocal的方法很少,最主要的是ThreadLocalMap内部的方法,ThreadLocalMapThreadLocal的内部类。

5. ThreadLocalMap源码解析

ThreadLocalMapThreadLocal的内部类,每一个Thread内部都会有一个ThreadLocalMap对象,ThreadLocalMap对象可以看成一个Map,但是会与我们平常使用的Map有一些区别,ThreadLocalMap内部采用线性探测法处理哈希冲突,这些我们下面都会讲到。

5.1 成员变量

// ThreadLocalMap内部是一个Entry数组,INITIAL_CAPACITY就是这个Entry数组的容量大小
private static final int INITIAL_CAPACITY = 16;

// table就是ThreadLocalMap内部的Entry数组
private Entry[] table;

// 当前数组占用情况
private int size = 0;

// 扩容阈值,初始值为 len * 2 / 3
// 触发后调用rehash()方法,rehash()方法
// 先做一次全面检查过期数据,把数组中所有过期的Entry移除,
// 如果移除之后,数组中的Entry个数仍然达到threshold的3/4就扩容
private int threshold; // Default to 0

// 将扩容阈值设置为当前Entry数组长度的2/3
private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
/*
注:ThreadLocalMap虽然可以看成一个Map,但是其内部并不是采用拉链法来解决哈希冲突的,而是采用
   线性探测法来解决哈希冲突的,每当遇到哈希冲突,往后移一位,直到没有出现哈希冲突,然后将键值对
   存储进去
*/

5.2 内部类

内部类Entry继承了弱引用,弱引用的对象下一次垃圾回收时一定会把回收掉。

key采用的是弱引用,value采用的是强引用。

Entrykey为什么设置为弱引用呢?

  • 如果key强引用,当ThreadLocal对象被回收时,ThreadLocalMap中的Entry如果没有被手动回收,会造成内存泄漏问题。
  • 但是如果key为弱引用,当ThreadLocal对象被回收时,ThreadLocalMap中的Entry可以被自动回收。还有一点当ThreadLocalMap的一些key被回收后,ThreadLocalMap可以区分哪些是过期的,那些不是过期的。

Entryvalue为什么设置为强引用呢?

Entryvalue设置为强引用的原因是如果采用弱引用假如往ThreadLocalMap里面存了一个value,GC过后value便消失了,也就达不到存储全局的效果了。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

5.3 构造方法

线程的ThreadLocalMap是延迟初始化的,只有当线程第一次存储key-value键值对时才会进行初始化。

// 传入一个键值对,键为ThreadLocal对象,值为当前线程与当前ThreadLocal对象相关联的值
// firstKey:ThreadLocal对象
// firstValue:当前线程与当前ThreadLocal对象相关联的值
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 创建一个容量为初始容量INITIAL_CAPACITY(16)的Entry数组
    table = new Entry[INITIAL_CAPACITY];
    // 获取键值对应该存储在Entry数组中的位置
    // 通过ThreadLocal对象的哈希值(也就是ThreadLocal对象的threadLocalHashCode实现)
    // &
    // Entry数组的长度-1(也就是INITIAL_CAPACITY - 1)求取
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    // 设置存储的元素个数为1
    size = 1;
    // 设置扩容阈值,将当前Entry数组的长度传进去,在setThreshold方法内部会将阈值设置为Entry数组长度 * 2 / 3
    setThreshold(INITIAL_CAPACITY);
}
// 传入一个ThreadLocalMap对象
// 内部其实就是创建了一个和传入的ThreadLocalMap内部的Entry数组相同长度的Entry数组
// 并将值一个一个赋值进去
private ThreadLocalMap(ThreadLocalMap parentMap) {
    // 获取传入的ThreadLocalMap对象内部的Entry数组
    Entry[] parentTable = parentMap.table;
    // 获取传入的ThreadLocalMap对象内部的Entry数组长度
    int len = parentTable.length;
    // 设置扩容阈值
    setThreshold(len);
    // 在当前ThreadLocalMap里面new一个相同大小的Entry数组
    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++;
            }
        }
    }
}

5.4 成员方法

5.4.1 nextIndex(int i, int len)

获取Entry数组索引i处的下一个索引位置

// 回环询问,如果i + 1 < len, 就返回len,否则返回0
// 这个方法其实是为了后面处理哈希冲突的线性探测法做准备的
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

5.4.2 prevIndex(int i, int len)

获取Entry数组索引i处的上一个索引位置

private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

5.4.3 getEntry(ThreadLocal<?> key)

由键获取对应的键值对其实就是ThreadLocal对象。

// 参数key:ThreadLocal对象
private Entry getEntry(ThreadLocal<?> key) {
    // 获取该键对应的键值对应该存储在的位置
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 如果对应的Entry不为空,且Entry对象对应的key和传入的key相等就返回这个Entry对象,说明找到了
    if (e != null && e.get() == key)
        return e;
    else
    // 否则调用getEntryAfterMiss(key, i, e)方法
    // 执行到这里有以下几种情况:
    // 1. 对应位置的Entry对象不存在
    // 2. 存在,但是发生哈希冲突,调用了nextIndex获取了新的位置
        return getEntryAfterMiss(key, i, e);
}

5.4.4 getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法

如果调用ThreadLocalMap的getEntry(ThreadLocal<?> key)方法没有找到对应的键值对就会调用这个方法进行后续操作。

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    // 首先得到Entry数组
    Entry[] tab = table;
    // 然后得到Entry数组的长度
    int len = tab.length;
	// 如果对应位置的Entry不为空,就一个一个往后找同时清除过期数据,直到找到或下一个Entry对象为空为止
    while (e != null) {
        ThreadLocal<?> k = e.get();
        // 找到了,直接返回这个Entry对象
        if (k == key)
            return e;
        // 没找到往下执行
        // Entry对象为空,就清除过期数据
        if (k == null)
            expungeStaleEntry(i);
        else
        // Entry不为空,获取下一个Entry对象
            i = nextIndex(i, len);
        e = tab[i];
    }
    // 没找到返回null
    return null;
}

5.4.5 expungeStaleEntry(int staleSlot)方法

探测式清除过期数据,从下标staleSlot开始,直到碰到的Entry数组中的Entry对象为null时结束,并返回结束时的下标。

private int expungeStaleEntry(int staleSlot) {
    // 得到Entry数组
    Entry[] tab = table;
    // 获取Entry数组的长度
    int len = tab.length;

    // 帮助GC
    // 因为是从getEntryAfterMiss方法进来的,在里面已经判断过key为null才进来的
    // 所以这里就不用设置key为null了,因为key一定为null
    tab[staleSlot].value = null;
    // 设置这个Entry对象为null
    tab[staleSlot] = null;
    // Entry数组中存储的键值对数量减1
    size--;

    // 表示当前遍历到的键值对结点
    Entry e;
    // 表示当前遍历到的索引
    int i;
    // 从staleSlot+1开始搜索过期数据,直到碰到的Entry对象为null位置
    for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // 如果当前Entry对象的key为null,说明是过期数据,要清理
        if (k == null) {
            // 当前Entry对象的key为空,说明这个是过期数据,这个Entry对象要回收
            e.value = null;
            // 设置当前Entry对象对应的索引处为null
            tab[i] = null;
            // 设置Entry数组中存储的元素数量减1
            size--;
        }
        // 当前Entry对象的key不为null,说明当前Entry不是过期数据,
        // 因为当前Entry对象存储的位置可能是发生了哈希冲突后往后移而存储的位置,
        // 这个时候就应该尝试去优化位置,让这个位置更靠近本来的位置
        // 这样的话,查询的时候效率才会更高
        else {
            // 获取本来应该存储的位置
            int h = k.threadLocalHashCode & (len - 1);
            // h != i,说明当前Entry对象是发生哈希冲突后往后移之后存储的位置
            // 这个时候就应该去存储到本来的位置
            if (h != i) {
                // 将移动后的位置设为null,再重新找一个更加接近本来位置的地方
                tab[i] = null;
                while (tab[h] != null)
                    h = nextIndex(h, len);
                // 将Entry对象存储到最接近本来位置的地方,也有可能就是本来的位置
                tab[h] = e;
            }
        }
    }
    return i;
}

5.4.6 set(ThreadLocal<?> key, Object value)方法

ThreadLocalMap独享添加键为key值为value的键值对

// 参数key:键
// 参数value:值
private void set(ThreadLocal<?> key, Object value) {
	// 获取Entry数组
    Entry[] tab = table;
    // 获取Entry数组的长度
    int len = tab.length;
    // 获取Entry应该存储的位置
    int i = key.threadLocalHashCode & (len-1);
	// 从应该存储的位置开始寻找
    for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
		// 如果 k == key,说明本来已经存储了一个一样的键,则只更新值,更新之后返回
        if (k == key) {
            e.value = value;
            return;
        }
		// 碰到 k == null,说明碰到过期数据了,执行replaceStaleEntry(key, value, i)方法去替换过期数据
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	// 碰到了一个Entry还没有找到,就将键值对添加到这个位置
    tab[i] = new Entry(key, value);
    // Entry存储的元素数量加1
    int sz = ++size;
    // 做一次启发式清理,如果cleanSomeSlots(i, sz)返回false,说明未清理到任何Entry对象,判断是否达到扩容阈值了
    // 如果达到扩容阈值,就去扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

5.4.7 replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot)方法

调用ThreadLocalMap的set方法往里面添加值时当碰到了过期数据,调用replaceStaleEntry方法替换过期数据

private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {
    // 获取Entry数组
    Entry[] tab = table;
    // 获取Entry数组的长度
    int len = tab.length;
    Entry e;
    // 获取起始位置
    int slotToExpunge = staleSlot;
    // 一直往前找,如果还可以找到过期数据,就继续往前找,直到找到的Entry对象不为空,并将过期数据的索引位置
    // 赋值给slotToExpunge,slotToExpunge其实是为了下面的探测式清除数据准备的
    for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // 从staleSlot往后找,直到碰到null为止
    for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
		// 条件成立,尝试去替换
        if (k == key) {
            e.value = value;
			// 替换逻辑
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
			
            // 条件成立说明向前查找并未找到过期的Entry
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            // 启发式清理
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
		// key == null说明当前Entry为过期数据,并且slotToExpunge == staleSlot说明
        // 向前查找过期数据也没有找到,这时设置slotToExpunge为i表示从这里开始清除数据
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
	// 什么时候执行到这里?
    // 向后查找过程中并未发现 k == key的Entry,说明当前set是一个添加逻辑,按照普通的添加逻辑就好
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
	// 发现了过期数据,要清理过期数据
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

5.4.8 cleanSomeSlots(int i, int 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];
        // e != null 并且 e.key == null,说明当前Entry一定是一个过期数据,就从当前位置开始探测式清理数据
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

5.4.9 rehash()方法

先清理一下过期数据,如果清理之后的元素数量大于阈值的3/4,就执行真正的扩容方法。

private void rehash() {
    // 清除列表里所有过期数据
    expungeStaleEntries();
    // 如果当前数组里的元素数量还是大于阈值的3/4,就执行真正的扩容方式
    if (size >= threshold - threshold / 4)
        resize();
}

5.4.10 resize()方法

真正的扩容方法

private void resize() {
    // 获取旧的Entry数组
    Entry[] oldTab = table;
    // 获取旧的Entry数组的长度
    int oldLen = oldTab.length;
    // 设置扩容后的Entry数组为原来的两倍
    int newLen = oldLen * 2;
    // 初始化一个Entry数组
    Entry[] newTab = new Entry[newLen];
    int count = 0;
	// 接下来就是一个一个填充了,同时碰到过期数据就清理了,过期数据不用往新的Entry数组填
    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;
}

5.4.11 remove(ThreadLocal<?> key)方法

移除ThreadLocalMap中键为key的键值对

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;
        }
    }
}

文章学习于:
史上最全ThreadLocal 详解
ThreadLocal源码分析_02 内核(ThreadLocalMap)
【JDK源码】线程系列之ThreadLocal
深挖ThreadLocal
ThreadLocal原理及内存泄露预防
ThreadLocal原理详解——终于弄明白了ThreadLocal
ThreadLocal使用与原理
史上最全ThreadLocal 详解

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值