线程本地变量(ThreadLocal)

一、前言

之前看 Android Looper 源码时,在看到 Looper.prepare 时,有个静态成员变量:sThreadLocal:

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

我们看到,先 get ,没有就会去 set。

然后就很好奇的去看了下 ThreadLocal 源码,看源码的过程中,JDK又给了我一个惊喜:

/**
 * 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;

发现了一个很奇怪的常数,但不知道这个常数是怎么得来的,故在网上查找了相关资料,总算明白这个常数的含义,以及 ThreadLocal 的作用。

二、ThreadLocal

先来说说这个 ThreadLocal 是什么吧?
ThreadLocal 是线程的内部存储类,可以在指定的线程内,存储数据,同时,也只有指定的线程可以访问该数据;不同的数程是无法访问其它线程的这个变量的,因此,也就做到了线程的数据隔离。

This class provides thread-local variables.  These variables differ from their normal counterparts in 
that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently 
initialized copy of the variable.  {@code ThreadLocal} instances are typically private static fields in 
classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

官方注释:ThreadLocal 提供了线程本地变量,和其它的变量不同,只有每个线程自己可以 get / set,每个变量(副本)是独立的。

2.1、ThreadLocal.set

public class ThreadLocal<T> {
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value); // map的key = ThreadLocal, value 是要存储的对象
        else
            createMap(t, value); // 没有就为这个 Thread 创建 ThreadLocalMap 并赋值
    }
    
    ThreadLocalMap getMap(Thread t) {
        // Thread 中有个 threadLocals变量
        // 类型:ThreadLocal.ThreadLocalMap
        return t.threadLocals;
    }
    
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
}

2.2、ThreadLocal.get

public class ThreadLocal<T> {
    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(); // 初始值为 null
    }
}

2.3、ThreadLocalMap

static class ThreadLocalMap {
    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

    // 初始容量,之后扩容是 2的N次方扩容
    private static final int INITIAL_CAPACITY = 16;
    // 存储 key & value
    private Entry[] table;
    // 已使用的个数
    private int size = 0;
    // 当前 table.length * 2 / 3,即超过三分之二就开始扩容,和 HashMap的负载因子 0.75 不一样
    private int threshold;
}

\color{red}{看到网上有说到,内存泄露:}
因为 ThreadLocalMap 的生命周期与 Thread 一样长(Thread 持有 threadLocals),当你的 key( ThreadLocal )为 null 时,value 仍被 table 持有,因此,需要主动调用 ThreadLocal 的 remove 方法来清除内存。

三、魔数:0x61c88647

我们在一进入 ThreadLocal 类时,就能看到一段代码:

public class ThreadLocal<T> {
    /**
     * 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(); // 1640531527

    /**
     * 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; // 1640531527

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
}

上面的代码注释中也解释到:HASH_INCREMENT 是为了让哈希码能均匀的分布在2的N次方的数组里
每个 ThreadLocal 都使用该散列值!

散列是什么?

散列(Hash)也称为哈希,就是把任意长度的输入,通过散列算法,变换成固定长度的输出,这个输出值就是散列值。
在实际使用中,不同的输入可能会散列成相同的输出,这时也就产生了冲突。
通过上文提到的 HASH_INCREMENT 再借助一定的算法,就可以将哈希码能均匀的分布在 2 的 N 次方的数组里,保证了散列表的离散度,从而降低了冲突几率。

散列算法

  • 取模法

散列长度 m, 对于一个小于 m 的数 p 取模,所得结果为散列地址。对 p 的选择很重要,一般取素数或 m
公式:f(k) = k % p (p<=m)
因为求模数其实是通过一个除法运算得到的,所以叫“取模法”

  • 平方散列法(平方取中法)

先通过求关键字的平方值扩大相近数的差别,然后根据表长度取中间的几位数作为散列函数值。
又因为一个乘积的中间几位数和乘数的每一位都相关,所以由此产生的散列地址较为均匀。
公式:f(k) = ((k * k) >> X) << Y
对于常见的32位整数而言,也就是 f(k) = (k * k) >> 28

  • 斐波那契(Fibonacci)散列法

和平方散列法类似,此种方法使用斐波那契数列的值作为乘数而不是自己。
对于 16 位整数而言,这个乘数是 40503。
对于 32 位整数而言,这个乘数是 2654435769。
对于 64 位整数而言,这个乘数是 11400714819323198485。

公式:f(k) = ((k * 2654435769) >> X) << Y

对于常见的32位整数而言,也就是 f(k) = (k * 2654435769) >> 28
这时我们可以隐隐感觉到 0x61c88647 与斐波那契数列有些关系。

  • 随机数法

选择一随机函数,取关键字的随机值作为散列地址,通常用于关键字长度不同的场合。
公式:f(k) = random(k)

  • 链地址法(拉链法)

懂了散列算法,我们再来了解下拉链法。拉链法是为了 HashMap 中降低冲突,除了拉链法,还可以使用开放寻址法、再散列法、链地址法、公共溢出区等方法。这里就只简单介绍了拉链法。

把具有相同散列地址的关键字(同义词)值放在同一个单链表中,称为同义词链表。有 m 个散列地址就有 m 个链表,同时用指针数组 T[0..m-1] 存放各个链表的头指针,凡是散列地址为 i 的记录都以结点方式插入到以 T[i] 为指针的单链表中。T 中各分量的初值应为空指针。

\color{red}{对于HashMap}

hashmap.png

\color{red}{对于取模法( k = 16 }

mode.png

\color{red}{对于斐波那契散列法}

fac.png

可以看出用斐波那契散列法调整之后会比原来的除法散列离散度好很多。

再来说说,魔数怎么来的?

public class HashTest {
    public static void main(String[] args) {
        long lg = (long) ((1L << 32) * (Math.sqrt(5) - 1)/2);
        System.out.println("as 32 bit unsigned: " + lg);
        int i = (int) lg;
        System.out.println("as 32 bit signed:   " + i);
        System.out.println("MAGIC = " + 0x61c88647);
    }
}

// as 32 bit unsigned: 2654435769
// as 32 bit signed:   -1640531527
// MAGIC = 1640531527

可以发现 0x61c88647 与一个神奇的数字产生了关系,它就是 (Math.sqrt(5) - 1)/2。
也就是传说中的黄金比例 0.618(0.618 只是一个粗略值),即 0x61c88647 = 2^32 * 黄金分割比。同时也对应上了上文所提到的斐波那契散列法。

四、ThreadLocal 应用场景

ThreadLocal的使用场景:

  • 某些对象在多线程并发访问时可能出现问题,比如使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了,我们就可以用ThreadLocal<SimpleDataFormat>来解决并发修改的问题;
  • 另一种场景是Spring事务,事务是和线程绑定起来的,Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离:
public abstract class TransactionSynchronizationManager {
        //线程绑定的资源,比如DataSourceTransactionManager绑定是的某个数据源的一个Connection,在整个事务执行过程中
        //都使用同一个Jdbc Connection
        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来解决,我们还可以用synchronized或者锁来实现线程安全。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容