ThreadLocal源码分析

0x00 ThreadLocal

ThreadLocal提供了线程局部的变量,但和普通局部变量不同,同一个ThreadLocal变量可以被多个线程共享,而不是线程私有的。

在ThreadLocal源代码中有一个使用例子,代码如下:

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadId {
    // Atomic integer containing the next thread ID to be assigned
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // Thread local variable containing each thread's ID
    private static final ThreadLocal<Integer> threadId =
    new ThreadLocal<Integer>() {
        @Override protected Integer initialValue() {
            return nextId.getAndIncrement();
        }
    };

    // Returns the current thread's unique ID, assigning it if necessary
    public static int get() {
        return threadId.get();
    }
}

我们以这段代码为例,简单探索一下ThreadLocal的内部原理。

假设我们现在需要生成线程ID,则可以这么写:

public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(ThreadId.get());
        }).start();

        new Thread(() -> {
            System.out.println(ThreadId.get());
        }).start();
}

// 输出
// 0
// 1

为什么不同线程调用ThreadId.get()会有不同的行为呢?我们看一下ThreadLocal类的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 getMap(Thread t) {
    return t.threadLocals;
}

可以发现当一个线程调用get()时,会取出该线程内部的threadLocals变量,并以ThreadLocal对象为key取出map中对应的值。如果threadLocals为null,则再调用setInitialValue()方法,创建一个新的threadLocals变量。

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);
    }
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    return value;
}

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

protected T initialValue() {
    return null;
}

在调用setInitialValue()方法时需要调用initialValue()获得一个初值,默认情况下是null。因此在开头的例子中也重写了这个initialValue()来为ThreadLocal变量提供初值。

既然有get()方法,同样也有set()方法,内部仅仅是调用了map的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);
    }
}

所以到目前为止,我们发现:每个Thread对象内部都会有一个成员变量threadLocals,这实质上是一个哈希表,它以ThreadLocal为key,而value的类型是不固定的,取决于ThreadLocal对象声明时的泛型。
当同一个ThreadLocal对象被多个线程访问时,每个线程都会创建一个键值对并放入自己的threadLocals变量内,这个键值对的key是该ThreadLocal对象,而值则是由ThreadLocal对象的initialValue()提供。除了一个ThreadLocal对象可以被多个线程访问,一个线程内的threadLocals也可以同时持有多个ThreadLocal对象,因此线程和ThreadLocal是多对多的关系。

回顾开头提到的,同一个ThreadLocal对象被多个线程共享但仍然线程安全的原因在于,这个ThreadLocal对象仅仅是作为一个只读的key存放在每个线程的threadLocals内部,而它对应的值则是以副本的形式作为键值对的value,每个线程的threadLocals以相同的ThreadLocal作为key,但是值是不共享的。

0x01 ThreadLocalMap

在前一节中,我们发现ThreadLocal仅仅是存在每个线程的threadLocals里,threadLocals是一个ThreadLocalMap对象,我们简单探索一下它的内部。

static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

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

    private Entry[] table;
    
    // ...
}

可以发现ThreadLocalMap是一个哈希表,并且采用线性探测法来解决冲突。其内部类Entry继承了弱引用,因此每个entry的key是对ThreadLocal对象的弱引用,而value则是强引用。

这里其实有些令人疑惑,为什么Entry类要对key使用弱引用而不是强引用呢?在源代码中我发现这样一段注释:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. However, since reference queues are not used, stale entries are guaranteed to be removed only when the table starts running out of space.
原意大概是说,为了帮助处理那些大且长时间存活的对象,因此对key进行弱引用,但是由于没有使用到引用队列,因此这些过期的entry只有在哈希表快用完时才保证一定删除。实际上,在ThreadLocalMap的getEntry或setEntry方法中,只要检测到某个key为null,就会将对应的value引用置为null,让GC能够及时回收。

因此我们可以想象,当某一个ThreadLocal对象不再使用了,那么所有线程中关于该ThreadLocal对象的value也会被回收。这里涉及到弱引用的概念,如果某个对象仅仅存在弱引用,那么GC一旦发现该对象,无论在内存是否充足的情况下都会立刻回收它。

0x02 ThreadLocal内存泄漏

前面我们提高,ThreadLocal类对象一旦不再使用,其对应的value在各个线程中也会被回收。然而,实际使用中我们通常将ThreadLocal类对象设为static,这就意味着整个程序运行过程中它都会一直存活,那么只要该线程不结束,即使某一个ThreadLocal对象不再使用了,线程内部的threadLocals始终不会被回收。这就引起了内存泄漏问题。

要解决这个问题也很简单,就是一旦ThreadLocal对象在某个线程中不再使用了,就调用remove方法去手动删除对应的entry。

/**
 * Remove the entry for 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.refersTo(key)) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}
ThreadLocal源码Java中一个关键的类,它提供了一种在多线程环境下实现线程本地变量的机制。在JDK 8之前和之后,ThreadLocal的内部结构有所变化。ThreadLocal源码分为两部分:ThreadLocal类和ThreadLocalMap类。 ThreadLocal类是一个泛型类,它包含了两个核心方法:set()和get()。set()方法用于将一个值与当前线程关联起来,get()方法用于获取当前线程关联的值。 ThreadLocalMap类是ThreadLocal的内部类,它用于存储每个线程的本地变量。在JDK 8之前,ThreadLocalMap是通过线性探测法解决哈希冲突的,每个ThreadLocal对象都对应一个Entry对象,Entry对象包含了ThreadLocal对象和与之关联的值[2]。 在JDK 8之后,ThreadLocalMap的实现方式发生了改变。使用了类似于HashMap的方式,采用了分段锁的机制来提高并发性能。每个线程维护一个ThreadLocalMap对象,其中的Entry对象也是采用链表的形式来解决哈希冲突。 总结起来,ThreadLocal源码主要由ThreadLocal类和ThreadLocalMap类组成。ThreadLocal类提供了set()和get()方法来管理线程本地变量,而ThreadLocalMap类则负责存储每个线程的本地变量,并解决哈希冲突的问题。 史上最全ThreadLocal 详解 ThreadLocal源码分析_02 内核(ThreadLocalMap) 【JDK源码】线程系列之ThreadLocal 深挖ThreadLocal ThreadLocal原理及内存泄露预防 ThreadLocal原理详解——终于弄明白了ThreadLocal ThreadLocal使用与原理 史上最全ThreadLocal 详解。 ThreadLocal源码分析,主要有ThreadLocal源码以及ThreadLocal的内部结构在jdk8前后的变化。 使用方式非常简单,核心就两个方法set/get public class TestThreadLocal { private static final ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { try { threadLocal.set("aaa"); Thread.sleep(500); System.out.println("threadA:" threadLocal.get()); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { threadLocal.set("bbb"); System.out.println("threadB:" threadLocal.get()); } }).start(); } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值