Netty_ThreadLocal和FastThreadLocal详解

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

一. ThreadLocal

1.1 例子

   private static final ThreadLocal threadLocal = new ThreadLocal(){
        @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 可以看出两部分:

  • 一个是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

 private static final FastThreadLocal fastThreadLocal = new FastThreadLocal(){
        @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

从运行结果来看,FastThreadLocalThreadLocal 效果是一样的,那么 FastThreadLocal 的优点在哪里呢?

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

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

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

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

需要注意的点:

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

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

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

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

  • InternalThreadLocalMap0 下标做了特殊处理,用来存放每个线程拥有的 FastThreadLocal 实例集合,当线程退出时,清理这些 FastThreadLocal 实例为当前线程中产生的对象。
  • ThreadLocalMap 没有做这方面的处理,那是因为 ThreadLocalMap 中使用 WeakReference> 来记录 ThreadLocal 实例的。

你可能感兴趣的:(Netty_ThreadLocal和FastThreadLocal详解)