ThreadLocal之源码剖析

ThreadLocal介绍、示例和原理

ThreadLocal为线程提供了私有变量保存的方式,每一个线程通过ThreadLocal只能访问自己的私有变量。每个线程对私有变量包含这一个隐含的引用,只要线程还存活,那么这些私有的变量就可以访问,当线程销毁,这些私有变量就会被回收,当然这些私有变量在别处是没有引用的。

简单示例

现在我们有一个需求,就是为每一线程创建一个线程ID,而这个ID在线程执行时随时可以访问,那么就可以通过如下进行实现:

/**
 * @Description: 线程id实例
 * @Author: binga
 * @Date: 2020/9/17 17:16
 * @Blog: https://blog.csdn.net/pang5356
 */
public class ThreadId {

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " : " + ThreadId.getThreadId());
                System.out.println(Thread.currentThread().getName() + " : " + ThreadId.getThreadId());

            }
        };

        Thread t1 = new Thread(runnable, "t1");
        Thread t2 = new Thread(runnable, "t2");
        Thread t3 = new Thread(runnable, "t2");

        t1.start();
        t2.start();
        t3.start();
    }

    private static AtomicInteger nextId = new AtomicInteger(0);

    private static ThreadLocal<Integer> threadIds = ThreadLocal.withInitial(() -> {
        return nextId.getAndIncrement();
    });

    public static Integer getThreadId() {
        return threadIds.get();
    }
}

输出打印结果如下:

t1 : 0
t1 : 0
t2 : 1
t2 : 1
t2 : 2
t2 : 2

可以看到每个线程有自己的变量值,及时多次调用都是同一个值,并且调用的是ThreadId的静态方法,那么在任何地方都可以获取到线程Id。一般地,ThreadLocal都作为一个类的静态变量并提供静态的方法方法从而保证了再任何地方都可以访问当前线程的私有变量。当然每一个ThreadLocal只可以存储一个静态变量,多个ThreadLocal实例可以存储多个本地变量。

ThreadLocal的原理

ThreadLocal的原理相对来说比较简单,在Thread实例中包含有一个类型为ThreadLocalMap、名称为threadLocals全局变量:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

其实现就是一个Map,可以将其看成HashMap。而在这个threadLocals的Map中,键就是ThreadLocal实例,而值则是存储的本地变量,ThreadLocal实例通过访问Thread实例的threadLocals全局变量从而实现本地变量的存储和获取。Thread和ThreadLocal的关系如下:
在这里插入图片描述
需要注意的是,这里的key是对ThreadLocal实例的弱引用,这个在后续讨论内存泄露时再详细的讲解。

ThreadLocal源码剖析

在了解了ThreadLocal的使用及原理后,对ThreadLoca有一个大致的了解,接下来来深入的剖析一个ThreadLocal相关的源码来加深对ThreadLocal的理解。这里首先分析ThreadLocalMap的相关源码,因为所有的数据操作都是围绕这个数据结构进行的,在分析完ThreadLocalMap的源码后,紧接着再分析ThreadLocal的源码。

ThreadLocalMap源码分析

在分析ThreadLocalMap源码之前,先介绍一下ThreadLocalHashMap的数据结构实现,其是通过数组实现的,同时确定元素位置也是将key的hashCode对数组长度取余从而确定元素位置的,与HashMap不同的是,ThreadLocalHashMap在处理hash冲突时是使用的开放寻址,这里简单说明一下开放寻址。
假设一个Map,该Map使用数组容纳元素,此时该Map的数组长度为8,如下:
在这里插入图片描述
现在向该Map中存放元素e1,该元素e1的hashCode值为36,那么36%8 = 4,那么元素e1则会放入下标为4的槽位,如下:
在这里插入图片描述
紧接着要将元素e2放入该Map中,而e2的hashCode的值为52,那么52%8 = 4,那么元素e1则也会放入下标为4的槽位,那么使用开放寻址,从小标为4的槽位向后寻找,直至槽位没有放置元素,然后将元素e2放入指定的槽位,如下:
在这里插入图片描述
在了解了ThreadLocalMap的数据结构实现后,接下来通过分析每一个方法的的作用来梳理源码。

ThreadLocalMap中元素Entry的实现

ThreadLocalMap中元素为Entry,其为ThreadLocalMap的静态内部类,如下:

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

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

通过Entry的继承关系可知,Entry继承了WeakReference,所以Entry实例对键ThreadLocal的实例引用是虚引用,但是Entry实例对value的应用则是强引用。

ThreadLocalMap各个全局属性剖析

// ThreadLocalMap创建时初始化容量
private static final int INITIAL_CAPACITY = 16;
// ThreadLocalMap基于数组实现,用于存放元素数组
private Entry[] table;
// 用于记录当前ThreadLocalMap中存放的键值对个数
private int size = 0;
// 用于控制扩容的阀值,一般将其设置为table.length * 2 / 3
private int threshold;

ThreadLocalMap方法剖析

接下来来梳理一下ThreadLocalMap中提供重复功能的一些素有方法。

  1. setThreshold
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

该方法就是将threshold属性设置为参数的 2 / 3,一般是在扩容后调用该方法重新设置扩容阀值。

  1. nextIndex
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

该方法就是用于给定参数槽位,然后返回给定参数槽位的下一个槽位。在ThreadLocalMap中解决hash冲突使用的是开放寻址,所以当指定槽位被占用后需要向后续槽位查找,但是用于存储元素的数组长度是有限的,所以当达到数组长度后,然后下一个槽位则从数组开始计算。

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

与nextIndex相反,返回指定槽位的前一个槽位的下标

  1. resize
    该方法是将table数组扩大一倍,并且对原先的元素进行重新hash计算,并且重新设置threshold和size属性,方法如下:
private void resize() {
    // 构造新的数组为原先table长度的两倍
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;
    
    // 遍历原先的数组,处理原先的每一个元素,进行重hash
    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            // 由于Entry实例对ThreadLocal是弱引用,所以可能存在ThreadLocal已
            // 经被回收的情况,所以对于此种情况,将Entry对于value引用断开以便于
            // GC
            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++;
            }
        }
    }
    // 重新设置扩容阀值threshold和size数量
    setThreshold(newLen);
    size = count;
    table = newTab;
}
  1. expungeStaleEntry
    顾名思义,该方法是丢弃旧的Entry元素,所谓旧的是指key为空的Entry实例,该方法参数为旧的Entry所在槽位下标,而其返回值则是基于参数staleSlot后第一个槽位为空的下标。该方法如下:
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 首先将参数staleSlot的Entry丢弃并清空槽位
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // 接下来的逻辑就是从staleSlot的下一个槽位开始对每一个非空槽位的Entry重新
    // hash,知道遇到一个空的槽位,然后将该空的槽位下标返回
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // 遇到旧的Entry,就将Entry清空
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 针对那些hash后对应槽位不对的进行移动
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

该方法的过程可以大致如下,首先如图所示,参数staleSlot及后面的槽位情况如下(红色代表为旧Entry,绿色代表正常Entry,白色则槽位为空):
在这里插入图片描述
首先将staleSlot置为空:
在这里插入图片描述
然后从下标为staleSlot+1槽位开始向后遍历,假设staleSlot正常且hash后位置不变,则继续向后遍历,如下:
在这里插入图片描述
在下标为staleSlot+2的槽位处其Entry为无效的,则清空,继续向后遍历:
在这里插入图片描述
在遍历到下标为staleSlot+3的槽位后,该Entry有效,但是重新hash计算后位置为staleSlot+1,但是该位置不为空,则开放寻址向后遍历,放置到staleSlot+2处,完后继续向后遍历:
在这里插入图片描述
在下标为staleSlot+4的槽位为空则停止遍历将stale+4这个位置返回。

  1. expungeStaleEntries
    该方法就是清除Map的所有旧的Entry,其方法如下:
private void 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);
    }
}

可以看到该方法遍历整个table,然后判断遍历到的每一个元素是否是是旧的,如果是则调用expungeStaleEntry方法进行清除,然后再遍历下一个。(个人理解该方法存在重复遍历,因为expungeStaleEntry方法可能会从当前j位置向后重新hash元素并清理,结束后仍然从j+1位置继续遍历判断,可能存在重复遍历)。

  1. rehash
private void rehash() {
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

该方法就是先是清除所有的旧的Entry,然后判断size是否大于threshold的3 / 4,这里为什么不是直接使用threshold判断呢?这是因为在判断前清除了旧的Entry,所以算上之前旧的Entry已经是大于threshold了,所以这里保守起见使用了3 / 4。满足的话就调用resize进行扩容。

  1. cleanSomeSlots
    通过该方法名称可知为清理一些槽位,先来看该方法:
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        // 从参数i的下一个槽位开始遍历
        i = nextIndex(i, len);
        Entry e = tab[i];
        // 当遇到旧的Entry则通过expungeStaleEntry方法遍历清除一些,然后从
        // expungeStaleEntry返回的槽位继续比遍历
        if (e != null && e.get() == null) {
            // 当存在旧的Entry则将n置为len,从而可以保证进行
            // 更多次的遍历
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    // 遍历的条件就是n一直除2直到为0
    } while ( (n >>>= 1) != 0);
    return removed;
}

cleanSomeSlots的方法还是比较巧妙的,该方法是在不清理和全部清理中取了一个折中的选择,如果全清理则时间复杂度为O(n),但是cleanSomeSlots方法则是根据遇见的旧的Entry情况从而动态的变化其清除的次数,所以该方法下来平均时间复杂度为Log(n),最坏就是全部扫描一遍。当然返回是一个boolean类型,标识是否发生清除。

  1. 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;
        // 遇到旧的Entry则调用expunge方法进行清除
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

该方法是在查找Entry时,通过key的hashCode进行hash计算后指定的槽位不是与key相等的Entry,则从计算的槽位向后遍历查找指定key的Entry,当然在该过程中具有清除旧的Entry的操作。

  1. getEntry
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

通过参数key的hashCode进行hash计算确定其所在槽位下标,指定槽位Entry的key与参数key相等则返回该Entry,不相等则通过getEntryAfterMiss方法进行查找。

  1. replaceStaleEntry
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
    // 1
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // 2
    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;

            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
    
    // 3
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

如上述代码,我这里将其分成三部分,这里将每一部分单独来梳理,最后进行一个总结,首先来看第一部分代码如下:

int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
     (e = tab[i]) != null;
     i = prevIndex(i, len))
    if (e.get() == null)
        slotToExpunge = i;

该方法就是从staleSlot向前遍历,知道遍历到槽位为空的位置,在遍历过程中将最新的旧的槽位下标记录,赋值给slotToExpubge。
在这里插入图片描述
如上图所示从staleSlot遍历,当结束后则staleToExpunge为下标为staleSlot-2的槽位。
接下来来看第二部分代码:

for (int i = nextIndex(staleSlot, len);
     (e = tab[i]) != null;
     i = nextIndex(i, len)) {
    ThreadLocal<?> k = e.get();
    
    // 判断当前Entry的key与参数相同,则将当前Entry的value设置为参数value
    // 然后将当前槽位的Entry与下标为staleSlot槽位互换
    if (k == key) {
        e.value = value;
        
        // 将槽位互换
        tab[i] = tab[staleSlot];
        tab[staleSlot] = e;
        
        // 如果条件成立则代表第一部分向前遍历时没有遇到旧的Entry,而且上面的互换
        // 槽位操作将原先的staleSlot的旧的Entry向后移动了,所以包含旧的Entry
        // 位置就变为当前的i了,所以将i赋值给slotToExpunge
        if (slotToExpunge == staleSlot)
            slotToExpunge = i;
        // 首先通过expungeStaleEntry方法遍历清除一下,返回清除到为空的槽位下标
        // 然后再次通过cleanSomeSlots方法进行清除
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        return;
    }
    
    // 如果遇到就旧的槽位,并且在第一部分没有扫描到旧的槽位则将当前位置赋值给
    //     slotToExpunge用于清理时的标记
    if (k == null && slotToExpunge == staleSlot)
        slotToExpunge = i;
}

接下来来看第三部分代码,如下:

// 在第二部分没有找到与参数key相同的Entry,代表这是一个新key,不是原先放
// 放入的,那么就将新key方到staleSlot这个槽位中
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 扫描到旧的Entry则清除
if (slotToExpunge != staleSlot)
    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

综合上面的三部分总结如下:

  • 通过staleSlot向前扫描,有旧Entry则使用slotToExpungge记录。
  • 从staleSlot向后遍历,查找是否存在的key,存在则复制并将存在的Entry与staleSlot槽位的Entry互换位置,然后清除旧的Entry。
  • 在遍历后没有找到指定key的Entry,则代表这个key是新的key,则将其放置到下标为staleSlot槽位,然后判断是否有旧的Enrty,有则清除旧的Entry。
  1. set
private void set(ThreadLocal<?> key, Object value) {
    // 获取key的hashCode并进行hash计算
    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)]) {
        ThreadLocal<?> k = e.get();
        
        // 存在参数key则替换value值即可
        if (k == key) {
            e.value = value;
            return;
        }
        // 存在旧的Entry则调用replaceStaleEntry处理
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 上面步骤都没有存储,则通过开放寻址找到通过hash计算槽位后的第一个空槽位
    // 将元素存储
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 满足扩容条件则扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
  1. 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;
        }
    }
}

该方法就是就是将参数指定的key清除,当然其也伴随这旧的Entry的清理,但是都是局部的。
到此ThreadLocalMap的各个方法属性都梳理完毕,接下来来梳理ThreadLocal的各个方法。

ThreadLocal源码剖析

分析完ThreadLocal源码后,接下来分析ThreadLocal的源码,首先分析一下其全局变量,然后分析其非public方法吗,这些都是一些公用的基础方法,接下来来了解set、get和remove方法。

ThreadLocal属性剖析

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的nextHashcode属性、HAHS_INCREMENT指定了每一个ThreadLocal在创建是已经指定了。

ThreadLocal各个方法剖析

  1. initialValue
protected T initialValue() {
    return null;
}

在ThreadLocal没有设置值时通过该方法获取到设置的默认值。

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

为参数线程的threadLocals初始化,threadLocals为一个ThreadLocalMap。

  1. getMap
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

获取到参数线程的threadLocals属性。

  1. setInitialValue
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

设置默认的value,通过调用initialValue方法获取默认值,然后以当前ThreadLocal实例为key,调用initialValue方法返回的值为值放入Thread的threadLocals的map中。

  1. get
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

首先通过当前线程获取到ThreadLocalMap,然后通过当前ThreadLocal为key查找值,查找不到则调用setInitialValue方法设置并返回默认值。

  1. 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);
}

首先判断当前线程的threadLocals是否初始化,没有则先初始化,然后将当前ThreadLocal实例为key,参数value为值放入当前线程的threadLocals中。

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

remove方法将当前ThreadLocal实例对应的key和value从当前线程的threadLocals中移除。
到此ThreadLocal的主要方法源码已经剖析完毕。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值