热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

深入理解Threadlocal

前言并发是Java开发中绕不开的一个话题。现代处理器都是多核心,想要更好地榨干机器的性能,多线程编程是必不可少,所以,线程安全是每位JavaEngineer的必修课。应对线程安全问
前言

并发是Java开发中绕不开的一个话题。现代处理器都是多核心,想要更好地榨干机器的性能,多线程编程是必不可少,所以,线程安全是每位Java Engineer的必修课。

应对线程安全问题,可大致分为两种方式:

  1. 同步: 用Synchronized关键字,或者用java.util.concurrent.locks.Lock工具类给临界资源加锁。
  2. 避免资源争用: 将全局资源放在ThreadLocal变量中,避免并发访问。

本文将介绍第二种方式:ThreadLocal的实现原理以及为什么能保证线程安全。

ThreadLocal

下面是ThreadLocal的一个常见使用场景:

public class ThreadLocalTest {
    // 一般都将ThreadLocal定义为静态变量
    private static final ThreadLocal format = new ThreadLocal(){
        // 初始化ThreadLocal的值
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static void main(String[] args) {
        // 启动20个线程
        for (int i = 0; i <20; i++) {
            new Thread(() -> {
                try {
                    // 得到SimpleDateFormat在本线程中的副本
                    DateFormat localFormat = format.get();
                    // 解析日期,这里并不会报错
                    Date date = localFormat.parse("2000-11-11 11:11:11");
                    System.out.println(date);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

大家应该都知道,Java中SimpleDateFormat不是线程安全的,参考这篇文章。然而上述代码的确不会报错,说明ThreadLocal确实能保证并发安全。

源码解析

ThreadLocal概览

上面的例子中,我们调用了ThreadLocalinitialValueget方法,且来看一下get方法的实现:

// 此类的作者是两个大神,前者是《Effective Java》的作者,后者是Java并发包的作者,并发大师
/*
 * [@author](https://my.oschina.net/arthor)  Josh Bloch and Doug Lea
 * [@since](https://my.oschina.net/u/266547)   1.2
 */
public class ThreadLocal {
    public T get() {
        // 得到当前线程
        Thread t = Thread.currentThread();
        // 根据当前线程,拿到一个Map,暂且可以将之类比为HashMap键值对形式
        // 可见这个Map是与本线程相关的
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 通过this从Map中拿Entry,说明Map中的Key就是ThreadLocal变量本身
            // value就是ThreadLocal中所保存的对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 若Map没有初始化(map == null),或者当前ThreadLocal变量没有初始化(e == null)
        // 则调用此方法完成初始化
        return setInitialValue();
    }

    // 原来,这个ThreadLocalMap只是线程的一个成员变量!
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
}

public class Thread implements Runnable {
    // Thread类中定义了一个全局变量ThreadLocalMap
    // 用来存放本线程中所有的ThreadLocal类型变量,初始值为null
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

get方法,我们可以得到如下信息: > 1. ThreadLocal变量保存在一个Map中,而这个Map正是Thread类的一个全局变量。这也是ThreadLocal实现线程安全的一个关键点:各个线程都有自己的Map,每个线程操作的都是自己的ThreadLocal变量副本,互不影响。 > 2. ThreadLocalMap保存线程中所有的ThreadLocal变量,ThreadLocal变量是Key,ThreadLocal所对应的值为Value。(在本文开始的例子中,Key为format变量,Value为initialValue方法返回的值new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") > 3. ThreadLocal是懒加载的,当发现ThreadLocalMap或者当前ThreadLocal变量未初始化时,会调用setInitialValue方法进行初始化。

ThreadLocal

ThreadLocal其他方法

继续来看setInitialValue方法做了什么事情:

    private T setInitialValue() {
        // 调用initialValue方法初始化
        // 这个方法即为我们定义ThreadLocal变量的时候重写的方法
        T value = initialValue();
        Thread t = Thread.currentThread();
        // 获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // 如果Map已经初始化好了,那直接初始化当前ThreadLocal变量:
            // 将自己(当前ThreadLocal变量)作为key,保存的值作为value,set到Map里面去
            map.set(this, value);
        else
            // 如果Map还未初始化,则初始化Map
            createMap(t, value);
        return value;
    }

    // 默认的initialValue方法定义为protected,就是给我们重写的
    protected T initialValue() {
        return null;
    }

    void createMap(Thread t, T firstValue) {
        // 新建一个ThreadLocalMap,赋值给当前Thread
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

其他还有setremove方法,很简单这里不另外讲解。

难道ThreadLocal就此结束了么?有这么简单么?当然没有。因为ThreadLocalMapThread的一个成员变量,所以它的生命周期跟线程是一样长的。也就是说,只要线程还没有被销毁,那么Map就会常驻内存,无法被GC,很容易造成内存泄漏。那ThreadLocal是如何解决的呢?

答案是弱引用,Java中的引用类型,可以参考这篇文章。

ThreadLocalMap

ThreadLocalMapThreadLocal的一个内部类。Java中有现成的类HashMap,而ThreadLocal又费劲千辛万苦自己实现了一个ThreadLocalMap,就是为防止内存泄漏。

下面我们来探秘ThreadLocalMap,它跟普通的HashMap有什么区别。

ThreadLocalMap的数据结构

static class ThreadLocalMap {

    // 内部类Entry继承了WeakReference
    static class Entry extends WeakReference> {
        // ThreadLocal变量中保存的值
        Object value;

        // 可以看到,Entry只是简单的Key-Value,并没有类似HashMap中的链表
        Entry(ThreadLocal k, Object v) {
            super(k);
            value = v;
        }
    }
    // ThreadLocalMap默认大小
    private static final int INITIAL_CAPACITY = 16;
    // 此Entry数组,就是所有ThreadLocal存放的地方
    private Entry[] table;
}

ThreadLocalMap维护了一个Entry数组(没有链表,这是跟HashMap不一样的地方),用来存放线程中所有的ThreadLocal变量。Entry继承了WeakReference,并关联了ThreadLocal,当外界没有其他强引用指向ThreadLocal对象时,该ThreadLocal对象会在下一次GC时被内存回收,也就是Entry中的Key会被回收掉,所以下面会看到清理key为null的Entry的操作。

Set操作

HashMap遇到哈希冲突的时候,是通过在同一个Hash Key上建立链表来解决的。既然ThreadLocalMap只维护了一个Entry数组,那它是怎么解决哈希冲突的呢?我们来看set方法的源码:

    private void set(ThreadLocal key, Object value) {
        Entry[] tab = table;
        int len = tab.length;
        // 根据ThreadLocal的hashcode,计算出在table中的槽位(index)
        int i = key.threadLocalHashCode & (len-1);
        // 从位置i开始,逐个往后循环,找到第一个空的槽位(条件e == null)
        for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
            ThreadLocal k = e.get();
            // 如果key相等,则直接将旧value覆盖掉,换成新value
            if (k == key) {
                // 新值替换掉旧值,并return掉
                e.value = value;
                return;
            }
            // key == null,说明弱引用之前已经被内存回收,则将值设在此槽位
            if (k == null) {
                // 该方法后面再解析
                replaceStaleEntry(key, value, i);
                return;
            }
        }

        // 走到这里,这个i 是从key真正所在的hash槽之后数,第一个非空槽位
        // 将value包装成Entry,放到位置i中
        tab[i] = new Entry(key, value);
        int sz = ++size;
        // 查找是否有Entry已经被回收
        // 如果找到有Entry被回收,或者table的size大于阈值,执行rehash操作
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
    
    // 获取下一个index。其实就是i + 1。当超出table长度的时候,归0重新来
    private static int nextIndex(int i, int len) {
        return ((i + 1 

ThreadLocalMap是用开放地址发来解决哈希冲突的。如果目标槽位已经有值了,首先判断该值是不是就是自己。如果是,那就替换旧值;如果不是,再判断该槽位的值是否有效(槽位上的ThreadLocal变量有没有被垃圾回收),如果无效,则直接设置在该槽位,并执行一些清理操作。如果该槽位上是一个有效的值,那么往后继续寻找,直到找到空槽位为止。流程大概如下:

ThreadLocalMap

清理无效的Entry

到这里,我们应该带着一个疑问:弱引用清除的只是Entry中的key,也就是ThreadLocal变量,而Entry本身依然占据着table中的槽位。那代码中是在哪里清理这些无效的Entry的呢?我们重点看一下上面没有分析的两个方法replaceStaleEntrycleanSomeSlots

cleanSomeSlots

    // 顾名思义,清除部分槽位,默认扫描log(n)个槽位
    private boolean cleanSomeSlots(int i, int n) {
        boolean removed = false;
        Entry[] tab = table;
        int len = tab.length;
        do {
            i = nextIndex(i, len);
            Entry e = tab[i];
            // 注意无效Entry的判断条件是,e.get() == null
            // 即Entry中保存的弱引用已经被GC,这种情况需要将对应Entry清除
            if (e != null && e.get() == null) {
                // 如果发现有无效entry,那n会重新设置为table的长度
                // 即会继续查找log(n)个槽位,判断有没有无效Entry
                n = len;
                removed = true;
                // 调用expungeStaleEntry方法清除i位置的槽位
                i = expungeStaleEntry(i);
            }
        // 循环条件为n右移一位,即除以2。所以默认是循环log(n)次
        } while ( (n >>>= 1) != 0);
        // 如果有槽位被清除,返回true
        return removed;
    }

    private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
        // 将i位置的槽位置为空
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;

        Entry e;
        int i;
        // 继续往后检查是否有无效Entry,直到遇到空的槽位tab[i]==null为止
        for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
            ThreadLocal k = e.get();
            // 如果Entry无效,将其清除
            if (k == null) {
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                // 重新计算hash值h
                int h = k.threadLocalHashCode & (len - 1);
                // 如果新hash值h不等于当前位置的槽位值i,这种情况需要rehash
                // 给当前i位置的e,重新找更合理的槽位
                if (h != i) {
                    // 将i位置置空
                    tab[i] = null;
                    // 从h位置往后找第一个空槽位
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    // 将e放在第一个空槽位上
                    tab[h] = e;
                }
            }
        }
        // 返回接下来第一个空槽位的下标
        return i;
    }

cleanSomeSlots方法会扫描部分的槽位,查看是否有无效的Entry。如果没有找到,那么只扫描log(n)个槽位;如果有找到无效槽位,则会清除该槽位,并额外再扫描log(n)个槽位,以此类推。 清空槽位的工作是expungeStaleEntry方法做的,除了清除当前位置的Entry之外,它还会检查往后连续的非空Entry,并清除其中无效值。同时还会判断并处理rehash。这里为什么要rehash?因为前面有无效Entry被清除掉了,如果后面的Entry是因为hash冲突而被延到后面的,就可以把后面的Entry移到前面空出来的位置上,从而提高查询效率。

cleanSomeSlots举例

CleanSomeSlots Example

上图的情况,我们分两种情况讨论:

  • 如果从i=2开始找:

    1. tab[2]所在位置为null,继续循环i=nextIndex(i, len)=nextIndex(2, 8)=3
    2. tab[3]所在位置(k3,v3)有效,继续循环i=nextIndex(i, len)=nextIndex(3, 4)=0
    3. tab[0]所在位置(k1,v1)有效,继续循环i=nextIndex(i, len)=nextIndex(0, 2)=1
    4. tab[1]所在位置(k2,v2)有效,继续循环i=nextIndex(i, len)=nextIndex(1, 1)=0
    5. tab[0]所在位置(k1,v1)有效,n==0结束
  • 如果从i=11开始找:

    1. tab[11]所在位置(null,v7)无效,调用expungeStaleEntry方法,expungeStaleEntry方法清空tab[11],并会往后循环判断。因为tab[12]位置(null,v8)无效,所以tab[12]也会被清空;tab[13]位置(k9,v9)有效,则会判断是否需要给(k9,v9)重新放位置。如果对k9执行rehash之后依然是12,则不作处理;如果对k9执行rehash之后是11,说明该元素是因为hash碰撞被放到了12的位置,那么需要把元素放到tab[11]的位置。expungeStaleEntry方法返回第一个为null的下标14,n重新设置为16,i=nextIndex(i, len)=nextIndex(14, 16)=15
    2. tab[15]所在位置(k10,v10)有效,继续循环i=nextIndex(i, len)=nextIndex(15, 8)=0
    3. tab[0]所在位置(k1,v1)有效,继续循环i=nextIndex(i, len)=nextIndex(0, 2)=1
    4. tab[1]所在位置(k2,v2)有效,继续循环i=nextIndex(i, len)=nextIndex(1, 1)=0
    5. tab[0]所在位置(k1,v1)有效,n==0结束

replaceStaleEntry方法

    private void replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
        Entry e;
        // slotToExpunge记录了包含staleSlot的连续段上,第一个无效Entry的下标
        int slotToExpunge = staleSlot;
        // 往前遍历非空槽位,找到第一个无效Entry的下标,记录为slotToExpunge
        for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
            if (e.get() == null)
                slotToExpunge = i;
        // 往后遍历非空段,查找key所在的位置,即检查key之前是否之前已经被添加过
        // 为什么到tab[i]==null为止?因为空的槽之后的hash值肯定已经不一样
        for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
            ThreadLocal k = e.get();
            if (k == key) {
                // 如果找到了key,那么说明此key之前已经添加过,直接覆盖旧值
                // 因为staleSlot小于i,需要将两个槽位的值进行交换,以提高查询效率
                // 而被换到i处的无效Entry,会在之后的cleanSomeSlots被清除掉
                e.value = value;
                tab[i] = tab[staleSlot];
                tab[staleSlot] = e;
                // 如果slotToExpunge的值并没有变,说明往前查找的过程中并未发现无效Entry
                // 那么以当前位置作为cleanSomeSlots的起点
                if (slotToExpunge == staleSlot)
                    slotToExpunge = i;
                // 这两个方法都已经分析过,从slotToExpunge位置开始清理无效Entry
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                return;
            }

            // 如果前面往前查找没有发现无效Entry,且此处的Entry无效(k==null)
            // 那么将说明i处是第一个无效Entry,将slotToExpunge计为i
            if (k == null && slotToExpunge == staleSlot)
                slotToExpunge = i;
        }
        // 如果key没有找到,说明这是一个新Entry,那么直接新建一个Entry放在staleSlot位置
        tab[staleSlot].value = null;
        tab[staleSlot] = new Entry(key, value);
        if (slotToExpunge != staleSlot)
            // 这两个方法都已经分析过,从slotToExpunge位置开始清理无效Entry
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }

这个方法其实就是三个步骤:

  1. 往后查找该key在table中是否存在。如果存在,即之前已经set过该key,那么需要覆盖掉旧值,并且将key所在元素移到staleSlot位置。(为什么要移位置?因为原元素所在的位置i,肯定在staleSlot之后,所以将元素往前放到staleSlot上可以提高查询效率,并避免后续的rehash操作。)
  2. 如果key不存在,说明是新set的操作,直接新建Entry,放在staleSlot位置。
  3. 调用cleanSomeSlots方法,清除无效的Entry

其他方法

剩下的方法都比较简单,解析见源码注释,不另外解释 get方法:

    // get操作的方法
    private Entry getEntry(ThreadLocal key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        // i位置元素即为要找的元素,直接返回
        if (e != null && e.get() == key)
            return e;
        else
            // 否则调用getEntryAfterMiss方法
            return getEntryAfterMiss(key, i, e);
    }

    private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
        // 从i位置开始,往后遍历查找,直到空槽位为止。为什么到空槽位为止?
        // 根据开地址法,空槽位之后的元素hash值肯定已经不一样,没必要再继续
        while (e != null) {
            ThreadLocal k = e.get();
            // key相等,这就是目标元素,直接返回
            if (k == key)
                return e;
            // key为null,则是无效元素,调用expungeStaleEntry方法清除i位置的元素
            if (k == null)
                expungeStaleEntry(i);
            else
                // 继续寻找下一个元素
                i = nextIndex(i, len);
            e = tab[i];
        }
        // 没有找到目标元素,返回null
        return null;
    }

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; e = tab[i = nextIndex(i, len)]) {
            // 找到目标元素
            if (e.get() == key) {
                e.clear();
                // 调用expungeStaleEntry方法清除i位置的元素
                expungeStaleEntry(i);
                return;
            }
        }
    }

resize方法

    // 当元素个数大于threshold(默认是table长度的2/3)时,需要resize
    private void resize() {
        Entry[] oldTab = table;
        int oldLen = oldTab.length;
        // 新table长度是旧table的2倍
        int newLen = oldLen * 2;
        Entry[] newTab = new Entry[newLen];
        int count = 0;
        // 遍历旧table
        for (int j = 0; j 
总结

本文从代码层面,深入介绍了ThreadLocal的实现原理。 ThreadLocal可以保证线程安全,是因为它给为每个线程都创建了一个变量的副本。每个线程访问的都是自己内部的变量,不会有并发冲突。 作为线程内部变量,它跟局部变量有什么区别呢?一般ThreadLocal都被定义为static,也就是说,每个线程只需要创建一份,生命周期跟线程一样。而局部变量生命周期跟方法与方法一样,每调用一次方法,创建一次变量,方法结束,对象销毁。ThreadLocal可以避免一些大对象的重复创建销毁。

ThreadLocalMapEntry继承自WeakReference,当没有其他的强引用指向ThreadLocal变量时,该ThreadLocal变量会在下次GC中被回收。对于被回收掉的ThreadLocal变量,不会显式地去清理,而是在接下来的getsetremove操作中去检查删除掉这些无效ThreadLocal变量所在的Entry,防止可能的内存泄漏。


推荐阅读
  • 本文详细探讨了Java集合框架的使用方法及其性能特点。首先,通过关系图展示了集合接口之间的层次结构,如`Collection`接口作为对象集合的基础,其下分为`List`、`Set`和`Queue`等子接口。其中,`List`接口支持按插入顺序保存元素且允许重复,而`Set`接口则确保元素唯一性。此外,文章还深入分析了不同集合类在实际应用中的性能表现,为开发者选择合适的集合类型提供了参考依据。 ... [详细]
  • 2019年后蚂蚁集团与拼多多面试经验详述与深度剖析
    2019年后蚂蚁集团与拼多多面试经验详述与深度剖析 ... [详细]
  • 本项目在Java Maven框架下,利用POI库实现了Excel数据的高效导入与导出功能。通过优化数据处理流程,提升了数据操作的性能和稳定性。项目已发布至GitHub,当前最新版本为0.0.5。该项目不仅适用于小型应用,也可扩展用于大型企业级系统,提供了灵活的数据管理解决方案。GitHub地址:https://github.com/83945105/holygrail,Maven坐标:`com.github.83945105:holygrail:0.0.5`。 ... [详细]
  • 在处理高并发场景时,确保业务逻辑的正确性是关键。本文深入探讨了Java原生锁机制的多种细粒度实现方法,旨在通过使用数据的时间戳、ID等关键字段进行锁定,以最小化对系统性能的影响。文章详细分析了不同锁策略的优缺点,并提供了实际应用中的最佳实践,帮助开发者在高并发环境下高效地实现锁机制。 ... [详细]
  • 如何在 Java LinkedHashMap 中高效地提取首个或末尾的键值对? ... [详细]
  • 全面解析Java虚拟机:内存模型深度剖析 ... [详细]
  • PHP中元素的计量单位是什么? ... [详细]
  • 池子比率:BSV 区块链上的去中心化金融应用——Uniswap 分析
    池子比率:BSV 区块链上的去中心化金融应用——Uniswap 分析 ... [详细]
  • 本文详细解析了如何使用 jQuery 实现一个在浏览器地址栏运行的射击游戏。通过源代码分析,展示了关键的 JavaScript 技术和实现方法,并提供了在线演示链接供读者参考。此外,还介绍了如何在 Visual Studio Code 中进行开发和调试,为开发者提供了实用的技巧和建议。 ... [详细]
  • 本题库精选了Java核心知识点的练习题,旨在帮助学习者巩固和检验对Java理论基础的掌握。其中,选择题部分涵盖了访问控制权限等关键概念,例如,Java语言中仅允许子类或同一包内的类访问的访问权限为protected。此外,题库还包括其他重要知识点,如异常处理、多线程、集合框架等,全面覆盖Java编程的核心内容。 ... [详细]
  • 在稀疏直接法视觉里程计中,通过优化特征点并采用基于光度误差最小化的灰度图像线性插值技术,提高了定位精度。该方法通过对空间点的非齐次和齐次表示进行处理,利用RGB-D传感器获取的3D坐标信息,在两帧图像之间实现精确匹配,有效减少了光度误差,提升了系统的鲁棒性和稳定性。 ... [详细]
  • 深入解析Gradle中的Project核心组件
    在Gradle构建系统中,`Project` 是一个核心组件,扮演着至关重要的角色。通过使用 `./gradlew projects` 命令,可以清晰地列出当前项目结构中包含的所有子项目,这有助于开发者更好地理解和管理复杂的多模块项目。此外,`Project` 对象还提供了丰富的配置选项和生命周期管理功能,使得构建过程更加灵活高效。 ... [详细]
  • Java 9 中 SafeVarargs 注释的使用与示例解析 ... [详细]
  • 深入解析Java中HashCode的功能与应用
    本文深入探讨了Java中HashCode的功能与应用。在Java中,HashCode主要用于提高哈希表(如HashMap、HashSet)的性能,通过快速定位对象存储位置,减少碰撞概率。文章详细解析了HashCode的生成机制及其在集合框架中的作用,帮助开发者更好地理解和优化代码。此外,还介绍了如何自定义HashCode方法以满足特定需求,并讨论了常见的实现误区和最佳实践。 ... [详细]
  • Understanding the Distinction Between decodeURIComponent and Its Encoding Counterpart
    本文探讨了 JavaScript 中 `decodeURIComponent` 和其编码对应函数之间的区别。通过详细分析这两个函数的功能和应用场景,帮助开发者更好地理解和使用它们,避免常见的编码和解码错误。 ... [详细]
author-avatar
Th川_546
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有