ThreadLocal的使用与源码解析详解编程语言

前言

对于ThreadLocal,那是众所周知的,因为在项目中它是常常被使用的,先简单说下平常使用的场景:

  • 设置每个线程所需要的独享数据
  • 设置内存中需要的全局变量(譬如拦截其中的用户信息)

在ThreadLocal类的注释中,作者给的便是这样的定义,如图:

ThreadLocal的使用与源码解析详解编程语言

这段注释大体的意思是说(笔者英语极差):ThreadLocal这个类提供了thread-local变量,这些变量与普通变量不同,每个线程可以根据get或者set方法来操作自己独立初始化的变量副本。ThreadLocal 实例通常是类中的 private static 字段,是要将状态与线程相关联的。

那么上面说的两种使用场景其实都是基于这样的一个定义去做的,这里就第一种场景来提供一下代码实现:

public class SimpleDateFormatterDemo { 
 
    public static ExecutorService executorService = Executors.newFixedThreadPool(10); 
 
    public static void main(String[] args) { 
        for (int i = 0; i< 10; i++) { 
            int j = i; 
            executorService.submit(new Runnable() { 
                @Override 
                public void run() { 
                    String date = new SimpleDateFormatterDemo().date(j); 
                    System.out.println(date); 
                } 
            }); 
        } 
        executorService.shutdown(); 
    } 
 
    public String date(int seconds) { 
        Date date = new Date(1000 * seconds); 
        SimpleDateFormat dateFormat = SafeDateFormatter.dateFormatThreadLocal.get(); 
        return dateFormat.format(date); 
    } 
 
    static class SafeDateFormatter { 
        public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() { 
            @Override 
            protected SimpleDateFormat initialValue() { 
                return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 
            } 
        }; 
    } 
}

二、ThreadLocal的设计

先不多说,直接上ThreadLocal的UML图,如下:

ThreadLocal的使用与源码解析详解编程语言

从上面的图中可以得出,ThreadLocal内部有 SuppliedThreadLocal 和 ThreadLocalMap这两个类,在 ThreadLocalMap 内部还有一个 Entry 内部类,这个类它是一个键值对(键:就是当前的类ThreadLocal,值是实际的成员变量,譬如上面那个示例代码中的SimpleDateFormat),Entry是基于 WeakReference 的,这样的设计也给ThreadLocal带来了一些相应的问题,我们后面再讨论这个。这里的 ThreadLocalMap 其实就是一个定制的 HashMap。

其实ThreadLocal是基于Thread类的,而在Threa中还有一个inheritableThreadLocals(另一个是threadLocals),在默认情况下,每个线程中的这两个变量都为null,只有在当前线程第一次调用ThreadLocal的 set 或 get 方法的时候才会创建他们,每个线程的本地变量并不是放在ThreadLocal实例中的,而是放在 threadLocals 中的,当线程调用get方法的时候,再从当前线程的threadLocals 变量中将其拿出来使用,当不需要的时候在调用remve方法,从当前线程的threadLocals中删除数据。在这里ThreadLocal的作者将Thread的threadLocals设计为map结构,其实是为了使每个线程可以关联多个ThreadLocal变量。

三、ThreadLocalMap源码

在上面说了下ThreadLocalMap的代码实现,这里我们来看看ThreadLocalMap的源码,注:ThreadLocalMap中是采用线性探测法,也就是如果发生冲突,就会继续找下一个空位置,而不是用链表拉链。

成员变量

        // 初始容量,必须是2次幂 
        private static final int INITIAL_CAPACITY = 16; 
 
        // 存放数据的table,Entry类的定义在下面分析,table.length必须是2的冥 
        private Entry[] table; 
 
        // 数组里面entrys的个数,可以用于判断table当前使用量是否超过负因子 
        private int size = 0; 
 
        // 进行扩容的阈值,表使用量大于它的时候进行扩容 
        private int threshold; // Default to 0 
 
        // 定义为长度的2/3 
        private void setThreshold(int len) { 
            threshold = len * 2 / 3; 
        }

存储方式:Entry

/** 
 * Entry继承WeakReference,并且用ThreadLocal作为key.如果key为null 
 * (entry.get() == null)表示key不再被引用,表示ThreadLocal对象被回收 
 * 因此这时候value也可以从table从清除。 
 */ 
static class Entry extends WeakReference<ThreadLocal<?>> { 
    /** The value associated with this ThreadLocal. */ 
    Object value; 
 
    Entry(ThreadLocal<?> k, Object v) { 
        super(k); 
        value = v; 
    } 
}

前面说了这里的Entry是继承自WeakReference这个类,且在Entry的构造函数中,关于key的赋值方式是通过WeakReference去赋值的,而弱引用的特点是如果这个对象只被弱引用关联,那么这个对象就可以被回收,所以弱引用不会阻止GC。ThreadLocalMap的每个Entry都是一个对key的弱引用,同时每个Entry又包含一个对value的强引用,因此在正常的情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用。但是当这个线程一直不终止,那么key对应的value就不能被回收,因此就会产生这样的调用连:Thread——> ThreadLocalMap ——> Entry(key为null)——> Value。因为value和Thread值阿健存在强引用关系,所以会导致value无法回收,以至于可能出现OOM。但是JDK已经帮我们想到了这一点,看源码:

        private void resize() { 
            Entry[] oldTab = table; 
            int oldLen = oldTab.length; 
            int newLen = oldLen * 2; 
            Entry[] newTab = new Entry[newLen]; 
            int count = 0; 
 
            for (int j = 0; j < oldLen; ++j) { 
                Entry e = oldTab[j]; 
                if (e != null) { 
                    ThreadLocal<?> k = e.get(); 
                    if (k == null) { 
                        // 这里判断当前的key为null时,就会把value的值设为null,一次来帮助GC 
                        e.value = null; // Help the GC 
                    } else { 
                        int h = k.threadLocalHashCode & (newLen - 1); 
                        while (newTab[h] != null) 
                            h = nextIndex(h, newLen); 
                        newTab[h] = e; 
                        count++; 
                    } 
                } 
            } 
 
            setThreshold(newLen); 
            size = count; 
            table = newTab; 
        }

四、set方法源码

    public void set(T value) { 
        // 获取当前线程 
        Thread t = Thread.currentThread(); 
        // 将当前线程作为key,去查找对应的线程变量,找到了之后便进行设置 
        ThreadLocalMap map = getMap(t); 
        if (map != null) 
            map.set(this, value); 
        else 
            // 第一次调用便创建当前线程对应的HashMap 
            createMap(t, value); 
    } 
 
    ThreadLocalMap getMap(Thread t) { 
        return t.threadLocals; 
    } 
 
    void createMap(Thread t, T firstValue) { 
        t.threadLocals = new ThreadLocalMap(this, firstValue); 
    }

这段代码很简单,不过在前面有说过,ThreadLocalMap是在第一次调用get()或者set()方法的时候才会进行初始化的。代码如下:

        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { 
            // 初始化 Entry 
            table = new Entry[INITIAL_CAPACITY]; 
            // 计算索引值 
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 
            // 根据索引值进行设置 
            table[i] = new Entry(firstKey, firstValue); 
            size = 1; 
            // 设置阈值 
            setThreshold(INITIAL_CAPACITY); 
        }

这计算索引值的步骤是比较重要的:firstKey.threadLocalHashCode & (INITIAL_CAPACITY – 1),这里firstKey.threadLocalHashCode是基于原子类AtomicInteger,先看具体代码:

    private final int threadLocalHashCode = nextHashCode(); 
 
    private static int nextHashCode() { 
        return nextHashCode.getAndAdd(HASH_INCREMENT); 
    } 
 
 
    private static AtomicInteger nextHashCode = new AtomicInteger(); 
 
    // 基于 unsafe 中的 CAS 
    public final int getAndAdd(int delta) { 
        return unsafe.getAndAddInt(this, valueOffset, delta); 
    } 
 
    private static final int HASH_INCREMENT = 0x61c88647;

关于& (INITIAL_CAPACITY – 1),通过AtomicInteger并与其相关的阈值获得hashCode,然后来进行取模,这里是以2次幂作为模数进行取模,用%代替代替2^n。

 

五、get方法源码

    public T get() { 
        // 获取当前线程 
        Thread t = Thread.currentThread(); 
        // 获取当前线程的 threadLocals变量 
        ThreadLocalMap map = getMap(t); 
        // 如果threadLocals不为null,则返回对应的本地变量的值 
        if (map != null) { 
            ThreadLocalMap.Entry e = map.getEntry(this); 
            if (e != null) { 
                @SuppressWarnings("unchecked") 
                T result = (T)e.value; 
                return result; 
            } 
        } 
        // threadLocals 为 null,则初始化当前线程的threadLocals变量 
        return setInitialValue(); 
    } 
 
    private T setInitialValue() { 
        // 初始化为 null 
        T value = initialValue(); 
        Thread t = Thread.currentThread(); 
        ThreadLocalMap map = getMap(t); 
        // 如果当前线程的 threadLocals 变量不为null 
        if (map != null) 
            map.set(this, value); 
        else 
            // 如果当前线程的 threadLocals 变量为null 
            createMap(t, value); 
        return value; 
    } 
 
    protected T initialValue() { 
        return null; 
    }

结束语

本文是基于源码分析jdk1.8.0_131,本文主要从以下几点进行源码分析的:

  • 简单的说了一下ThreadLocal的设计理念和基本使用
  • 分析了ThreadLocal的源码实现与其实现的意义
  • ThreadLocalMap源码分析
  • set方法源码分析
  • get方法源码分析

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/19403.html

(0)
上一篇 2021年7月19日
下一篇 2021年7月19日

相关推荐

发表回复

登录后才能评论