ThreadLocal源码解析

ThreadLocal是什么?

在Java体系中有一个ThreadLocal类,它用于提供线程内部的局部变量,这些变量与它们的正常对象不同,每个线程拥有一个单独属于自己的,独立的变量的初始副本。
简单来说,通过ThreadLocal可以实现多线程中数据隔离效果。

ThreadLocal如何使用?

通过ThreadLocal的简单使用,来验证数据隔离效果:

public class Main {
    public static void main(String[] args) {
    
        ThreadLocal<String> threadLocal = new ThreadLocal<>(); // 使用同一个threadLocal对象

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    threadLocal.set("a"); // 设置字符串“a”
                    Thread.currentThread().sleep(3000); // 此处睡眠3秒钟,让其他线程拥有足够时间更新threadLocal中的数据
                    System.out.println(threadLocal.get()); // 查看设置的字符串是否还是当前线程中设置的值
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.set("b"); // 设置字符串“b”
            }
        }).start();
    }
}

在这里插入图片描述

ThreadLocalMap内部类

ThreadLocalMap是ThreadLocal中的一个静态内部类,它实现了ThreadLocal类中大部分的底层操作,也是实现各个线程数据隔离的关键所在。
ThreadLocalMap这个类本质上是一个map,和HashMap之类的实现相似,保存数据依然是key-value的形式。
所以在全面了解ThreadLocal之前,先来简单了解下ThreadLocalMap的各个重要属性:

/**
* ThreadLocalMap其中还有一个静态内部类Entry,可以将其看做保存的数据结点
* 其中key可以看做是ThreadLocal实例,但是其本质是持有ThreadLocal实例的弱引用
* 其中value就是对应保存的数据了
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

// 保存数据结点的数组
private Entry[] table;

// 数据初始容量
private static final int INITIAL_CAPACITY = 16;

// 记录数据结点数量
private int size = 0;

// 数组扩容的临界值,默认是0
private int threshold;

从上面的注释可以知道,这个Entry内部类其实和HashMap的Node内部类十分类似,都是用于封装数据的类。
但是这里有个疑惑,对于这个Entry类中的保存key值的方式是什么呢?我们来看下它的构造方法:

Entry(ThreadLocal<?> k, Object v) {
    super(k); // 调用父类的构造方法,将ThreadLocal实例作为key值传递进去
    value = v;
}

public WeakReference(T referent) {
    super(referent); // 再次调用父类构造方法,将ThreadLocal实例作为参数传递进去
}

Reference(T referent) {
    this(referent, null);
}

Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent; // 从这里可以看到,最终ThreadLocal实例赋值给了当前实例的referent属性
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

到此,以上内容就是ThreadLocalMap内部类的初步认识。

ThreadLocal重点方法源码解析

set

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); // 获取ThreadLocalMap实例对象map
    if (map != null) // map对象不为空,说明已初始化
        map.set(this, value); // 直接设置添加数据
    else
        createMap(t, value); // 否则,先初始化ThreadLocalMap,再添加数据
}
  • getMap——获取ThreadLocalMap实例对象
/**
 * 其实通过返回值就可以看到,所谓的getMap获取ThreadLocalMap实例对象中的实例对象,其实就是Thread线程类中的一个属性
 * 说明,不同的线程就会拥有不同的ThreadLocalMap实例对象
 * 也正是如此,才实现了不同线程之间的数据隔离
*/
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
  • map.set——为ThreadLocalMap中设置添加数据
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1); // 根据ThreadLocal的哈希码和table的长度,再通过路由算法找出应该将数据存放在table数组的位置索引
    
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { // 遍历整个table,查看是否有可替换的数据
        ThreadLocal<?> k = e.get(); // 获取元素中的ThreadLocal实例对象k
        if (k == key) { // k与key双方地址相等,说明找到同个ThreadLocal实例对象
            e.value = value; // 替换value值
            return;
        }
        if (k == null) { // k为空,说明原本数据中的key值为空,原数据已失效
            replaceStaleEntry(key, value, i); // 替换无效数据,并清除其他无效数据
            return;
        }
    }
	// 无可替换的数据,则添加数据
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) // 计划清除从i位置往后的无效数据,但是不存在无效数据,而且数据结点个数大于等于table扩容临界值
        rehash(); // 调整table大小
}

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0); // 由此可看出,当获取下一个索引超过table的长度时,就会重新归0。说明,可以将table看作是一个环形数组
}

public T get() {
    return this.referent; // 还记的之前保存数据的key值时候的referent属性吗?是的,这里就是获取当初保存的ThreadLocal实例对象
}
  • replaceStaleEntry——替换无效数据
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
    
    int slotToExpunge = staleSlot; // slotToExpunge之后会作为清除无效数据的起点
    for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) // 往前遍历,寻找第一个key为空的无效数据,在之后作为清除无效数据的起始索引位置
        if (e.get() == null)
            slotToExpunge = i;

    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == key) { // 有已存在key和需要保存的key相等,则更新其value值,并替换掉staleSlot索引位置中key为空的数据
            e.value = value;
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
            if (slotToExpunge == staleSlot) // 相等,说明往前遍历寻找清除无效数据起始索引没有找到,staleSlot位置前面的数据都是有效的
                slotToExpunge = i; // 因为进行了数据替换,现在无效数据处于i索引位置,所以更新slotToExpunge 
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); // 清除无效数据
            return;
        }

        if (k == null && slotToExpunge == staleSlot) // 往前查找没有找到无效数据,而且当前i位置中的是无效数据,更新slotToExpunge(对于staleSlot中的无效数据会在之后的操作中替换成有效数据)
            slotToExpunge = i;
    }
    // 没有找到可替换的有效数据,则直接将staleSlot位置的无效数据进行更新
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
    if (slotToExpunge != staleSlot) // 不相等,说明在存在slotToExpunge之后无效数据
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
  • expungeStaleEntry——从指定的索引位置往后开始清除无效数据,并返回从staleSlot之后的第一个空数据的索引位置
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
	// 首先清除第一个无效数据
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { // 往后遍历,清除无效数据
        ThreadLocal<?> k = e.get();
        if (k == null) { // k为空,清除该位置的无效数据
            e.value = null;
            tab[i] = null;
            size--;
        } else { // k不为空,则根据k值重新计算索引
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) { // 计算后的索引与当前索引不相等
                tab[i] = null; // i索引位置置空
                while (tab[h] != null) // 从h往后寻找到为空的位置,将原i索引位置的数据移动过去
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}
  • cleanSomeSlots——清除从指定位置之后的无效数据,若存在无效数据则返回true,否则返回false
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];
        if (e != null && e.get() == null) { 
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}
  • rehash——调整table大小
private void rehash() {
    expungeStaleEntries(); // 删除table中存在的无效数据
    if (size >= threshold - threshold / 4) // 删除完无效数据后,仍然超过扩容临界值
        resize(); // 扩容
}

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);
    }
}
  • resize——table扩容
private void resize() {
     Entry[] oldTab = table;
     int oldLen = oldTab.length;
     int newLen = oldLen * 2; // 扩容为原table的2倍
     Entry[] newTab = new Entry[newLen];
     int count = 0;

     for (int j = 0; j < oldLen; ++j) { // 将原table中的数据迁移到新table中
         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;
 }
  • createMap——创建ThreadLocalMap
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY]; // 初始化table
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 计算索引
    table[i] = new Entry(firstKey, firstValue); // 保存数据
    size = 1;
    setThreshold(INITIAL_CAPACITY); // 设置临界值
}

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(); // map为空,或者获取到的e为空,则初始化map,或者初始化数据
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals; // 返回当前线程的ThreadLocalMap实例对象
}
  • getEntry——根据指定ThreadLocal获取value
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1); // 计算索引
    Entry e = table[i];
    if (e != null && e.get() == key) // key值相等,说明找到指定数据,并返回
        return e;
    else
        return getEntryAfterMiss(key, i, e); // e等于空,或者key值不相等,说明原位置数据失效被删除,或者被迁移到其他位置去了
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) { // e不等于空,说明是key值不相等,说明原位置数据被迁移。则往后寻找指定数据
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null; // e等于空,说明原位置数据失效被删除,返回空
}
  • 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;
}

protected T initialValue() {
    return null; // 此处返回空,该方法是用于子类覆盖的
}

remove

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

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)]) { // 循环遍历table
        if (e.get() == key) {
            e.clear(); // 清空指定数据
            expungeStaleEntry(i); // 清除无效数据
            return;
        }
    }
}

public void clear() {
    this.referent = null;
}

总体来说,ThreadLocal实现数据隔离的关键就是内部类ThreadLocalMap。从源码分析的角度来看,ThreadLocalMap其实是Thread线程类的一个属性,也就是说不同的线程拥有不同的ThreadLocalMap,而ThreadLocalMap中的Key又是相同的ThreadLocal对象,由此达到数据隔离的效果

ThreadLocal内存泄漏问题

对于ThreadLocal来说,虽然可以实现数据隔离,但使用不当的话会存在内存泄漏问题。
基于上面的源码解析,可以知道ThreadLocal在内存方面上的实现如下:
在这里插入图片描述
在讲解ThreadLocalMap如何存储以ThreadLocal作为key值存储的时候,有说到是作为弱引用来存储。正因为是弱引用,在GC垃圾回收机制工作的时候,会自动回收这个key。自然地,当再次调用set()方法设置数据时候,就无法根据key值找到这个Entry进行替换了,而是执行添加数据的操作。而原本Entry的key虽然被回收,但是value的引用却是强引用,GC不会回收,导致了已经无法获取的Entry仍然会存在于内存中。当这样的Entry越来越多,占用的内存也就越多,就会导致内存泄漏问题。
因此需要及时调用remove()方法。

ThreadLocal脏数据问题

从以上分析可知,之所以使用ThreadLocal可以达到各个线程数据隔离的效果,是因为每个线程都有自己的ThreadLocalMap,以ThreadLocal作为key值存储。也就是说,只要你是同个线程,那么你的ThreadLocalMap就是同一个,自然地其中保存的数据也是相同的。
那么,如果我使用线程池呢?
了解线程池的都知道,使用线程池就是为了线程复用,而使用线程复用就会导致复用了同一个ThreadLocalMap,导致这次线程任务get到的数据有可能是上次任务的,这样就会导致脏数据问题。对于这个问题,没有什么特别的办法,只能说是针对业务谨慎使用ThreadLocal。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值