Java ThreadLocal 类简析

2022/5/27 1:20:04

本文主要是介绍Java ThreadLocal 类简析,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

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 ThreadLocal 类简析的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程