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内部有 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方法源码分析
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值