ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

Java ThreadLocal 类简析

2022-05-26 23:34:56  阅读:235  来源: 互联网

标签:Java ThreadLocalMap ThreadLocal value 简析 线程 key Entry


ThreadLocal

ThreadLocal 类的作用就是实现每一个线程都有自己的专属本地变量

使用

简单示例

public class Demo01 implements Runnable {
    // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    public static void main(String[] args) throws InterruptedException {
        Demo01 obj = new Demo01();
        for(int i=0 ; i<10; i++){
            Thread t = new Thread(obj, ""+i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }

    @Override
    public void run() {
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // formatter 被重新设置,但是其它的线程不受影响,它们仍然持有原始的那一份副本
        formatter.set(new SimpleDateFormat());

        System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
    }
}

上述代码中,Thread-0 使用 set 方法改变了 formatter,但是这个改变只对自己生效。其它线程看到的仍然是最开始的值。

ThreadLocal 原理

初始化

我们一般调用 withInitial 方法进行 ThreadLocal 对象的初始化,可以看到它的返回值的实际类型为 SuppliedThreadLocal :

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    // 返回的是 SuppliedThreadLocal<> 类型的对象
    return new SuppliedThreadLocal<>(supplier);
}

继续跟进,SuppliedThreadLocal 不过是 ThreadLocal 的静态内部类,继承了 ThreadLocal 结构也很简单:

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

    // 我们初始化的数据就存储在这个 supplier 对象中
    private final Supplier<? extends T> supplier;

    // withInitial 方法调用的构造器
    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }

    // 这是一个被重载的方法,很重要
    @Override
    protected T initialValue() {
        // 返回 supplier 中我们实际提供的对象
        return supplier.get();
    }
}
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

我们的 formatter 对象的真实类型应该是 SuppliedThreadLocal,这点很重要。

get

先来看看 Thread 类的源码,每个Thread 都有两个 ThreadLocalMap 类型的字段,且访问权限为 friendly,即同包下的类可以访问:

image-20220515172056540

摆上几个关键类的关系:

image-20220515163956135

当我们调用 ThreadLocal 的 get 方法时:

image-20220515164236134

会首先使用 getMap 方法获取当前线程的 ThreadLocalMap,由于 ThreadLocal 和 Thread 类在同一个包下(java/lang),所以可以直接访问 Thread 对象(当前线程)的 threadLocals 字段:

image-20220515164412604

如果当前线程的 ThreadLocalMap 的字段为 null,那么就需要 setInitialValue 函数来进行当前 Thread 中 ThreadLocalMap 字段的初始化:

private T setInitialValue() {
    T value = initialValue();	// 1,2
    Thread t = Thread.currentThread();	// t = 当前线程
    ThreadLocalMap map = getMap(t);	// 获取当前线程的 map,为 null
    if (map != null)
        map.set(this, value);	
    else
        createMap(t, value);	// 创建 map
    return value;	// 返回 null 或者已经初始化的值
}

// 1
// ThreadLocal 中的 initialValue 方法
// 不是用 withInitial 初始化的 ThreadLocal 调用
protected T initialValue() {
    return null;
}

// 2
// SuppliedThreadLocal 中的 initialValue
// 用 withInitial 初始化的 ThreadLocal 调用
@Override
protected T initialValue() {
    return supplier.get();	// 返回我们存储的对象
}


// t 是当前线程,firstValue 为 null
void createMap(Thread t, T firstValue) {
    // 赋值给 Thread 对象
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

如果当前线程的 ThreadLocalMap 字段不为 null,则直接使用 ThreadLocalMap 的 get 方法获取 ThreadLocalMap.Entry,get 方法的参数就是当前的 ThreadLocal 对象,而该方法的结果是一个一个弱引用对象:

// ThreadLocal 类中的 ThreadLocalMap 内部类的 Entry 内部类

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;	// 就是我们赋予给 ThreadLocal 对象的值

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

set

// 设置 ThreadLocal 的值
public void set(T value) {
    // 获取当前线程,作为 KEY
    Thread t = Thread.currentThread();
    // 以当前线程为 key 查找 map
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 如果当前线程存在对应的 map,将新的对象设置进去,key 为当前的 ThreadLocal 对象
        map.set(this, value);
    else
        // 否则就创建新的 ThreadLocalMap,并且设置值
        createMap(t, value);
}

简单总结

也就是说每个 Thread 对象中(线程)存储着一个 map(ThreadLocalMap),这个 map 的 key 值为 ThreadLocal 对象,value 为我们想赋予的值。

当我们使用 ThreadLocal 的 get 方法时,ThreadLocal 对象会根据当前线程获取到当前线程的 map,并且再以自己(ThreadLocal 对象)为 key 找到我们所赋予的值。

image-20220515173330105

内存泄露问题

在谈及 ThreadLocal 内存泄露问题之前,我们需要先看一下 ThreadLocalMap 的具体实现。

ThreadLocalMap

表项

表项的 key 为 对 ThreadLocal 对象的弱引用,value 为我们赋予的值:

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

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

ThreadLocal 构造器

调用 ThreadLocal 的 createMap 方法会直接初始化一个 ThreadLocalMap:

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 创建新的 map 表项
    table = new Entry[INITIAL_CAPACITY];
    // 计算当前 ThreadLocal 对象的 hash 值
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 塞进去
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

getEntry

ThreadLocal 的 get 方法将会调用 getEntry 方法:

private Entry getEntry(ThreadLocal<?> key) {
    // 计算 hash 值
    int i = key.threadLocalHashCode & (table.length - 1);
    // 直接获取目标对象
    Entry e = table[i];
    // 目标对象不为 null
    if (e != null && e.get() == key)
        // 返回 Entry
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

set

较为复杂,暂不涉及

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    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();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

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;
         // 获取下一个 Entry
         e = tab[i = nextIndex(i, len)]) {
        // e 是 Reference 对象,get 方法将获取弱引用的 ThreadLocal 对象
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

原因

ThreadLocalMap 存储表项的数据结构就是一个简单的数组,通过 ThreadLocal 的 hash 值来计算索引并进行存储。ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。

所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。

解决

假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。

使用完 ThreadLocal方法后 最好手动调用remove()方法。

标签:Java,ThreadLocalMap,ThreadLocal,value,简析,线程,key,Entry
来源: https://www.cnblogs.com/locustree/p/16315861.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有