Netty_ThreadLocal和FastThreadLocal详解

在平常开发的时候,经常使用到线程本地变量,这种类型的变量会在每个线程中都有一份,互相不会产生影响,这样来解决多线程并发问题。
那么是如何实现的呢?

一. ThreadLocal<T>

1.1 例子

   private static final ThreadLocal<AtomicInteger> threadLocal = new ThreadLocal<AtomicInteger>(){
        @Override
        protected AtomicInteger initialValue() {
            AtomicInteger result = new AtomicInteger(0);
            System.out.println("创建 AtomicInteger("+result.hashCode()+")   thread:"+Thread.currentThread().getName());
            return result;
        }
    };

    public static void main(String[] args) {

        for (int index = 0; index < 2; index++) {
            new Thread(() -> {
               for (int i = 0; i < 5; i++) {
                   System.out.println(threadLocal.get().incrementAndGet()
                           +"   thread:"+Thread.currentThread().getName());
               }
            }).start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

运行结果

创建 AtomicInteger(952204834)   thread:Thread-0
创建 AtomicInteger(471787139)   thread:Thread-1
1   thread:Thread-0
2   thread:Thread-0
3   thread:Thread-0
4   thread:Thread-0
1   thread:Thread-1
5   thread:Thread-0
2   thread:Thread-1
3   thread:Thread-1
4   thread:Thread-1
5   thread:Thread-1

从运行结果可以得出每个线程都创建了AtomicInteger 实例,因此彼此不会产生影响。

ThreadLocal<T> 可以看出两部分:

  • 一个是ThreadLocal 对象实例(即例子中的 threadLocal),这个实例只有一个,多线程共享的。
  • 另一个是由ThreadLocal 对象实例创建的对象(即例子中的AtomicInteger),这个是每个线程都会创建并持有。

因此你会发现:

  • 每个线程可以根据ThreadLocal 对象实例threadLocal来查找对应的所创建的对象AtomicInteger,相当于key->value 的键值映射关系。
  • 而每个线程可以有多个ThreadLocal 对象实例,即多个key
  • 那么我们可以断定,每个线程肯定有一个集合对象来存储上面的多个key->value 键值映射关系,其实就是 Thread 中成员属性 ThreadLocal.ThreadLocalMap threadLocals

1.2 get 方法

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

    public T get() {
        // 先获取当前线程
        Thread t = Thread.currentThread();
        // 从当前线程中获取存储键值映射关系的Map
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 如果这个Map存在,那么直接从里面获取映射关系e
            ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                // 映射关系e 存在,那么直接获取创建的对象
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 程序走到这里,说明当前线程 这个ThreadLocal 实例对应对象还没有创建,
        // 那么就进行初始化创建
        return setInitialValue();
    }

从当前线程存储的映射关系集合 threadLocals 中,查找当前这个ThreadLocal 对象实例所对应的对象是否存在;存在就返回,不存在就setInitialValue() 方法进行创建。

1.3 setInitialValue() 方法

    private T setInitialValue() {
        // 调用 initialValue() 方法得到初始化值
        T value = initialValue();
        // 先获取当前线程
        Thread t = Thread.currentThread();
        // 从当前线程中获取存储键值映射关系的Map
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null)
            // 存储 key-value 的映射关系
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

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

1.4 ThreadLocalMap

ThreadLocalMap也是用一个哈希表数据结构来储存key-value 的映射关系,只不过它不是用链地址法来解决哈希冲突,而是用开放地址法的 线性探测来解决哈希冲突。

关于哈希表,以及链地址法和开放地址法的原理,在我的这篇文章哈希表 中有全面的介绍。

1.5 小结

ThreadLocal.png

从图中我们就可以知道,每个线程中都一个threadLocals 属性,它的类型是 ThreadLocalMap, 这个 ThreadLocalMap 会记录当前线程所有产生的 ThreadLocal 对象。

二. FastThreadLocal<V>

 private static final FastThreadLocal<AtomicInteger> fastThreadLocal = new FastThreadLocal<AtomicInteger>(){
        @Override
        protected AtomicInteger initialValue() {
            AtomicInteger result = new AtomicInteger(0);
            System.out.println("创建 AtomicInteger("+result.hashCode()+")   thread:"+Thread.currentThread().getName());
            return result;
        }
    };

    public static void main(String[] args) {
        for (int index = 0; index < 2; index++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 5; i++) {
                        System.out.println(fastThreadLocal.get().incrementAndGet()
                                +"   thread:"+Thread.currentThread().getName());
                    }
                }
            }).start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

运行结果

创建 AtomicInteger(125165235)   thread:Thread-1
创建 AtomicInteger(1223947416)   thread:Thread-0
1   thread:Thread-1
1   thread:Thread-0
2   thread:Thread-1
2   thread:Thread-0
3   thread:Thread-1
3   thread:Thread-0
4   thread:Thread-1
5   thread:Thread-1
4   thread:Thread-0
5   thread:Thread-0

从运行结果来看,FastThreadLocal<V>ThreadLocal<T> 效果是一样的,那么 FastThreadLocal<V> 的优点在哪里呢?

从上面的介绍中,我们知道 ThreadLocal<T> 通过哈希表来储存数据,从哈希表查找数据的过程如下:

  • 根据 ThreadLocal<T> 实例对象threadLocal的哈希值,得到对应数组下标。
  • 再比较这个数组下标存储的映射关系entrykey 和实例对象threadLocal是否相等,相等的话,就直接返回entryvalue值。
  • 如果不等,那么就要进行线性探测,查找下一个下标的映射关系entry,是否符合要求,知道找到映射关系entrykey和当前实例对象threadLocal相等。
  • 所以对于这种开发地址法的哈希表,极个别情况下,查找过程可能会耗时,要进行多次线性探测。

那么 FastThreadLocal<V> 就采用了空间换时间的方式加快查找速度。

  • ThreadLocal<T> VS FastThreadLocal<V>
    • ThreadLocal<T> 有一个threadLocalHashCode 属性,在创建的时候被赋值,而且是不可变的属性,代表当前这个 ThreadLocal<T>实例对象的哈希值,用来在哈希表ThreadLocalMap中查找对应的映射关系。
    • FastThreadLocal<V> 有一个 index 属性,在创建的时候被赋值,而且是不可变的属性,这个值就代表当前FastThreadLocal<V>实例对象在 InternalThreadLocalMap 实例的 indexedVariables 的下标,通过这个下标得到FastThreadLocal<V>所创建的当前线程对象。
  • ThreadLocalMapInternalThreadLocalMap
    • ThreadLocalMap 是一个哈希表,用来储存ThreadLocal<T>实例对象和它所创建的当前线程对象的映射关系,就可以通过ThreadLocal<T>实例对象查找它所创建的当前线程对象。
    • InternalThreadLocalMap 就是一个数组,用来存储FastThreadLocal<V>实例对象所创建的当前线程对象,不过存储这个值的数组下标就是FastThreadLocal<V>实例对象的index 属性值。
    • InternalThreadLocalMap 数组下标0 这个位置比较特殊,0 下标存储当前线程所有的 FastThreadLocal<V> 对象实例,用于当前线程销毁时,移除当前线程所有的 FastThreadLocal<V> 对象实例所创建的当前线程对象。
FastThreadLocal.png

需要注意的点:

  • 每个FastThreadLocal 再创建的时候,index 属性就被赋值了,也就是说这个 FastThreadLocal 实例,在每个线程获取的 InternalThreadLocalMap 中的下标是一样的,都是 index

    这就导致一个严重问题,如果 FastThreadLocal 实例较多的话,某一个线程用到了 index 较大FastThreadLocal 实例的话,它必须创建一个很大的数组,这样才能在FastThreadLocal 实例对应下标 index 中储存FastThreadLocal 实例创建的对象。

  • ThreadLocal 没有这个问题,虽然它的哈希值也是创建的时候就确定了,但是它通过 哈希的方法寻找数组下标,那么当前线程中ThreadLocalMap 的数组长度只会和当前线程拥有的ThreadLocal 实例有关。

    这个的问题就是通过哈希查找,效率有点影响。

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

推荐阅读更多精彩内容