作 者: haifeiWu 原文链接: https://www.hchstudio.cn/article/2018/1766/ 版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。
由于版权原因,请阅读原文 --> 死磕Java之聊聊ThreadLocal源码(基于JDK1.8)
作 者: haifeiWu 原文链接: https://www.hchstudio.cn/article/2018/1766/ 版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。
作 者: haifeiWu 原文链接: https://www.hchstudio.cn/article/2018/1766/ 版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。
记得在一次面试中被问到ThreadLocal,答得马马虎虎,所以打算研究一下ThreadLocal的源码
面试官 : 用过ThreadLocal吗?
楼主答 : 用过,当时使用ThreadLocal的时候,使用Spring实现横切整个Controller层,使用ThreadLocal实现了统计每次请求对应方法的执行时间,具体代码如下1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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,会先创建一个)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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;
1 2 3 4 5 6 7 8 9 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中比较重要的,还是比较容易看懂的,就不在一一细说
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 public class ThreadLocal <T > { private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647 ; private static int nextHashCode () { return nextHashCode.getAndAdd(HASH_INCREMENT); } 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); } 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(); } } static class ThreadLocalMap { static class Entry extends WeakReference <ThreadLocal <?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super (k); value = v; } } 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); } 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 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() * 100 D) ); 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(); thread2.join(); } }
本示例创建一个传递给两个不同线程的MyRunnable实例。 两个线程都执行run()方法,从而在ThreadLocal实例上设置不同的值。 如果对set()调用的访问已经同步,并且它不是ThreadLocal对象,则第二个线程将覆盖第一个线程设置的值。 但是,由于它是一个ThreadLocal对象,因此两个线程无法看到对方的值。 因此,他们设定并获得不同的价值观。
小结
1、实现单个线程单例以及单个线程上下文信息存储,比如交易id等
2、实现线程安全,非线程安全的对象使用ThreadLocal之后就会变得线程安全,因为每个线程都会有一个对应的实例
3、承载一些线程相关的数据,避免在方法中来回传递参数
1、ThreadLocal并未解决多线程访问共享对象的问题,如果ThreadLocal.set()的对象是多线程共享的,那么还是涉及并发问题;
2、会导致内存泄露么? 有人认为ThreadLocal会导致内存泄露,原因如下
首先ThreadLocal实例被线程的ThreadLocalMap实例持有,也可以看成被线程持有。 如果应用使用了线程池,那么之前的线程实例处理完之后出于复用的目的依然存活 所以,ThreadLocal设定的值被持有,导致内存泄露。
上面的逻辑是清晰的,然而Java的设计者早已经想到了这个问题,ThreadLocal并不会产生内存泄露,因为ThreadLocalMap在选择key的时候, 并不是直接选择ThreadLocal实例,而是ThreadLocal实例的弱引用。所以实际上从ThreadLocal设计角度来说是不会导致内存泄露的,具体代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 static class ThreadLocalMap { static class Entry extends WeakReference <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和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。
下面我们用代码来验证一下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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(); 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,但运行程序,结果却为:
第三次执行异步任务,结果就不对了,为什么呢?因为线程池中的线程在执行完一个任务,执行下一个任务时,其中的ThreadLocal对象并不会被清空,修改后的值带到了下一个异步任务 。那怎么办呢?有几种思路:
第一次使用ThreadLocal对象时,总是先调用set设置初始值,或者如果ThreaLocal重写了initialValue方法,先调用remove
使用完ThreadLocal对象后,总是调用其remove方法
使用自定义的线程池
参考文章
作 者: haifeiWu 原文链接: https://www.hchstudio.cn/article/2018/1766/ 版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。