ThreadLocal 源码浅析(一)

前言

多线程在访问同一个共享变量时很可能会出现并发问题,特别是在多线程对共享变量进行写入时,那么除了加锁还有其他方法避免并发问题吗?本文将详细讲解 ThreadLocal 的使用及其源码。


一、什么是 ThreadLocal?

ThreadLocal 是 JDK 包提供的,它提供了线程本地变量,也就是说,如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每一个线程,都创建这个变量的一个本地副本。

这样可以解决什么问题呢?当多个线程操作这个变量时,实际操作的是自己线程本地内存里的数据,从而避免线程安全问题

如下图,线程表中的每个线程,都有自己 ThreadLocal 变量,线程操作这个变量只是在自己的本地内存在,跟其他线程是隔离的。

二、如何使用 ThreadLocal

ThreadLocal 就是一个简单的容器,使用起来也没有难度,初始化后仅需通过 get/set 方法进行操作即可。

如下代码,开辟两个线程对 ThreadLocal 变量进行操作,获取的值是不同的。

public class FuXing {     /**     * 初始化ThreadLocal     */    private static final ThreadLocal<String> myThreadLocal = new ThreadLocal<>();     public static void main (String[] args) {        // 线程1中操作 myThreadLocal        new Thread(()->{            myThreadLocal.set("thread 1");		//set方法设置值            System.out.println(myThreadLocal.get());	//get方法获取值"thread 1"        },"thread 1").start();         // 线程2中操作 myThreadLocal        new Thread(()->{            myThreadLocal.set("thread 2");		//set方法设置值            System.out.println(myThreadLocal.get());	//get方法获取值"thread 2"        },"thread 2").start();    }}

复制代码

三、ThreadLocal 实现原理

ThreadLocal 是如何保证操作的对象只被当前线程进行访问呢,我们通过源码一起进行分析学习。

一般分析源码我们都先看它的构造方法是如何初始化的,接着通过对 ThreadLocal 的简单使用,我们知道了关键的两个方法 set/get,所以源码分析也按照这个顺序。

1. 构造方法

泛型类的空参构造,没有什么特别的

2. set 方法源码

源码如下,ThreadLocalMap 是什么呢?由于比较复杂,这里先不做解释,你暂时可以理解为是一个 HashMap,其中 key 为 ThreadLocal 当前对象,value 就是我们设置的值,后面会单独解释源码。

public void set(T value) {    //获取本地线程    Thread t = Thread.currentThread();     //获取当前线程下的threadLocals对象,对象类型是ThreadLocalMap    ThreadLocalMap map = getMap(t);    if (map != null)        //获取到则添加值        map.set(this, value);    else        //否则初始化ThreadLocalMap --第一次设置值        createMap(t, value);}

复制代码

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

复制代码

3. get 方法源码

public T get() {    //获取本地线程    Thread t = Thread.currentThread();     //获取当前线程下的threadLocals对象,对象类型是ThreadLocalMap    ThreadLocalMap map = getMap(t);    if (map != null) {         //通过当前的ThreadLocal作为key去获取对应value        ThreadLocalMap.Entry e = map.getEntry(this);        if (e != null) {            //@SuppressWarnings忽略告警的注解            //"unchecked"表示未经检查的转换相关的警告,通常出现在泛型编程中            @SuppressWarnings("unchecked")            T result = (T)e.value;            return result;        }    }    //threadLocals为空或它的Entry为空时,需要对其进行初始化操作。    return setInitialValue();}

复制代码

private T setInitialValue() {    //初始化为null    T value = initialValue();        //获取当前线程    Thread t = Thread.currentThread();        //获取当前线程下的threadLocals对象,对象类型是ThreadLocalMap    ThreadLocalMap map = getMap(t);    if (map != null)        map.set(this, value);    else        createMap(t, value);        //返回的其实就是个null    return value;}

复制代码

protected T initialValue() {    return null;}

复制代码

4. remove 方法源码

核心也是 ThreadLocalMap 中的 remove 方法,会删除 key 对应的 Entry,具体源码后面统一在 ThreadLocalMap 源码中分析。

public void remove() {    //获取当前线程下的threadLocals对象,对象类型是ThreadLocalMap    ThreadLocalMap m = getMap(Thread.currentThread());    if (m != null)        //通过当前的ThreadLocal作为key调用remove        m.remove(this);}

复制代码

5. ThreadLocalMap 源码

ThreadLocalMap 是 ThreadLocal 的一个静态内部类,看了上面的几个源码解释,可以了解到 ThreadLocalMap 其实才是核心。

简单的说,ThreadLocalMap 与 HashMap 类似,如,初始容量 16,一定范围内扩容,Entry 数组存储等,那它与 HashMap 有什么不同呢,下面将对源码进行详解。

ThreadLocalMap 的底层数据结构:

5.1 常量

//初始容量,一定是2的幂等数。private static final int INITIAL_CAPACITY = 16; // Entry 数组private Entry[] table; //table的长度private int size = 0; //扩容阈值private int threshold;  //设置扩容阈值,长度的 2 / 3private void setThreshold(int len) {    threshold = len * 2 / 3;} //计算下一个存储位置private static int nextIndex(int i, int len) {    return ((i + 1 < len) ? i + 1 : 0);} // 计算前一个存储位置private static int prevIndex(int i, int len) {    return ((i - 1 >= 0) ? i - 1 : len - 1);}

复制代码

5.2 Entry 相关源码

由于 Entry 是底层核心源码,所有的操作几乎都是围绕着它来进行的,所以关于 Entry 的源码会比较多,我一一拆分进行分析讲解。

静态内部类 Entry

这个是 ThreadLocalMap 的底层数据结构,Entry 数组,每个 Entry 对象,这里的 Entry 继承了 WeakReference,关于弱引用不懂得,可以看我的另一篇文章《Java 引用》

然后将 Entry 的 key 设置承了 弱引用,这有什么作用呢?作用是当 ThreadLocal 失去强引用后,在系统 GC 时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉 key,进而 Entry 被内部清理。

//静态内部类Entrystatic class Entry extends WeakReference<ThreadLocal<?>> {    Object value;    Entry(ThreadLocal<?> k, Object v) {        // key为弱引用        super(k);        value = v;    }}

复制代码

获取 Entry

拿到当前线程中对应的 ThreadLocal 所在的 Entry,找不到的话会重新寻找,因为当前的 Entry 可能已经扩容,扩容后会重新计算索引位置,详情见扩容机制源码。

源码中的计算索引位置的算法我没有解释,这个我会放在后面解释,涉及到了如何解决 Hash 冲突的问题,这个和我们熟知的 HashMap 是不同的。

//获取Entryprivate Entry getEntry(ThreadLocal<?> key) {    //计算索引位置    int i = key.threadLocalHashCode & (table.length - 1);    Entry e = table[i];     //找到了就返回Entry    if (e != null && e.get() == key)            return e;    else        //没找到则重新寻找,因为可能发生扩容导致索引重新计算        return getEntryAfterMiss(key, i, e);} //重新获取Entry --从当前索引i的位置向后搜索private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {    Entry[] tab = table;    int len = tab.length;     //循环遍历,获取对应的 ThreadLocal 所在的 Entry    while (e != null) {        //获取Entry对象的弱引用,WeakReference的方法        ThreadLocal<?> k = e.get();        if (k == key)            return e;        if (k == null)            //清除无效 Entry,详解见下方            expungeStaleEntry(i);        else            //计算下一个索引位置            i = nextIndex(i, len);                //可以理解为指针后移        e = tab[i];    }    return null;}

复制代码

清除无效 Entry

expunge 删除,抹去,stale 陈旧的,没有用的

第 1 个方法:根据索引删除对应的桶位,并从给定索引开始,遍历清除无效的 Entry,何为无效?就是当 Entry 的 key 为 null 时,代表 key 已经被 GC 掉了,对应的 Entry 就无效了。

第 2 个方法:删除 Entry 数组中所有无效的 Entry,方法中的e.get() == null,代表 key 被回收了。

第 3 个方法:清除一些失效桶位,它执行对数数量的扫描,向后遍历 logn 个位置,如 8,4,2,1。

方法 2、3 最后都通过方法 1 进行桶位的删除。

//根据索引删除对应的桶位private int expungeStaleEntry(int staleSlot) {    Entry[] tab = table;    int len = tab.length;     //删除该桶位的元素,并将数组长度减1    tab[staleSlot].value = null;    tab[staleSlot] = null;    size--;     Entry e;    int i;    //从当前索引开始,直到当前 Entry为null才会停止遍历    for (i = nextIndex(staleSlot, len);         (e = tab[i]) != null;         i = nextIndex(i, len)) {        //获取Entry对象的弱引用,WeakReference的方法        ThreadLocal<?> k = e.get();        if (k == null) {//说明key已失效            //删除该桶位的元素,并将数组长度减1            e.value = null;            tab[i] = null;            size--;        } else {//说明key有效,需要将其Rehash            //计算rehash后索引位置            int h = k.threadLocalHashCode & (len - 1);            if (h != i) {                tab[i] = null;                //移动元素位置,若rehash后索引位置有其他元素,则继续向后移动,直至为空                while (tab[h] != null)                    h = nextIndex(h, len);                tab[h] = e;            }        }    }    //直到当前 Entry为null才会停止遍历,i为其索引    return i;} //删除Entry数组中所有无效的Entry,用于rehash时private void expungeStaleEntries() {    Entry[] tab = table;    int len = tab.length;    for (int j = 0; j < len; j++) {        Entry e = tab[j];        //获取Entry对象的弱引用,Entry不为空而弱引用为空,代表被GC了        if (e != null && e.get() == null)            //根据索引删除对应的桶位            expungeStaleEntry(j);    }} //清楚一些清除桶位,它执行对数数量的扫描private boolean cleanSomeSlots(int i, int n) {    boolean removed = false;    Entry[] tab = table;    int len = tab.length;    //向后遍历logn个位置,如8,4,2,1    do {        i = nextIndex(i, len);        Entry e = tab[i];        //获取Entry对象的弱引用,Entry不为空而弱引用为空,代表被GC了        if (e != null && e.get() == null) {            n = len;            removed = true;            //根据索引删除对应的桶位            i = expungeStaleEntry(i);        }    } while ( (n >>>= 1) != 0);//对数递减    return removed;}
  • 14
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值