ThreadLocal的源码分析和理解

ThreadLocal的字面意思就是线程的本地变量,也就是说线程的局部变量。ThreadLocal在每个线程中都有自己的副本,亦即一个线程一份数据,相互之间不影响。
ThreadLocal提供getset访问方法,使用get时总是返回set方法的最新值。ThreadLocal一个典型的应用就是减少一个线程内的参数传递,如数据库连接JDBCConnection或用户的Session,避免每个方法调用都要传递这个参数。
Spring的事务处理就大量使用了ThreadLocal,如TransactionSynchronizationManager这个类。

private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal("Actual transaction active");

ThreadLocal用法示例

package org.encyclopedia.thread;

import java.util.concurrent.CountDownLatch;

/**
 * Created by massivestars on 2018/3/10.
 */
public class ThreadLocalTest {

    static ThreadLocal<String> tl = new ThreadLocal<String>();

    /**
     * 使用CountDownLatch使得三个线程都设置完值后再打印输出
     */
    static CountDownLatch latch = new CountDownLatch(3);


    static class SimpleThread extends Thread {


        public SimpleThread(String threadName) {
            super(threadName);       //将线程的名称设为val
        }

        public void run() {
            String threadName = Thread.currentThread().getName();
            tl.set(threadName);
            System.out.println(threadName + " has set value");
            try {
                latch.countDown();
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadName + " threadLocal value is: " + tl.get());
        }

    }

    public static void main(String[] args) throws InterruptedException {

        String currentThreadName = Thread.currentThread().getName();

        tl.set(currentThreadName);
        System.out.println(currentThreadName + " has set value");
        latch.countDown();

        Thread thread1 = new SimpleThread("thread1");
        Thread thread2 = new SimpleThread("thread2");

        thread1.start();
        thread2.start();

        latch.await();
        System.out.println(currentThreadName + " threadLocal value is: " + tl.get());
    }

}

示例程序一共有三个线程,分别为主线程main、线程thread1和thread2,三个线程根据线程名字给ThreadLocal设置值,然后用CountdownLatch阻塞各个线程直至三个线程都设置Theadlocal完毕 。运行后的输出结果如下, 三个线程内部输出的ThreadLocal值都和线程的名字一样,由于可以看出ThreadLocal在每个线程都有一个副本,互不影响。

main has set value
thread1 has set value
thread2 has set value
thread1 threadLocal value is: thread1
thread2 threadLocal value is: thread2
main threadLocal value is: main

实现细节

ThreadLocal最重要的两个方法是get()set(),所以查阅源码自然而然从这两方法入手,查看源码可以看出ThreadLocal实际上是取当前线程的成员变量threadLocals进行存取,threadLocals的类型为ThreadLocalMap

  • 数据结构
    这里写图片描述

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

ThreadLocalMap getMap(Thread t) {
    //Thread t传参值为当前线程
    //由此处可看出ThreadLocalMap变量是当前线程的成员变量
    return t.threadLocals;
}

Thread类的ThreadLocalMap threadLocals 成员变量

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap的定义如源码所示,是ThreadLocal的内部类,实现了一个自定义的Map数据结构。

static class ThreadLocalMap {

    //ThreadLocalMap真正的存储数据结构是Entry数组
    private Entry[] table;

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

        Entry(ThreadLocal<?> k, Object v) {
            //key为弱引用
            super(k);
            value = v;
        }
    }
}

ThreadLocalset方法实际上是调用当前线程ThreadLocalMap的set方法,而实际存储的Entry里,下标为Threadlocal的this对象里threadLocalHashCode的低位值,由此可知,一个线程可以有多个ThreadLocal变量,每个ThreadLocal变量对应一个Entry

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    //上面提过ThreadLocal的真正存储是一个Entry数组
    Entry[] tab = table;
    int len = tab.length;
    //len为Entry数组的长度,为2的N次方幂,len-1的值转为二进制是高位值取0,低位取1
    //所以key.threadLocalHashCode & (len-1) 的值实际上是key.threadLocalHashCode的值取低位
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
        //若e不为null,则该下标已被占用
        //下标被占(hash冲突会出现这情况)后循环取下一个值(+1)直至没有被占用
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        //k之前已被设置,用新值替换旧值
        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
/**
 * Increment i modulo len.
 * 注: i + 1大于等于len时则从0开始
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

实际上ThreadLocal只有一个非静态成员量,就是threadLocalHashCode,threadLocalHashCode的低位是作为ThreadLocalMap实际存储数据的Entry数组的下标, 每初始化一个ThreadLocal实例就要给threadLocalHashCode赋值,从源码可看出threadLocalHashCode的值是AtomicInteger + HASH_INCREMENT(0x61c88647)。所以JVM里第一个初始化的ThreadLocal实例的threadLocalHashCode的值为new AtomicInteger()的默认值0,第二个ThreadLocal实例的threadLocalHashCode的值为0x61c88647, 第三个为 0x61c88647 + 0x61c88647, 依此类推。至于为什么用0x61c88647累加我们在后面作简单分析。

注意: 1、AtomicInteger是静态变量 2、AtomicInteger#getAndAdd 是计算当前值相加所传参数的值,但返回的是当前值

/**
     * ThreadLocals rely on per-thread linear-probe hash maps attached
     * to each thread (Thread.threadLocals and
     * inheritableThreadLocals).  The ThreadLocal objects act as keys,
     * searched via threadLocalHashCode.  This is a custom hash code
     * (useful only within ThreadLocalMaps) that eliminates collisions
     * in the common case where consecutively constructed ThreadLocals
     * are used by the same threads, while remaining well-behaved in
     * less common cases.
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        //getAndAdd 是计算当前值相加所传参数的值,但返回的是当前值
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

get()方法

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //this为ThreadLocal自身
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

//ThreadLocalMap#getEntry()
private Entry getEntry(ThreadLocal<?> key) {
    //从上一调用栈帧可知ThreadLocal<?> key是ThreadLocal自身(this)
    //key.threadLocalHashCode & (table.length - 1)实际上是取key.threadLocalHashCode的低位
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        //由set知道由于不同的TheadLocal发生下标冲突时会取nextIndex
        //所以在get方法中i没有命中时也要继续继续遍历nextInt
        return getEntryAfterMiss(key, i, e);
}
/**
 * 
 * 当使用i取table[i]没命中时会继续遍历Entry[] table
 */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    //由于Entry[] table的元素数量达到阀值后会自动扩容
    //也就是说一定会有Entry的key为null,所以while不会无限循环
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            //清除无效Entry(key为null)
            expungeStaleEntry(i);
        else
            //注:若nextIndex大于等于len会从0开始
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

expungeStaleEntry方法清除下标为staleSlot的entry,并且清除过期和重新分配发生过hash冲突导致下标位移的entry

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

    //清除下标等于staleSlot的Entry
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    //rehash直到entry为null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            //当k不为null且计算出的下标值和i不相等时证明在set时已经发生过冲突
            //此处将此entry重新设置在数组中的位置
            if (h != i) {
                tab[i] = null;

                //.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

关于0x61c88647

从上面的代码分析可以看出ThreadLocal实例里面threadLocalHashCode的值为JVM里该ThreadLocal实例化次序进行累加0x61c88647,每实例化一个ThreadLocal,就是在原值的基础上加上0x61c88647。为什么要用0x61c88647这数字呢,该数字有什么魔力?ThreadLocal用到这个数值称为魔数,是为了让哈希码能均匀的分布在2的N次方的数组里(即Entry[] table),均匀分布的好处显然而见,能减少数组的下标探测,提高性能。
0x61c88647这个数字的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。斐波那契散列的值为2的32次方再乘以黄金分割数0.618, 黄金分割数的计算方法为公式(√5+1)÷2, 所以这个魔数为(int)(long)((1L << 32) * ((Math.sqrt(5) - 1)/2)),得到-1640531527-16405315271640531527的无符号整数,亦即0x61c88647

下面我们用一个实验证实上面的理论

package org.encyclopedia.thread;

/**
 * Created by massivestars on 2018/3/11.
 */
public class HashIncrement {

    private static final int HASH_INCREMENT = 0x61c88647;

    public static void hashCodeVal(int len) {
        int nextHashCode = 0;
        for (int i = 0; i < len; i++) {
            System.out.print((nextHashCode & len -1) + " ");
            nextHashCode = nextHashCode + HASH_INCREMENT;
        }
        System.out.print("\n");
    }

    public static void main(String[] args) {
        hashCodeVal(16);
        hashCodeVal(32);
    }

}

程序输出如下,可见使用0x61c88647这个确实能更均匀的分布在即Entry[] table数组里。

0 7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 
0 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 

关于ThreadLocal的内存泄漏

这里写图片描述

每个thread中都存在一个ThreadLocalMap, ThreadLocalMap中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被GC回收.

我们再来看看ThreadLocal的引用链:
Thread -> ThreadLocalMap -> Entry -> value

虽然ThreadLocal的key作为弱引用会在每次GC的时候回收,从而key变为null,然而线程没有结束的话,上面的引用链还是会存在。ThreadLocal对这个情况也是作了很大优化,在每次get()set()的时候都会遍历把key为null的Entry清除。所以在绝大部分情况都不会存在内存泄漏的情况,但要注意到在get()、set()的操作间隔且key还没有被GC时value存储大内存对象的情况,若多个线程都各自持有大内存value且线程没有被收回则很容易出现内存溢出。所在我们在每次使用ThreadLocal对象后最好调用remove()方法显式清除value。

特别需要注意使用线程池的时候,线程结束是不会销毁的,会再次使用的,会出现读脏数据和内存泄露的情况。
  

最佳实践

  • ThreadLocal对象使用static修饰。否则每个线程对于每个使用的非静态ThreadLocal实例都初始化一个ThreadLocal实例。由于每个线程都有自己的副本,ThreadLocal只是作一个key的存在,使用static静态修饰创建一个实例即可。

  • 每次使用ThreadLocal对象结束后调用remove()方法清除key和value。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值