前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >死磕Java之聊聊ThreadLocal源码(基于JDK1.8)

死磕Java之聊聊ThreadLocal源码(基于JDK1.8)

作者头像
haifeiWu
发布2018-09-11 10:14:30
1.4K0
发布2018-09-11 10:14:30
举报
文章被收录于专栏:haifeiWu与他朋友们的专栏

记得在一次面试中被问到ThreadLocal,答得马马虎虎,所以打算研究一下ThreadLocal的源码

面试官 : 用过ThreadLocal吗?

楼主答 : 用过,当时使用ThreadLocal的时候,使用Spring实现横切整个Controller层,使用ThreadLocal实现了统计每次请求对应方法的执行时间,具体代码如下

代码语言:javascript
复制
public class ProfilerAdvice {

    Logger logger = Logger.getLogger(ProfilerAdvice.class);

    private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>(){
        @Override
        protected Long initialValue() {
            return System.currentTimeMillis();
        }
    };

    public static final void begin() {
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }

    public static final Long end() {
        return System.currentTimeMillis() - TIME_THREADLOCAL.get();
    }

    public void requestStart() {
        ProfilerAdvice.begin();
    }

    public void requestEnd(JoinPoint joinPoint) {
        /**
         * 获取被拦截的方法.
         */
        String methodName = joinPoint.getSignature().getName();
        logger.info(methodName + "方法请求耗时 "+ProfilerAdvice.end()/1000.0+"s");

    }
}

面试官 : 那ThreadLocal对应的数据结构是咋样的啊?

楼主答 :应该是使用哈希表实现的吧,楼主这个时候心里开始没底了,然后就没有然后……

聊聊JDK源码中ThreadLocal的实现

主要方法:

  • ThreadLocal的get方法 ThreadLocal之get流程: 1、获取当前线程t; 2、返回当前线程t的成员变量ThreadLocalMap(以下简写map); 3、map不为null,则获取以当前线程为key的ThreadLocalMap的Entry(以下简写e),如果e不为null,则直接返回该Entry的value; 4、如果map为null或者e为null,返回setInitialValue()的值。setInitialValue()调用重写的initialValue()返回新值(如果没有重写initialValue将返回默认值null),并将新值存入当前线程的ThreadLocalMap(如果当前线程没有ThreadLocalMap,会先创建一个)。
代码语言:javascript
复制
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();
 }
  • ThreadLocal的set方法 ThreadLocal之set流程: 1、获取当前线程t; 2、返回当前线程t的成员变量ThreadLocalMap(以下简写map); 3、map不为null,则更新以当前线程为key的ThreadLocalMap,否则创建一个ThreadLocalMap,其中当前线程t为key;
代码语言:javascript
复制
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocal主要的代码实现

下面代码时楼主认为ThreadLocal中比较重要的,还是比较容易看懂的,就不在一一细说

代码语言:javascript
复制
public class ThreadLocal<T> {
    /**
     * ThreadLocals依赖于附加的每线程线性探测哈希映射到每个线程(Thread.threadLocals和 
     * inheritableThreadLocals)。 ThreadLocal对象充当键,通过threadLocalHashCode搜索。 这是一个自定义哈希码
     * (仅在ThreadLocalMaps内有用),可以消除哈希冲突
     * 在连续构造ThreadLocals的常见情况下
     * 由相同的线程使用,同时保持良好的行为和异常情况的发生。
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * 下一个哈希码将被发出,原子更新,从零开始。
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * 连续生成的散列码之间的区别 , 将隐式顺序线程本地ID转换为近乎最佳的散布
     * 两倍大小的表的乘法散列值。
     */
    private static final int HASH_INCREMENT = 0x61c88647;

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

    /**
     * 返回此线程局部变量的当前线程的“初始值”。 在线程首次访问带有{@link #get}方法的变量时,将调用此方法,
     * 除非线程先前调用了{@link #set}方法,在这种情况下,initialValue方法不会 为该线程调用。 
     * 通常,每个线程最多调用一次此方法,但在后续调用{@link #remove}后跟{@link #get}时可能会再次调用此方法。 
     * <p>这个实现只是返回{@code null}; 如果程序员希望线程局部变量的初始值不是{@code null},
     * 则必须对子代码{@CodeLocal}进行子类化,并重写此方法。 通常,将使用匿名内部类。
     */
    protected T initialValue() {
        return null;
    }

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

    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    /**
     * SuppliedThreadLocal是JDK8新增的内部类,只是扩展了ThreadLocal的初始化值的方法而已
     * ,允许使用JDK8新增的Lambda表达式赋值。需要注意的是,函数式接口Supplier不允许为null。
     */
    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是定制的hashMap,关于HashMap的更详细的问题请参看《聊聊HashMap源码》,
     * 仅用于维护当前线程的本地变量值。仅ThreadLocal类对其有操作权限,
     * 是Thread的私有属性。为避免占用空间较大或生命周期较长的数据常驻于内存引发一系列问题,
     * hash table的key是弱引用WeakReferences。当空间不足时,会清理未被引用的entry。
     */
    static class ThreadLocalMap {

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

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

        /**
         * 构造一个新的包含初始映射,ThreadLocal映射的新映射,因此我们只在创建至少一个条目时创建一个。
         */
        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);
        }

        /**
         * 从给定的parentMap构造一个包含所有map的新ThreadLocal。仅由createInheritedMap调用。
         *
         * @param parentMap the map associated with parent thread.
         */
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }
    }
}

ThreadLocal 使用demo

代码语言:javascript
复制
public class ThreadLocalExample {


    public static class MyRunnable implements Runnable {

        private ThreadLocal<Integer> threadLocal =
               new ThreadLocal<Integer>();

        @Override
        public void run() {
            threadLocal.set( (int) (Math.random() * 100D) );
    
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
    
            System.out.println(threadLocal.get());
        }
    }


    public static void main(String[] args) {
        MyRunnable sharedRunnableInstance = new MyRunnable();

        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);

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

        thread1.join(); //wait for thread 1 to terminate
        thread2.join(); //wait for thread 2 to terminate
    }

}

本示例创建一个传递给两个不同线程的MyRunnable实例。 两个线程都执行run()方法,从而在ThreadLocal实例上设置不同的值。 如果对set()调用的访问已经同步,并且它不是ThreadLocal对象,则第二个线程将覆盖第一个线程设置的值。

但是,由于它是一个ThreadLocal对象,因此两个线程无法看到对方的值。 因此,他们设定并获得不同的价值观。

小结

  • ThreadLocal特性及使用场景

1、实现单个线程单例以及单个线程上下文信息存储,比如交易id等

2、实现线程安全,非线程安全的对象使用ThreadLocal之后就会变得线程安全,因为每个线程都会有一个对应的实例

3、承载一些线程相关的数据,避免在方法中来回传递参数

  • ThreadLocal使用过程中出现的问题

1、ThreadLocal并未解决多线程访问共享对象的问题,如果ThreadLocal.set()的对象是多线程共享的,那么还是涉及并发问题;

2、会导致内存泄露么?

有人认为ThreadLocal会导致内存泄露,原因如下

首先ThreadLocal实例被线程的ThreadLocalMap实例持有,也可以看成被线程持有。 如果应用使用了线程池,那么之前的线程实例处理完之后出于复用的目的依然存活 所以,ThreadLocal设定的值被持有,导致内存泄露。

上面的逻辑是清晰的,然而Java的设计者早已经想到了这个问题,ThreadLocal并不会产生内存泄露,因为ThreadLocalMap在选择key的时候,

并不是直接选择ThreadLocal实例,而是ThreadLocal实例的弱引用。所以实际上从ThreadLocal设计角度来说是不会导致内存泄露的,具体代码如下所示:

代码语言:javascript
复制
static class ThreadLocalMap { 
    
    static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

3、然而ThreadLocal真的不会导致内存泄漏吗?上面2说到的不会导致内存泄漏,是说java的设计者也想到了这一点,也不一定保证ThreadLocal一定不会出现内存泄漏的问题,如下图所示

每个thread中都存在一个map,map的类型是ThreadLocal.ThreadLocalMap,Map中的key为一个threadlocal实例。这个Map的确使用了弱引用,不过弱引用只是针对key。每个key都弱引用指向threadlocal。当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用。只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current Thread,Map,value将全部被GC回收。 所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。

下面我们用代码来验证一下,

代码语言:javascript
复制
public class ThreadPoolProblem {
    static ThreadLocal<AtomicInteger> sequencer = new ThreadLocal<AtomicInteger>() {

        @Override
        protected AtomicInteger initialValue() {
            return new AtomicInteger(0);
        }
    };

    static class Task implements Runnable {

        @Override
        public void run() {
            AtomicInteger s = sequencer.get();
            int initial = s.getAndIncrement();
            // 期望初始为0
            System.out.println(initial);
        }
    }

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.execute(new Task());
        executor.execute(new Task());
        executor.execute(new Task());
        executor.shutdown();
    }
}

对于异步任务Task而言,它期望的初始值应该总是0,但运行程序,结果却为:

代码语言:javascript
复制
0
0
1

第三次执行异步任务,结果就不对了,为什么呢?因为线程池中的线程在执行完一个任务,执行下一个任务时,其中的ThreadLocal对象并不会被清空,修改后的值带到了下一个异步任务。那怎么办呢?有几种思路:

  1. 第一次使用ThreadLocal对象时,总是先调用set设置初始值,或者如果ThreaLocal重写了initialValue方法,先调用remove
  2. 使用完ThreadLocal对象后,总是调用其remove方法
  3. 使用自定义的线程池

参考文章

作 者:haifeiWu

原文链接:https://cloud.tencent.com/developer/article/1333298

版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 聊聊JDK源码中ThreadLocal的实现
  • ThreadLocal主要的代码实现
  • ThreadLocal 使用demo
  • 小结
  • 参考文章
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档