ThreadLocal

1、前言:

看名知意,本地线程,采用”空间换时间“的思想,每个线程持有该份数据的拷贝,线程互不影响,可以用来解决在一些环境下的线程安全问题。

2、关键变量和常量:

 	// 本地线程的 hashcode
	private final int threadLocalHashCode = nextHashCode();

   	// 下一个线程的 hashcode 分配,原子性操作,初始值为 0 
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

   	/**
     * 为了保证hashcode分布均匀,在原来的基础上加上这部分数值,这翻译成十进制的值为 1640531527
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    // 返回 hashcode
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

2.1、为啥要使用这个 HASH_INCREMENT :

可以看出,它是在上一个被构造出的ThreadLocal的ID/threadLocalHashCode的基础上加上一个魔数0x61c88647的。这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。

斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。换句话说 (1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是1640531527也就是0x61c88647 。

通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。

ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。

来自(为什么使用0x61c88647 - 掘金

3、ThreadLocal 常用代码分析:

和外部相关的几个常用的方法,如 get()、set()、remove()方法等,主要对这几个方法和在方法中被调用的本类方法来进行代码分析。

3.1、get() 方法

	/**
     * 返回当前线程的的值,如果没得返回初始化值
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
             // 值都存储在 ThreadLocalMap.Entry 里边,key 是一个当前 ThreadLocal 对象,后边都是正常的值获取,然后强转为 T
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果当前线程没有初始化值,则进行初始化值
        return setInitialValue();
    }

	/**
     * Thread 存有 ThreadLocalMap 变量
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }


    /**
     * 设置初始化值
     */
    private T setInitialValue() {
        // 是否有子类重写该方法,有则返回子类方法的 value ,没有返回 null
        T value = initialValue();
        Thread t = Thread.currentThread();
        // 获取 ThreadLocalMap 对象
        ThreadLocalMap map = getMap(t);
        // 如果已经存在 ThreadLocalMap 对象,就设置值,否则创建一个 ThreadLocalMap
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

	/**
 	 * 根据子类重写的值返回一个初始化值,默认返回 null
     */
	protected T initialValue() {
        return null;
    }

	/**
     * 创建一个 ThreadLocalMap 对象
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

3.2、set() 方法

	/**
     * 设置值,方法代码比较简单
     */
    public void set(T value) {
        // 常用的套路,获取当前线程,从当前线程的 threadLocals 变量 来获取 ThreadLocalMap
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

3.3、remove() 方法

	/**
     * 删除值,还是老套路,通过当前线程获取 ThreadLocalMap,然后当前 ThreadLocal 对象作为 key 来删除值
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

4、ThreadLocalMap 静态内部类

上边的几个方法实现都简单明了,里面都涉及到了一个ThreadLocalMap对象,对外隐藏了内部的细节,很好的一个 API 实现思想,好了说远了,现在看看这个内部类的实现。

4.1、Entry,存储数据的数据结构

		/**
         * Entry 数组存放数据
         */
        private Entry[] table;

		/**
         * 存放值的 Entry 对象,是一个 Map 结构,key 为 ThreadLocal 对象,需要注意的是这个继承于弱引用 WeakReference, 里边把 key 链接到了弱引用对象那了
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

弱引用:

当一个对象仅仅被weak reference(弱引用)指向, 而没有任何其他 strong reference(强引用)指向的时候, 如果这时 GC 运行, 那么这个对象就会被回收,不论当前的内存空间是否足够,这个对象都会被回收。

具体可以看看这篇(关于Java中的WeakReference - 简书

4.2、getEntry() 方法

/**
         * 获取 Entry 对象,java 目前关于定位 table 的 index 都是采取一个位运算的方式了。
         */
        private Entry getEntry(ThreadLocal<?> key) {
            // 位运算定位 index
            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);
        }

        /**
         * 没找到,分两种情况,
         * 一种是里边根本没有,如 entry 为 null, 这种直接返回 null
         * 另外一种还抱有希望的,但前提是你这个 threadlocal 不为 null,
         * 前边说过 key 为 ThreadLocal 对象,被链接到了弱引用对象那了,所以一旦发生GC,这势必会被回收,所以这里需要判断下是否还存在
         */
        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)
                    // 不存在,通过重新散列位于staleSlot和下一个空槽之间的任何可能发生冲突的条目,删除过时的条目
                    expungeStaleEntry(i);
                else
                 	// 存在,就找下一个 index
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

4.3、set() 方法

		 /**
         * 设值,找到 key 所在位置,替换原有值,如果没找到,新建一个 slot 存放。
         */
        private void set(ThreadLocal<?> key, Object value) {
            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)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }
				// 替换过时的 entry,为啥过时呢,也是上边说到的 key 为弱引用的原因。
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

4.4、关于内存泄漏的问题

从 set() 、get()、remove() 方法里边,我们可以看到设计师考虑到这种情况了,会清理掉 key 为 null 的值。但是使用场景是多变的,可能出现使用完之后没有手动去 remove(),或者之后再也没有调用前边说到的那三个方法。这样才可能存在 key 为 null,但 value 却一直没有被清理的情况。所以为了保险起见,尤其是通过线程池这种来托管线程的方式,应该调用之后手动去 remove() ,避免内存泄漏的问题。

5、使用场景

5.1、每个线程需要有自身的一个实例

就拿最近开发的事来说,事件通知机制,公司原有的事件通知机制,是一个静态事件,添加、删除没啥问题,但通知就有问题了,可能会干扰到其他事件的发生。反应到UI上就是一个进度条混乱。对于这种情况,就可以利用这个ThreadLocal 来解决该问题。

同时还有常见的数据库连接的实现或者 web端 的 session 的保存。

6、参考

1、关于Java中的WeakReference - 简书

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值