ThreadLocal源码详解

ThreadLocal

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

提供线程的私有变量。

实现模型

具体的方式是每个线程都持有一个ThreadLocalMap对象

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

    /*
     * 可继承的本地变量
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

ThreadLocalMap持有一个Entry数组,key是ThreadLocal对象,value就是具体的值,要注意的一点是key是弱引用。

static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        private static final int INITIAL_CAPACITY = 16;
        private Entry[] table;
        private int size = 0;
        private int threshold; // Default to 0 默认大小是 len * 2/3
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZdyUn9Ed-1678182050913)(/Users/cool/Documents/门票md/学习/java/线程/ThreadLocal.assets/image-20230301125033536.png)]

解决hash冲突的常见方式

链表法

链表法比较简单,就是再每个哈希冲突的节点,增加一个链表来存储相同hash值的数据。java中使用此方式的典型就是HashMap.

HashMap中,当链表长度大于等于8的时候会进行树化的操作,当节点个数小于等于6的时候会进行链化

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SaOnB3kC-1678182050915)(/Users/cool/Documents/门票md/学习/java/线程/ThreadLocal.assets/image-20230228174836992.png)]

再哈希法

再哈希法其实很简单,就是再使用哈希函数去散列一个输入的时候,输出是同一个位置就再次哈希,直至不发生冲突位置

缺点:每次冲突都要重新哈希,计算时间增加。

开放寻址法

在开放寻址法中,所有元素都存在同一个哈希表里。也就是说,每个表项或包含动态集合的一个元素,或包含NULL(空)。当查找某个元素时,要系统地检查所有的表项,直到找到所需的元素或者最终查明该元素不在表中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rCEj615l-1678182050916)(/Users/cool/Documents/门票md/学习/java/线程/ThreadLocal.assets/image-20230228200109530.png)]

ThreadLocal已经尽可能的帮我们去避免内存泄漏和存取效率了,每次get和set都尝试去清除掉无效的entry,并且重置部分entry的位置使得查询效率变快。里面的很多算法都是在大量样本的筛选中去折中优化的。

get方法

   private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
              //如果hash的位置没有找到 就会继续查询
                return getEntryAfterMiss(key, i, e);
        }

       private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                  	// 清除失效的的数据 并且重新对数据进行排序
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0pClryb9-1678182050916)(/Users/cool/Documents/门票md/学习/java/线程/ThreadLocal.assets/image-20230302194040138.png)]

然后是清除的逻辑,见下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C39juyeA-1678182050917)(/Users/cool/Documents/门票md/学习/java/线程/ThreadLocal.assets/image-20230302201700322.png)]

Java17

 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);
        }
		//多了个引用查看 所有的key比较都替换成了此方法
    //应该涉及到弱引用,实际效率上比 == 更高
    public final boolean refersTo(T obj) {
      	//native方法
        return refersToImpl(obj);
    }
JNIEXPORT jboolean JNICALL
Java_java_lang_ref_Reference_refersTo0(JNIEnv *env, jobject ref, jobject o)
{
    return JVM_ReferenceRefersTo(env, ref, o);
}

JVM_ENTRY(jboolean, JVM_ReferenceRefersTo(JNIEnv* env, jobject ref, jobject o))
  oop ref_oop = JNIHandles::resolve_non_null(ref);
  oop referent = java_lang_ref_Reference::weak_referent_no_keepalive(ref_oop);
	//本质上是比较地址是否相等 resolve中会判断o是不是虚引用来进行不同的操作
  return referent == JNIHandles::resolve(o);
JVM_END
  
 template <DecoratorSet decorators, bool external_guard>
inline oop JNIHandles::resolve_impl(jobject handle) {
  assert(handle != NULL, "precondition");
  assert(!current_thread_in_native(), "must not be in native");
  oop result;
  //判断是否是虚引用
  if (is_jweak(handle)) {       // Unlikely
    result = NativeAccess<ON_PHANTOM_OOP_REF|decorators>::oop_load(jweak_ptr(handle));
  } else {
    result = NativeAccess<decorators>::oop_load(jobject_ptr(handle));
    // Construction of jobjects canonicalize a null value into a null
    // jobject, so for non-jweak the pointee should never be null.
    assert(external_guard || result != NULL, "Invalid JNI handle");
  }
  return result;
}
inline oop* JNIHandles::jobject_ptr(jobject handle) {
  assert(!is_jweak(handle), "precondition");
  return reinterpret_cast<oop*>(handle);
}

inline oop* JNIHandles::jweak_ptr(jobject handle) {
  assert(is_jweak(handle), "precondition");
  char* ptr = reinterpret_cast<char*>(handle) - weak_tag_value;
  return reinterpret_cast<oop*>(ptr);
}

set方法

关于set的插入,上面开放寻址法已经介绍了简单的流程,但是真正的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.
 	//上面说set方法会更加麻烦,不能像使用get的流程,因为插入更容易冲突。
  
    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();
      //查到key直接替换value
        if (k == key) {
            e.value = value;
            return;
        }
      //如果查到key已经失效的 那就进行数据替换 并且执行清除逻辑
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
   // 如果最终没有找到 就会新创建一个对象插入
    tab[i] = new Entry(key, value);
    int sz = ++size;
  //尝试清除一下slot 如果成功清除了数据 并且到了阈值 就尝试rehash
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

扩容阈值 size >= threshold - threshold / 4 ,而threshold = len * 2 /3 ,至于扩容阈值为什么会是3/4,源码注释为Use lower threshold for doubling to avoid hysteresis

   private void rehash() {
            expungeStaleEntries();
            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
 }

扩容后的大小为原大小的2倍,官方为了不内存泄漏,拼了命的帮我们添加补救措施

 private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
						//这个代码太长了 删除一下额外代码 保留核心代码
            int slotToExpunge = staleSlot;
   					//首先尝试向前寻找可清除的节点
   					//直到碰到第一个为null的节点 threadlocal中基本上边界都是null
   					//这一系列复杂的逻辑都是为了保护内存
            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();
							  //为什么上面向前查询的时候不会进行key的查询
              	//因为进入replace之前是从hash出来slot开始查找的 所以大概率数据会在后面
              	//并且next方法是个环形方法,最终肯定会扫到相关数据
                if (k == key) {
                    e.value = value;
                    //交换数据
                  	//因为下面的逻辑会把i所在位置的数据进行清除
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;
                    // Start expunge at preceding stale entry if it exists
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                  	//cleanSomeSlots方法会尝试扫描几次null边界开始往后的数据 是否存在需要清除的
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

        }
   			//如果最终没有扫到 也是进行创建插入
        tab[staleSlot].value = null;
        tab[staleSlot] = new Entry(key, value);
   			//上面清除逻辑没有执行到 会进行兜底清除
        if (slotToExpunge != staleSlot)
             cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
 }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mpZxZPM1-1678182050918)(/Users/cool/Documents/门票md/学习/java/线程/ThreadLocal.assets/image-20230302212638991.png)]

ThreadLocal的其他实现

InheritableThreadLocal

这个类是jdk提供的基于inheritableThreadLocals字段的实现类,重写相关方法来达到可继承的目的。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    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);
    }
}

但是这个对于每次新创建线程的时候使用是满足的,因为inheritableThreadLocals只有在线程初始化的时候进行一次继承,

所以这种实现方式对于使用线程池是无力的,当我们的主线程进行了修改,子线程无法感知。

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

TransmittableThreadLocal

这个是alibaba提供的新的ThreadLocal子类,需要配合TtlRunnable或者TtlExecutors来使用。

其中,能够保证子线程能给实时继承父线程数据有两点

  • TransmittableThreadLocal中保持了父线程的threadLocal。
  • TtlRunnable创建的时候保持了一份ThreadLocal的快照

下面是TransmittableThreadLocal的holder对象。

    // Note about holder:
    // 1. holder self is a InheritableThreadLocal(a *ThreadLocal*).
    // 2. The type of value in holder is WeakHashMap<TransmittableThreadLocal<Object>, ?>.
    //    2.1 but the WeakHashMap is used as a *Set*:
    //        - the value of WeakHashMap is *always null,
    //        - and never be used.
    //    2.2 WeakHashMap support *null* value.
    private static InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =
            new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
                @Override
                protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
                    return new WeakHashMap<TransmittableThreadLocal<Object>, Object>();
                }

                @Override
                protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
                    return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue);
                }
            };

TtlRunnable的核心实现如下

  //重新run方法
   @Override
    public void run() {
        Object captured = capturedRef.get();
        if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }
				//保留当前线程的数据备份 并且将快照中的数据 设置到线程中
        //这个的本质就是runnable不使用线程原有的threadLocal中的数据
        Object backup = replay(captured);
        try {
            runnable.run();
        } finally {
          //执行完之后恢复原有线程数据
            restore(backup);
        }
    }
		//back生成
    public static Object replay(@NonNull Object captured) {
            final Snapshot capturedSnapshot = (Snapshot) captured;
            return new Snapshot(replayTtlValues(capturedSnapshot.ttl2Value), replayThreadLocalValues(capturedSnapshot.threadLocal2Value));
        }

runnable初始化的时候会将holder中的数据进行快照存储

public static Object capture() {
    return new Snapshot(captureTtlValues(), captureThreadLocalValues());
}

private static WeakHashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() {
    WeakHashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new WeakHashMap<TransmittableThreadLocal<Object>, Object>();
  //将快照中的数据进行存储
    for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
        ttl2Value.put(threadLocal, threadLocal.copyValue());
    }
    return ttl2Value;
}
    private static WeakHashMap<TransmittableThreadLocal<Object>, Object> replayTtlValues(@NonNull WeakHashMap<TransmittableThreadLocal<Object>, Object> captured) {
            WeakHashMap<TransmittableThreadLocal<Object>, Object> backup = new WeakHashMap<TransmittableThreadLocal<Object>, Object>();
            for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
                TransmittableThreadLocal<Object> threadLocal = iterator.next();
                // backup 保留当前线程中的备份
              	// 这个位置value是单独存储的 不直接使用get()方法获得 是怕中间会变动数据 不能起到备份的效果
                // 当然如果value是个引用对象 修改了引用对象里面的数据 也是无能为力
                backup.put(threadLocal, threadLocal.get());
                // 清除初始化数据中没有 而当前线程中又新增加的数据
              	//我当时疑惑过为什么不直接使用 clear方法 可能是作者考虑到这种概率比较小 并且 clear方法比较耗资源
                if (!captured.containsKey(threadLocal)) {
                    iterator.remove();
                    threadLocal.superRemove();
                }
            }
            // 设置初始化的数据到线程中
            setTtlValuesTo(captured);
            // call beforeExecute callback 这个回调是空方法 需要自己实现
            doExecuteCallback(true);
            return backup;
        }
// 设置初始化的数据到线程中
 private static void setTtlValuesTo(@NonNull WeakHashMap<TransmittableThreadLocal<Object>, Object> ttlValues) {
            for (Map.Entry<TransmittableThreadLocal<Object>, Object> entry : ttlValues.entrySet()) {
                TransmittableThreadLocal<Object> threadLocal = entry.getKey();
                threadLocal.set(entry.getValue());
            }
        }

思考

为什么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;
            }
        }

因为我们不确定除了threadlocalmap在引用外,还有没有其他的value引用,如果没有了其他应用,那么value会被回收。我们在get的时候就会得到一个null,很明显预期不符。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
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、付费专栏及课程。

余额充值