一个面试进行的ThreadLocal源码深入分析


一 简介

面试问,synchronized和ThreadLocal的区别是什么?
这个问题很好理解,synchronized是jvm对于多线程并发情况下提供的一种互斥锁的机制,而Threadlocal是为解决多线程程序并发提供的一个新思路,
Threadlocal为每一个使用该变量的线程提供独立的变量副本,所以每一个线程都能独立地改变自己的副本,从而不影响其他线程对应的副本数据

二 源码

使用ThreadLocal

/**
 * Created by zhoucg on 2019-09-12.
 *
 * ThreadLocal 实例使用
 */
public class ThreadLocalPoolExample {
    public static class MyRunnable implements Runnable {
        /**
         * 实例对象
         */
        private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
        @Override
        public void run() {
            threadLocal.set( (int) (Math.random() * 100D) );
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());
        }
    }
    public  static void main(String[] args) throws Exception {
        MyRunnable sharedRunnableInstance = new MyRunnable();
        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
    }
}

代码中,首先是定义了一个公共的Runnable类,并启动两个线程执行对应的线程方法,在Runnable对象中,定义了一个ThreadLocal对象,最终的执行结果
在这里插入图片描述
下面就开始进入到ThreadLocal的源码中,看一看对应的ThreadLocal的底层实现
在这里插入图片描述
ThreadLocal中,公共的方法就5个
1,ThreadLocal();提供无参构造函数创建ThreadLocal对象
2,T get();获取当前线程的变量值
3,void remove();删除当前线程持有的副本的值
4,void set(T value);设置当前线程持有的变量值:Sets the current thread’s copy of this thread-local variable to the specified value
5,ThreadLocal<> withInitial(Supplier<? extends S> supplier);通过传入Supplire对象的方法创建对应的ThreadLocal,内部实际上是返回SuppliedThreadLocal类型的对象,这个类是ThreadLocal的内部类

ThreadLocal中,提供了两个内部类
SuppliedThreadLocal:继承ThreadLocal,接收Supplier对象,实现了ThreadLocal中的initialValue()方法

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

        private final Supplier<? extends T> supplier;

        SuppliedThreadLocal(Supplier<? extends T> supplier) {
            this.supplier = Objects.requireNonNull(supplier);
        }
        @Override
        protected T initialValue() {
            return supplier.get();
        }
    }

ThreadLocalMap:实际的内部变量副本存储
首先,我们进入到ThreadLocal的set(T value);方法,设置当前线程副本变量的值

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

set(T value)中,首先获取当前线程对象,然后通过getMap(t),获取当前线程对象持有的ThreadLocal的内部类ThreadLocalMap对象,
实际上,每一个Thread对象中,都持有了一个Thread.ThreadLocalMap变量,这个可以从Thread源码中看到

ThreadLocal.ThreadLocalMap threadLocals = null;

这个就是为什么ThreadLocal能够保证每个线程都持有变量的副本,并且线程之间互不影响的原因,
偶然间看到网上提出了这么一个问题
在这里插入图片描述
下面的博主回答的是正确的,我们在使用ThreadLocal的时候,是会出现一个线程使用多个ThreadLocal进行数据存储的场景的,这个也是ThreadLocalMap中使用数组进行存储数据的原因,当当前线程不存在指定ThreadLocalMap的时候,threadLocals为null的时候,set方法中,首先进行createMap(t,value)操作,创建一个新的ThreadLocalMap并赋值给指定线程的threadLocals变量

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

创建ThreadLocalMap时,传递当前ThreadLocal对象作为key,存储的结果作为value,ThreadLocalMap构造函数中,首先是创建table数组,数组的默认长度设置为16,然后获取key的threadLocalHashCode值和对应的数组长度-1进行与&操作,实际上就是取模,我们来重点关注一下是如何获取threadLocalHashCode值的

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
        new AtomicInteger();
private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

实际上是通过原子操作类AtomicInteger进行getAndAdd()操作获取的,这里有一个神奇的事情,就是每次创建ThreadLocal实例threadLocalHashCode值得时候,都是会累加0x61c88647

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

注解翻译的意思就是连续生成的哈希代码之间的差异,将生成的哈希代码能够均匀连续得分布在2的N次方的数组
这个魔数得选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527
通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。
ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。。为了优化效率。
如果当前线程存在threadLocals变量时,调用ThreadLocalMap的set(ThreadLocal<?> key,Object value)方法

/**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
        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.

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

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

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

首先是通过int i = key.threadLocalHashCode & (len-1);获取当前ThreaLocal存放在数组的位置,如果当前位置存在数据,ThreadLocalMap使用的是线性探测法查找下一个位置是否存在对应的值,不存在,则进行存在,这个和HashMap中使用链表法是不一样的
ThreadLocalMap对于扩容的操作是,首先清理掉数组中过期的Entry,并且判断对应数据容量的值达到默认阈值的时候调用rehash()。默认的阈值为当前数组的容量乘上3分之2

/**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

rehash()方法中,jdk1.8中,又进行了进一步的判断,当前容量大于阈值减去阈值的四分之一的时候,再继续扩容

private void rehash() {
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
        }

扩容的策略就是新建一个大小为原来数组长度的两倍的数组,然后遍历旧数组中的entry并将其插入到新的hash数组中,主要注意的是,在扩容的过程中针对脏entry的话会令value为null,以便能够被垃圾回收器能够回收,解决隐藏的内存泄漏的问题。
ThreadLocal中的get方法实际上就是通过ThreadLocal对象取模获取当前线程threads变量中ThreadLocalMap数组中的值
ThreadLocal中的remove方法通过往后环形查找到与指定key相同的entry后,先通过clear方法将key置为null后,使其转换为一个脏entry,然后调用expungeStaleEntry方法将其value置为null,以便垃圾回收时能够清理,同时将table[i]置为null。

三 疑问

   有一个小小的疑问,为什么ThreadLocal中使用ThreadLocalMap进行对应的数据存储,并且设置了默认16位的数组长度,其实在大部分的开发中,我们并不会在一个线程中同时副本出超过16个ThreadLocal对象,也就是一两个,那在多线程的开发中,因为每一个线程都持有ThreadLocalMap,不是存在浪费内存的情况吗?或许可以通过参数的控制设置默认的ThreadLocalMap数组的长度。
   应该是Josh Bloch and Doug Lea两位顶级大佬的开发思路更高级,为了设置更通用的工具类进行设计而成的吧,

并发容器之ThreadLocal

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值