记得在一次面试中被问到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. 1122 Hamiltonian Cycle

    题意:包含图中所有结点的简单环称为汉密尔顿环.给出无向图,然后给出k个查询,问每个查询是否是汉密尔顿环. 思路:根据题目可知,我们需要判断一下几个条件:(1).首先保证给定的环相邻两结点是连通的:(2 ...

  2. python开发函数进阶:生成器表达式&各种推导式

    一,生成器表达式 #生成器表达式比列表解析更省内存,因为惰性运算 #!/usr/bin/env python #_*_coding:utf-8_*_ new_2 = (i*i for i in ran ...

  3. usb设备驱动程序

    韦老师写的,供参考 /*  * drivers\hid\usbhid\usbmouse.c  */ #include <linux/kernel.h> #include <linux ...

  4. 基于LVS的负载均衡实现

    一 什么是负载均衡 负载均衡,英文名称为Load Balance,其意思就是分摊到多个操作单元上进行执行,例如Web服务器.FTP服务器.企业关键应用服务器和其它关键任务服务器等,从而共同完成工作任务 ...

  5. 搭建httpd服务

    实验环境:CentOS7 实验步骤: 安装httpd服务:yum -y install httpd 关闭SELinux:setenforce 0 禁用防火墙策略:iptables -F 启动httpd ...

  6. pcs与crmsh命令比较

    一.概念 1.crmsh This project is not part of the GNU Project. Pacemaker command line interface for manag ...

  7. temp5

  8. Apple Ad Hoc

    Apple Ad Hoc 发布测试 App只能通过Ad Hoc分享给绑定我们账号的设备上,所以至是一百台 1.官网member Center创建Ad Hoc证书 2.在官网下载Ad Hoc证书到mac ...

  9. 201671010127 2016—2017-2 java编程中遇到的问题

    学习了Java的一些基本语法后,心里的激动无法按捺,总是比较Java与C语言语法的区别,一有闲时间就会用刚学的Java基本语法写一些简单的程序.这不,一不小心又陷入了困难,本人在此诚挚的请教各位园友, ...

  10. SQL Server2012中的Throw语句尝试 RAISERROR和THROW比较

    SQL SERVER2012实现了类似C#抛出异常的Throw语句.相比较于SQL Server2005之前使用@@ERROR,和SQL Server2005之后使用RAISERROR()引发异常都是 ...