Loading

ThreadLocal源码阅读

简介

ThreadLocal通过通过线程隔离的方式防止任务在共享资源上产生冲突,即用资源副本保证本线程访问资源安全性。狭义地说就是,每个线程访问的同种资源实际上都是不同的实例。

例子

假设有两个不同的数据库源,并且这俩数据库在业务代码中都得被多线程共享,那么自然会为这两个数据库分别设置一个ThreadLocal,来使得数据库连接在不同够的线程中隔离。

public class DB1 {
  public static final ThreadLocal<DB1Connection> tl;
}
public class DB1 {
  public static final ThreadLocal<DB2Connection> tl;
}

在线程中获取数据库连接的时候通过ThreadLocal.get获取:

DB1Connection conn1 = db1.tl.get();
DB1Connection conn2 = db2.tl.get();

可以发现Thread与ThreadLocal形成了多对多的关系,如何做到呢,就靠ThreadLocalMap(Thread的成员变量threadLocals)。这里先把它看成一个键为ThreadLocal值为Object的普通map。线程调用ThreadLocal.get的时候,get方法只需要:

Thread.currentThread().threadLocals.get(this);

就能获取到该Thread在这个ThreadLocal上对应的资源了(在这里是数据库连接)。

代码分析

ThreadLocalMap

基于上面ThreadLocal的简单使用场景,梳理一下下面三个的类之间的关系:

  1. Thread
  2. ThreadLocal
  3. ThreadLocalMap

Thread有一个ThreadLocalMap成员变量,来记录Thread在不同ThreadLocal上所拥有的资源:

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

ThreadLocalMap是一个内部实现的一个map,ThreadLocal只用了ThreadLocalMap的如下三个方法:

  1. Entry getEntry(ThreadLocal<?> key)
  2. void set(ThreadLocal<?> key, Object value)
  3. void remove(ThreadLocal<?> key)

因此对于ThreadLocal来说这玩意可以看成一个键类型ThreadLocal值类型为Object的Map。

那么这个map如何被使用呢?由于一个Thread可以使用不同的ThreadLocalMap来获取资源,因此通过使用ThreadLocalMap作为Thread的成员变量,来记录该线程在各个ThreadLocal上所对应的资源。当线程使用ThreadLocal.get获取属于该线程的资源的时候,ThreadLocal就能通过该成员变量获取当前线程(Thread.currentThread())在该ThreadLocal(this)上对应的资源。

Thread.currentThread().threadLocals.get(this);

ThreadLocalMap用数组实现map,用线性探测法解决冲突,并且在复杂因子超过一定量的时候进行压缩甚至扩容。

最重要的成员变量table数组,数据元素是继承自WeakReference<ThreadLocal<?>>的Entry:

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作为key的考量在于:ThreadLocal可能不会再被任何线程引用,此时就能检测到这个Entry变成了"stale"的,可以拿来复用了。

其他的成员变量size, threshold就没什么好介绍的了。

接下来看看上面说的最重要的三个方法。

getEntry

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

既然ThreadLocal作ThreadLocalMap的键,那第一步就是计算它的哈希值...哦原来不用计算,哈希值就是threadLocalHashCode成员变量,至于它是怎么初始化的文后再说,只需要知道它保存了这个ThreadLocal实例的哈希值就行。

然后看一下数组对应位置里面是否是这个key,如果不在的话有两种可能:

  1. 由于哈希冲突、压缩表、扩容表,导致该键被分配到了别的slot
  2. 根本不存在该键

因此下面的getEntryAfterMiss会遍历其他的位置找这个key,最终找不到就返回null。

set

private void set(ThreadLocal<?> key, Object value) {
    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.value = value;
            return;
        }

        if (e.refersTo(null)) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

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

看着比getEntry长一点,但其实也很简单:使用线性探测法,从哈希的位置开始找到可用的slot,如果是stale slot的话就直接替换。最后稍微压缩一下表(cleanSomeSlots),然后如果负载因子还是太大的话就继续压缩(rehash里调用expungeStaleEntries),负载因子还是还是太大的话,直接扩容成两倍大小(rehash里调用resize)!

remove

上面两个方法能看懂的话,remove更加不用说了,简单!

ThreadLocalMap其他的细节

  • Entry.refersTo(obj)方法:检测弱引用是否引用了对象obj,不用get() == obj的原因在于get返回对象的强引用,对GC不友好。如果你只是想检测一下,并不想要返回强引用的话,refersTo是最佳选择。
  • ThreadLocalMap其他方法比如rehash、expungeStaleEntry等,基本是用于压缩表啥的细枝末节,研究意义不大。
  • ThreadLocal的threadLocalHashCode怎么来的,以及ThreadLocal这个魔数怎么来的:What is the meaning of 0x61C88647 constant in ThreadLocal.java

ThreadLocal

ThreadLocal本身没什么可以探究的,稍微翻看一下源码就能理解,巧妙的点在于ThreadLocal为线程资源隔离提供了一个很好的思路。总而言之,可以看成每个ThreadLocal实例对应一种资源,每个线程可以通过ThreadLocal获取该种资源的实例,同一个线程多次获取到的是同一个实例,不同线程获取到的是不同的实例,以实现线程对该种资源的隔离访问。

下面说一下ThreadLocal的一些派生类:

SuppliedThreadLocal

配合ThreadLocal的静态方法withInitial使用,不想用匿名类并重载initialValue的方式创建ThreadLocal的话,可以使用withInitial

TerminatingThreadLocal

这个类作为ThreadLocal的一个扩展,除了基本的ThreadLocal资源隔离,还能让你在线程退出的时候做一些比如资源清理之类的事情。因此用户只需要重载这个方法:

protected void threadTerminated(T value) {}

当线程结束的时候,这个方法就会自动被调用。那么这是如何做到的呢?看看这个方法:

/**
 * Invokes the TerminatingThreadLocal's {@link #threadTerminated()} method
 * on all instances registered in current thread.
 */
public static void threadTerminated() {
    for (TerminatingThreadLocal<?> ttl : REGISTRY.get()) {
        ttl._threadTerminated();
    }
}

这个方法会在Thread.exit中被调用。其中REGISTRY.get()获取该线程所有注册的TerminatingThreadLocal,对每个TerminatingThreadLoca将资源取出来作为参数,回调用户注册好的方法:

private void _threadTerminated() { threadTerminated(get()); }

最后,注册会在创建资源的时候发生,remove的时候取消(资源已经被移除了,因此也没必要拿着null值去回调)

InheritableThreadLocal

ThreadLocal很明显只要是不同的线程get拿到的一定是不同的资源,因此InheritableThreadLocal提供了子线程想要拿到父线程的资源的方法。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    public InheritableThreadLocal() {}

    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

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

这个派生类很简单,只重载了这么几个方法,Thread.inheritableThreadLocals成员保存了可用于继承资源的ThreadLocal对应的ThreadLocalMap。

相比ThreadLocal.ThreadLocalMap的懒加载(在get、set时才会为线程创建ThreadLocalMap),InheritableThreadLocal.ThreadLocalMap会在线程构造函数中就被创建,并且当且仅当在线程的构造函数中继承父线程的资源:

private Thread(ThreadGroup g, Runnable target, String name,
               long stackSize, AccessControlContext acc,
               boolean inheritThreadLocals) {
	// ...
  
  Thread parent = currentThread();
  
  // 在这里继承父线程在ThreadLocal中的资源
  if (inheritThreadLocals && parent.inheritableThreadLocals != null)
      this.inheritableThreadLocals =
          ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  
  // ...
}


static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
  	// 注意这里,子线程的ThreadLocalMap使用的数组是新的实例,而不是指向parentMap的数组
  	// 因此继承只在线程构造的时刻完成,在这之后父子线程对ThreadLocal的操作毫无关系
    table = new Entry[len];

    for (Entry e : parentTable) {
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

如果用了InheritableThreadLocal,但是某个子线程并不想继承父线程的资源怎么办?Thread的构造函数不是有inheritThreadLocals可以控制这个行为吗hhhhhh...

posted @ 2023-12-18 16:40  NOSAE  阅读(29)  评论(0编辑  收藏  举报