ThreadLocal源码解析

ThreadLocal源码解析,第1张

ThreadLocal源码解析

目录

1. 简介2. 基本使用3. ThreadLocal原理说明4. ThreadLocal源码解析

4.1 成员变量4.2 构造方法4.3 成员方法

4.3.1 get()及相关的方法4.3.2 set(T value)4.3.3 remove()方法 5. ThreadLocalMap源码解析

5.1 成员变量5.2 内部类5.3 构造方法5.4 成员方法

5.4.1 nextIndex(int i, int len)5.4.2 prevIndex(int i, int len)5.4.3 getEntry(ThreadLocal key)5.4.4 getEntryAfterMiss(ThreadLocal key, int i, Entry e)方法5.4.5 expungeStaleEntry(int staleSlot)方法5.4.6 set(ThreadLocal key, Object value)方法5.4.7 replaceStaleEntry(ThreadLocal key, Object value,int staleSlot)方法5.4.8 cleanSomeSlots(int i, int n)方法5.4.9 rehash()方法5.4.10 resize()方法5.4.11 remove(ThreadLocal key)方法

1. 简介

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

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

2. 基本使用
package ThreadLocal;


public class Demo01 {
    static ThreadLocal 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();
    }
}

结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CZp95M1H-1642907859817)(结果.png)]

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

3. ThreadLocal原理说明

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

4. ThreadLocal源码解析 4.1 成员变量

// 这个是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内部的方法,ThreadLocalMap是ThreadLocal的内部类。

5. ThreadLocalMap源码解析

ThreadLocalMap是ThreadLocal的内部类,每一个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;
        }

5.2 内部类

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

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

Entry的key为什么设置为弱引用呢?

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

Entry的value为什么设置为强引用呢?

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

static class Entry extends WeakReference> {
    
    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 key = (ThreadLocal) 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 详解

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5712398.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-18

发表评论

登录后才能评论

评论列表(0条)