ThreadLocal的源码分析和理解

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

java 复制代码
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用法示例

java 复制代码
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在每个线程都有一个副本,互不影响。

csharp 复制代码
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()方法

java 复制代码
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 成员变量

java 复制代码
/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
 
ThreadLocal.ThreadLocalMap threadLocals = null;

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

java 复制代码
    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

java 复制代码
    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();
    }
java 复制代码
/**
 * 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 是计算当前值相加所传参数的值,但返回的是当前值

java 复制代码
    /**
         * 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()方法

java 复制代码
    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

java 复制代码
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

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

java 复制代码
    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数组里。

markdown 复制代码
    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。

相关推荐
dj244294570720 分钟前
JAVA中的Lamda表达式
java·开发语言
工业3D_大熊33 分钟前
3D可视化引擎HOOPS Luminate场景图详解:形状的创建、销毁与管理
java·c++·3d·docker·c#·制造·数据可视化
szc176737 分钟前
docker 相关命令
java·docker·jenkins
程序媛-徐师姐1 小时前
Java 基于SpringBoot+vue框架的老年医疗保健网站
java·vue.js·spring boot·老年医疗保健·老年 医疗保健
yngsqq1 小时前
c#使用高版本8.0步骤
java·前端·c#
尘浮生1 小时前
Java项目实战II基于微信小程序的校运会管理系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
小白不太白9501 小时前
设计模式之 模板方法模式
java·设计模式·模板方法模式
Tech Synapse1 小时前
Java根据前端返回的字段名进行查询数据的方法
java·开发语言·后端
xoxo-Rachel1 小时前
(超级详细!!!)解决“com.mysql.jdbc.Driver is deprecated”警告:详解与优化
java·数据库·mysql