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

面试官 : 用过ThreadLocal吗?

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


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,会先创建一个)。

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;

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中比较重要的,还是比较容易看懂的,就不在一一细说


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


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设计角度来说是不会导致内存泄露的,具体代码如下所示:

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和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。

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

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,但运行程序,结果却为:

0
0
1

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

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

参考文章

死磕Java之聊聊ThreadLocal源码(基于JDK1.8)的更多相关文章

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

    HashSet的UML图 HashSet的成员变量及其含义 public class HashSet<E> extends AbstractSet<E> implements ...

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

    死磕Java之聊聊HashMap源码(基于JDK1.8) http://cmsblogs.com/?p=4731 为什么面试要问hashmap 的原理

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

    工作快一年了,近期打算研究一下JDK的源码,也就因此有了死磕java系列 LinkedList 是一个继承于AbstractSequentialList的双向链表,链表不需要capacity的设定,它 ...

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

    工作快一年了,近期打算研究一下JDK的源码,也就因此有了死磕java系列 ArrayList 是一个数组队列,相当于动态数组.与Java中的数组相比,它的容量能动态增长.它继承于AbstractLis ...

  5. 聊聊ThreadLocal源码(基于JDK1.8)

    原文:https://cloud.tencent.com/developer/article/1333298 聊聊JDK源码中ThreadLocal的实现 主要方法: ThreadLocal的get方 ...

  6. 死磕 java集合之DelayQueue源码分析

    问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于 ...

  7. 死磕 java集合之PriorityBlockingQueue源码分析

    问题 (1)PriorityBlockingQueue的实现方式? (2)PriorityBlockingQueue是否需要扩容? (3)PriorityBlockingQueue是怎么控制并发安全的 ...

  8. 死磕 java集合之PriorityQueue源码分析

    问题 (1)什么是优先级队列? (2)怎么实现一个优先级队列? (3)PriorityQueue是线程安全的吗? (4)PriorityQueue就有序的吗? 简介 优先级队列,是0个或多个元素的集合 ...

  9. 死磕 java集合之CopyOnWriteArraySet源码分析——内含巧妙设计

    问题 (1)CopyOnWriteArraySet是用Map实现的吗? (2)CopyOnWriteArraySet是有序的吗? (3)CopyOnWriteArraySet是并发安全的吗? (4)C ...

随机推荐

  1. PHP读取文件夹数据,并分页

    protected function read_all ($dir){ // 确保目录有权限进入 if(!is_dir($dir)) return false; $handle = opendir($ ...

  2. 从零开始搭建包含多个子系统的Vue工程项目

    本文以windows为例,介绍支持多个子系统的Vue工程项目的搭建过程,相对于单一系统的工程,多个子系统引入了如下一些问题: 项目目录结构设计 打包结果设计:每个子系统可以独立发布上线 多布局实现:多 ...

  3. mybatis配置ehcache缓存

    1:在spring配置文件中加载缓存配置文件 <!-- 使用ehcache缓存 --> <bean id="ehCacheManager" class=" ...

  4. Node.js的原型继承函数util.inherits

    util.inherits(constructor, superConstructor)是一个实现对象间原型继承 的函数.JavaScript 的面向对象特性是基于原型的,与常见的基于类的不同.Jav ...

  5. 优化深度神经网络(三)Batch Normalization

    Coursera吴恩达<优化深度神经网络>课程笔记(3)-- 超参数调试.Batch正则化和编程框架 1. Tuning Process 深度神经网络需要调试的超参数(Hyperparam ...

  6. leetcode682

    class Solution { public: int calPoints(vector<string>& ops) { stack<int> ST; ; for ( ...

  7. PHP数组在循环的时候修改本身的值

    这样的修改并不是修改本身,$item就相当于赋值了一份数组中的值,就跟JAVA中的值方式传递值类型一样,我只是拿了你的值,并不是拿了你的内存地址,所已$item的改变,并不会影响数组 第一种方式就是直 ...

  8. Setting up an OpenGL development environment in ubuntu

    1.opening terminal window and entering the apt-get command for the packages: sudo apt-get install me ...

  9. ubuntu14.04 使用笔记

    这是第二次安装使用ubuntu了,虽然上一次因为不习惯和不会使用一两天就放弃了,这次坚持的时间稍微长一点,目前ubuntu的基本使用也熟悉了.但是由于ubuntu上的应用太少,常用软件,比如QQ,Ph ...

  10. opennebula 安装指定参数

    [root@opennebula opennebula-]# ./install.sh -u oneadmin -g oneadmin -k -d /home/oneadmin/ -u 指定用户-g ...