ThreadLocal 到底是什么?我们来一探究竟

一、前言


对一个事务的认知是一个递进的过程。在了解ThreadLocal时,需要注意以下几点:

  • 什么是ThreadLocal? ThreadLocal出现的背景是什么?解决了什么问题?
  • ThreadLocal的使用方法是什么?使用的效果如何?
  • ThreadLocal是如何实现它的功能的,即ThreadLocal的原理是什么?

二、背景


??????在一个分布式系统中,多个线程同时访问同一类实例中的某个变量a,由于变量a是线程共享的,导致一个线程对变量a进行修改,其他线程读到的都是修改后的变量a的值(如果是存在多个线程同时写,需要加分布式锁,限制同时只能一个线程对其进行修改)。这种情况在普通的场景下是合理的,比如在电商系统中,买家点击支付订单两次(两个独立的线程),第一次生成订单,会修改幂等值(防止第二次重复下单),第二次访问的时候去判断幂等值,如果已被修改,则不会重新生成订单。所以线程间变量共享是必须的。
??????但存在这样一个场景:还是以电商系统为例。买家在访问订单详情页的时候,在不同的条件下会查订单(查数据库)。查库涉及io,对系统的开销和响应时间有较大的影响。由于订单详情页的渲染都是一些读操作,没有写操作,所以,需要在查数据库时做一层本地缓存。而且这个本地缓存是对线程敏感的,只在当前线程生效,别的线程无法访问这个缓存,也就是说线程间是隔离的。
??????上面只是以电商为例, 所以需要一种方式,能够实现变量的线程间隔离,此变量只能在当前线程生效,不同的线程变量有不同的值。基于以上诉求,java诞生了ThreadLocal,主要是为了解决内存的线程隔离。

三、使用方式


3.1 测试代码

  • 线程类
public class NormalThread implements Runnable {

    private int shareValue = 0;
    ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();

    @Override
    public void run() {

        shareValue += 1;
        threadLocalValue.set(shareValue);
        System.out.println(Thread.currentThread().getName() + "===== shareValue:" + shareValue + "   threadLocal:" + threadLocalValue.get());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "===== shareValue:" + shareValue + "   threadLocal:" + threadLocalValue.get());
    }
}
  • 线程池
public class MutiThreadUtil {

    private static int core_pool_size = 4;
    private static int max_pool_size = 10;
    //如果空闲立即退出
    private static long keep_alive_time = 0L;
    //队列的容量是0
    private static BlockingQueue queue = new SynchronousQueue();
    //队列容量为1
    private static ArrayBlockingQueue<Integer> arrayBlockingQueue = new ArrayBlockingQueue(1);

    public static ExecutorService initThreadPool() {
        ExecutorService executorService = new ThreadPoolExecutor(
            core_pool_size, max_pool_size, keep_alive_time,TimeUnit.SECONDS,queue
        );
        return executorService;
    }
}

  • 主线程
public class ThreadLocalTest {

    public static void main(String[] args) {

        NormalThread normalThread = new NormalThread();
        ExecutorService executorService = MutiThreadUtil.initThreadPool();
        for (int i = 0; i < 10; i ++) {
            executorService.execute(normalThread);
        }
        executorService.shutdown();
    }
}

3.2 结果分析

pool-1-thread-1===== shareValue:1   threadLocal:1
pool-1-thread-2===== shareValue:2   threadLocal:2
pool-1-thread-3===== shareValue:3   threadLocal:3
pool-1-thread-8===== shareValue:4   threadLocal:4
pool-1-thread-4===== shareValue:5   threadLocal:5
pool-1-thread-5===== shareValue:6   threadLocal:6
pool-1-thread-6===== shareValue:7   threadLocal:7
pool-1-thread-7===== shareValue:8   threadLocal:8
pool-1-thread-9===== shareValue:9   threadLocal:9
pool-1-thread-10===== shareValue:10   threadLocal:10
pool-1-thread-3===== shareValue:10   threadLocal:3
pool-1-thread-2===== shareValue:10   threadLocal:2
pool-1-thread-1===== shareValue:10   threadLocal:1
pool-1-thread-8===== shareValue:10   threadLocal:4
pool-1-thread-7===== shareValue:10   threadLocal:8
pool-1-thread-4===== shareValue:10   threadLocal:5
pool-1-thread-5===== shareValue:10   threadLocal:6
pool-1-thread-9===== shareValue:10   threadLocal:9
pool-1-thread-10===== shareValue:10   threadLocal:10
pool-1-thread-6===== shareValue:10   threadLocal:7

以线程pool-1-thread-1线程(后面简称线程1)作为分析,刚开始 线程1的shareValue 和 threadlocal 值均为1, shareValue是共享变量,在线程1 sleep阶段,线程2-10均执行了以下代码

 shareValue += 1;
 threadLocalValue.set(shareValue);

但从sleep后的打印结果来看,线程1只是更改了shareValue10的值,变为10, 而threadlocal的值没有变,还是1,这说明threadlocal的值是线程级的,是线程的私有空间,不会因为其他线程的改变而改变。证明了thread的线程隔离性。

四、ThreadLocal 原理


在不看源码之前,我们思考下如果让我们设计这样一个工具类,能够使得线程间的变量相互隔离,我们会怎样设计?
??????每一个线程,其执行均是依靠Thread类的实例的start方法来启动线程,然后CPU来执行线程。每一个Thread类的实例的运行即为一个线程。若要每个线程(每个Thread实例)的变量空间隔离,则需要将这个变量的定义声明在Thread这个类中。这样,每个实例都有属于自己的这个变量的空间,则实现了线程的隔离。事实上,ThreadLocal的源码也是这样实现的。

4.1 实现内存线程间隔离的原理

  1. 在Thread类中声明一个公共的类变量ThreadLocalMap,用以在Thread的实例中预占空间
ThreadLocal.ThreadLocalMap threadLocals = null;
  1. 在ThreadLocal中创建一个内部类ThreadLocalMap,这个Map的key是ThreadLoca对象,value是set进去的ThreadLocal中泛型类型的值
private void set(ThreadLocal<?> key, Object value) {...}
  1. 在new ThreadLocal时,只是简单的创建了个ThreadLocal对象,与线程还没有任何关系
  2. 真正产生关系的是在向ThreadLocal对象中set值得时候
  • 首先从当前的线程中获取ThreadLocalMap,如果为空,则初始化当前线程的ThreadLocalMap
  • 然后将值set到这个Map中去,如果不为空,则说明当前线程之前已经set过ThreadLocal对象了。
    这样用一个ThreadHashMap来存储当前线程的若干个可以线程间隔离的变量,key是ThreadLocal对象,value是要存储的值(类型是ThreadLocal的泛型)
 public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
  1. 从ThreadLocal中获取值 :还是先从当前线程中获取ThreadLocalMap,然后使用ThreadLocal对象(key)去获取这个对象对应的值(value)
 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();
    }

到这里,如果仅仅是理解ThreadLocal是如何实现的线程级别的隔离已经完全足够了。简单的讲,就是在Thread的类中声明了ThreadLocalMap这个类,然后在使用ThreadLocal对象set值的时候将当前线程(Thread实例)进行map初始化,并将Threadlocal对应的值塞进map中,下次get的时候,也是使用这个ThreadLcoal的对象(key)去从当前线程的map中获取值(value)就可以了

4.2 ThreadLocalMap的深究

从源码上看,ThreadLocalMap虽然叫做Map,但和我们常规理解的Map不太一样,因为这个类并没有实现Map这个接口,只是定义在ThreadLocal中的一个静态内部类。只是因为在存储的时候也是以key-value的形式作为方法的入参暴露出去,所以称为map。

static class ThreadLocalMap {...}
  1. ThreadLocalMap的创建,在使用ThreadLocal对象set值的时候,会创建ThreadLocalMap的对象,可以看到,入参就是KV,key是ThreadLocal对象,value是一个Entry对象,存储kv(HashMap是使用Node作为KV对象存储)。Entry的key是ThreadLocal对象,vaule是set进去的具体值。
 void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  1. 继续看看在创建ThreadLocalMap实例的时候做了什么?其实ThreadLocalMap存储是一个Entry类型的数组,key提供了hashcode用来计算存储的数组地址(散列法解决冲突)
  • 创建Entry数组(初始容量16)
  • 然后获取到key(ThreadLocal对象)的hashcode(是一个自增的原子int型)
private final int threadLocalHashCode = nextHashCode();

private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
private static AtomicInteger nextHashCode =    new AtomicInteger();
  • 使用【hashcode 模(%) 数组长度】的方式得到要将key存储到数组的哪一位。
  • 设置数组的扩容阈值,用以后续扩容
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

创建ThreadLcoalMap对象只有在当前线程第一次插入kv的时候发生,如果是第二次插入kv,则会进行第三步

  1. 这个set的过程其实就是根据ThreadLocal的hashcode来计算存储在Entry数组的位置
  • 利用ThreadLocal的【hashcode 模(%) 数组长度】的方式获取存储在数组的位置
  • 如果当前位置已存在值,则向右移一位,如果也存在值,则继续右移,直到有空位置出现为止
  • 将当前的value存储上面两部得到的索引位置(上面这两步就是散列法的实现)
  • 校验是否扩容,如果当前数组的中存储的值得数量大于阈值(数组长度的2/3),则扩容一倍,并将原来的数组的值重新hash至新数组中(这个过程其实就是HashMap的扩容过程)
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)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

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

4.3 ThreadLocalMap和HashMap的比较

  1. 上述的整个过程其实和HashMap的实现方式很相像,相同点:
  • 两个map都是最终用数组作为存储结构,使用key做索引,value是真正存储在数组索引上的值。
  1. 不同点:解决key冲突的方式
  • map解决冲突的方式不一样,HashMap采用链表法,ThreadLocalMap采用散列法(又称开放地址法)

思考:为什么不采用HashMap作为ThreadLocal的存储结构?
个人理解:

  • 引入链表,徒增了数据结构的复杂度,并且链表的读取效率较低
  • 更加灵活。包括方法的定义和数组的管理,更加适合当前场景
  • 不需要HashMap的额外的很多方法和变量,需要一个更加纯粹和干净map,来存储自己需要的值,减少内存的损耗。

4.4 ThreadLocal的生命周期

ThreadLocal的生命周期和当前Thread的生命周期强绑定

  1. 正常情况
    正常情况下(当然会有非正常情况),在线程退出的时候会将threadLocals这个变量置为null,等待JVM去自动回收。

注意:Thread这个方法只是用以系统能够显示的调用退出线程,线程在结束的时候是不会调用这个方法,启动的线程是非守护线程,会在线程结束的时候由jvm自动进行空间的释放和回收。

private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        /* Speed the release of some of these resources */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }
  1. 非正常情况

由于现在多线程一般都是由线程池管理,而线程池的线程一般都是复用的,这样会导致线程一直存活,而如果使用ThreadLocal大量存储变量,会使得空间开始膨胀

  1. 启发
    需要自己来管理ThreadLocal的生命周期,在ThreadLocal使用结束以后及时调用remove()方法进行清理。

五、注意事项


  • 注意管理ThreadLocal的声明周期,及时调用remove方法进行空间释放
  • 注意ThreadLocal的使用方式,如果在使用中发现没有获取到预期的值,只能是自己的使用方式不对,导致获取的不是同一线程下的ThreadLocal值。
最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容

  • 01/ 十一月份的北方已经进入了大冬季,屋内暖气送的极其的旺,屋外寒风萧瑟,落叶散落了满地,北方的冬季就是这样萧瑟...
    治愈里阅读 482评论 1 2
  • 协会换届在即,作为15级的老学姐,有一番话想说给16级的各位部长们。 亲爱小石榴们: 你们好。 转眼,我们相处已经...
    理智疯子阅读 249评论 0 2
  • 前几天我的一个朋友和他男朋友闹分手,我这个朋友跑过来问我,“如果你的男朋友对所有女生都一样好,都一样贴心,然后他们...
    冰月贰玖阅读 272评论 0 1