ThreadLocal 源码分析

ThreadLocal 提供线程局部的变量,即每个线程都有同一个变量的独有拷贝,对 ThreadLocal 设置值每个线程的值都是独立的。

一、 ThreadLocal 基本用法

static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 100);

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            System.out.println("thread1 thread initial: " + threadLocal.get());
            threadLocal.set(200);
            System.out.println("thread1 thread final: " + threadLocal.get());
        });
        Thread thread2 = new Thread(() -> {
            System.out.println("thread2 thread initial: " + threadLocal.get());
            threadLocal.set(300);
            System.out.println("thread2 thread final: " + threadLocal.get());
        });
        threadLocal.set(400);
        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();
        System.out.println("main thread final: " + threadLocal.get());
}

上面例子调用了 ThreadLocal 的静态方法 withInitial 创建了一个变量,提供了一个初始值100,不提供初始值默认返回 null,打印结果如下:

thread1 thread initial: 100
thread1 thread final: 200
thread2 thread initial: 100
thread2 thread final: 300
main thread final: 400

从打印结果可以说明,每个线程都有自己独立的值,thread1 对 threadLocal 设置值不会影响其它线程的值,虽然访问的都是同一个变量 threadLocal ,这就是 threadLocal 的作用。

二、 ThreadLocal 源码分析

我们首先看看构造方法:

 public ThreadLocal() {
 }

空方法,走不通,我们看看 set 和 get方法:

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}

首先获取当前线程,然后调用getMap(t) 获取ThreadLocalMap, 如果 map 不为null,就用当前ThreadLocal为key 存放 value,为空则创建一个ThreadLocalMap 给到当前线程。 从这里可以看出,ThreadLocalMap 才是我们实际存放值的地方。

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

直接返回线程的实例变量threadLocals,

  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();
}

获取当前线程然后获取ThreadLocalMap,如果 map 不为 null 则以ThreadLocal为 key 获取值并返回,如果为 null 则返回设置的初始值:

 private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
}

 protected T initialValue() {
        return null;
    }

此方法主要是返回默认值并存放,后面的逻辑跟 set 是一样的,默认值我们可以重写 initialValue() 方法,如果没有重写默认返回null。

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
}

此方法主要是以ThreadLocal 为 key 移除存放值

接下来我们看看存放值的ThreadLocalMap

//初始容量,必须是2的幂,
private static final int INITIAL_CAPACITY = 16;
//存储数据的数组
private Entry[] table;
//存储数据的容量
private int size = 0;
//阈值,超过该阈值就会扩容
private int threshold; // Default to 0

从构造方法入手看看

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);
}

首先实例化了一个 Entry数组, Entry 专门用来保存键值对,然后计算了 hash 值用于存放值,以及做了一些初始化工作,我们先看看 Entry 这个类

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
}

使用了ThreadLocal的key 以弱引用方式保存,避免了内存泄漏,我们看看这个类的几个主要的方法:

private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
}

首先计算 hash 值,获取当前的 Entry,如果不为 null 并且 key 是相等的,那么直接返回当前的 Entry, 也有可能存在hash 冲突,key 对应 Entry 的存储位置有可能不在通过 key 计算出来的位置,那么就要通过 getEntryAfterMiss(key, i, e) 获取

 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
}

如果 e 不为 null,那么进去循环,为 null 则直接换回 null,取到当前Entry 存贮位置的 key, 如果当前的key 跟 指定的 key 相等,那么直接返回 Entry, 如果当前Entry 存贮位置的 key 被回收了,则清除当前的 Entry, 否则获取下一个位置的 Entry 进行判断。

private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            //计算存储的索引位置
            int i = key.threadLocalHashCode & (len-1);
            //通过循环判断计算的索引位置是否有Entry,如果有则进入循环体
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                //当前Entry存贮位置的key和指定的key相等,那么则更新当前的值
                if (k == key) {
                    e.value = value;
                    return;
                }
                //如果被回收了,那么用当前计算的key值替换当前的Entry
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //当前位置没有Entry存放,那么将当前指定的key和value的Entry保存在此处
            tab[i] = new Entry(key, value);
            //size加一
            int sz = ++size;
            //清除一些无用的条目如果当前存储的数目已经大于阈值那么则要调整容量重新摆放Entry的位置
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
}

看看 remove 方法

private void remove(ThreadLocal<?> key) {
            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)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
 }

清除 Entry 的 key 的引用并清除无效的 Entry

 private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // 删除此位置上过期的Entry,更新size
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            Entry e;
            int i;
            //从当前位置扫描,Entry不为null则进入循环体
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //当前位置的key被回收了则清空当前的引用和value
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    //如果不为null,那么重新计算hash值
                    //如果重新计算的索引值值与原来Entry位置的索引值不相等则将原来的Entry资源清空
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;
                        //当前新的索引值存在Entry,则循环遍历找到一个Entry为null的索引位置,将该Entry放入
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
}

主要清除无效的Entry避免内存泄漏

  private void rehash() {
            expungeStaleEntries();

            if (size >= threshold - threshold / 4)
                resize();
}

先清除无效的Entry,接着判断是否超过了阈值的3/4,超过了则要进行扩容操作
计算索引的位置k.threadLocalHashCode & (len - 1) ,为什么使用这种方式获得呢?

//ThreadLocal的hashcode,每当创建ThreadLocal实例时这个值都会累加 0x61c88647,为了让哈希码能均匀的分布在2的N次方的数组里
private final int threadLocalHashCode = nextHashCode();
//实现线程同步
private static AtomicInteger nextHashCode =
        new AtomicInteger();
//神奇的数字,黄金比例数字
private static final int HASH_INCREMENT = 0x61c88647;
//计算hashcode的增量
private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

为什么是这个数字呢,因为容量是2的幂次方,那么(len - 1) 用二进制表示就是低位连续N个1,那么k.threadLocalHashCode & (len - 1) 的值就是threadLocalHashCode的低N位,我们模拟实现看究竟能不能均匀的分布:

int HASH_INCREMENT = 0x61c88647;
        int len = 16;
        for (int i = 0; i < len; i++) {
            System.out.print(((i * HASH_INCREMENT + HASH_INCREMENT) & (len - 1)) + " ");
        }

输出

7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 

这样就能使哈希码均匀的分布没有冲突,上面的测试分布都挺均匀的,查找资料发现通过这个数,我们可以得到一个斐波拉契散列——这个散列中的数是绝对分散且不重复的,更多了解可以查看这篇文章Why 0x61c88647?
我们再来总结一下ThreadLocal 如何实现每个线程的值独立不受影响的,根据每一个线程Thread 的变量ThreadLocal.ThreadLocalMap threadLocals 来存储值的,我们调用 get 和 set 的时候永远都是对当前线程的存储 map 进行操作,所以互不影响。

三、 ThreadLocal运用以及内存泄漏

在 Android 中,对于 Handler 来说,需要获取当前线程的 Looper,Looper 在不同线程具有不同的 Looper,那么使用 ThreadLocal 轻松实现 Looper 在线程中的存取。在 Java 中,ThreadLocal是实现线程安全的一种方案,比如对于 DateFormat/SimpleDateFormat,他们使用都不是线程安全的,为啥不安全可以看这篇文章SimpleDateFormat是线程不安全的 ,实现安全的一种方式是我们可以使用加锁解决,另外一种方式就是使用 ThreadLocal,每个线程使用自己的 DateFormat 就不会存在安全问题了。

内存泄漏为什么会发生呢,我们存数据的时候 key 是一个弱引用,但是 value 还是一个强引用,当我们使用过后,key 指定的对象会被垃圾器回收,但是 value 和 value 指向的对象之间仍然是强引用关系,就不会被回收发生内存泄漏,不过呢 ThreadLocal 在我们每次使用 set ,get, remove 过后都会调用expungeStaleEntry 方法清除 key 为 null 的 Entry,所以我们一般使用完成过后手动调用 remove 方法,避免内存泄漏。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值