ThreadLocal

定义

ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。

其实,ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。

各个线程的ThreadLocal关联的实例互不干扰。特征:

  • ThreadLocal表示线程的"局部变量",它确保每个线程的ThreadLocal变量都是各自独立的
  • ThreadLocal适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递)
  • 使用ThreadLocal要用try ... finally结构,并在finally中清除

常用方法

  • set:为当前线程设置变量,当前ThreadLocal作为索引
  • get:获取当前线程变量,当前ThreadLocal作为索引
  • initialValue:(需要子类实现,默认mull)执行get时,发现线程本地变量为null,就会执行initialValue的内容
  • remove:清空当前线程的ThreadLocal索引与映射的元素

底层结构及逻辑

Thread对象的属性

public class Thread implements Runnable {
// .....
ThreadLocal.ThreadLocalMap threadLocals = null;
// .....
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

ThreadLocalMap对象

public class ThreadLocal<T> {
// ..... 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;
}
} /**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16; /**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table; /**
* The number of entries in the table.
*/
private int size = 0; /**
* The next size value at which to resize.
*/
private int threshold; // Default to 0 /**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
} /**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
} /**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
}
}

set值流程

源码摘要:

// java.lang.ThreadLocal#set
public void set(T value) {
Thread t = Thread.currentThread();
// map惰性创建
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
} void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

重点来关注下 java.lang.ThreadLocal.ThreadLocalMap#set 方法

private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 根据ThreadLocal对象的hash值,定位到table中的位置i
int i = key.threadLocalHashCode & (len - 1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get(); // 如果位置i不为空,且这个Entry对象的key正好是即将设置的key,那么就覆盖Entry中的value
if (k == key) {
e.value = value;
return;
} // 如果当前位置是空的,就初始化一个Entry对象放在位置i上
if (k == null) {
// 里面会调到 expungeStaleEntry
replaceStaleEntry(key, value, i);
return;
} // 如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止
} tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

结合代码,set的过程如下图

冲突解决

线性探测的方式解决hash冲突的问题,如果没有找到空闲的slot,就不断往后尝试,直到找到一个空闲的位置,插入entry

get流程

源码摘要:

// java.lang.ThreadLocal#get
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();// 调用initialValue方法
}
// java.lang.ThreadLocal.ThreadLocalMap#getEntry
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
// 可能是没有,或者hash冲突了
return getEntryAfterMiss(key, i, e);
} private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// get的时候一样是根据ThreadLocal获取到table的i值,然后查找数据拿到后会对比key是否相等
while (e != null) {
ThreadLocal<?> k = e.get();
// 相等就直接返回,不相等就继续查找,找到相等位置。
if (k == key)
return e;
if (k == null)
// 清理回收无效value、entry
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}

弱引用

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value; Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

为什么使用弱引用?

弱引用的特点:弱引用的对象拥有更短暂的生命周期。垃圾回收器线程扫描的时候,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

结合到这里的场景,当ThreadLocal在没有外部强引用的时候,一旦发生gc,key就会被回收。

内存泄露问题

因为有了弱引用,可以确保Entry的key会被内存回收掉。但是Entry的value和Entry对象本身还是没有得到回收。

如果ThreadLocal的线程一直保持运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

解决办法:在finally里面调用remove方法

扩展

InheritableThreadLocal

InheritableThreadLocal 是 JDK 本身自带的一种线程传递解决方案,以完成父线程到子线程的值传递。在创建子线程的时候,就把父线程的ThreadLocal的内容复制过去。

// java.lang.Thread#init(java.lang.ThreadGroup, java.lang.Runnable, java.lang.String, long, java.security.AccessControlContext, boolean)
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// ...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
// 复制父线程的InheritableThreadLocal内容
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); // ...
} // java.lang.ThreadLocal#createInheritedMap
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
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) {
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// 这里的value 是同一个对象
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++;
}
}
}
}

不过,子线程ThreadLocalMap里的Entry.value指向的对象和父线程是同一个

特殊场景下的缺陷

在线程池的场景下,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义。比如:

public static void main(String[] args) throws InterruptedException {
// 线程池提前创建好
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 提前创建了一个子线程 [pool-1-thread-1]
executorService.submit(() -> {
System.out.println(Thread.currentThread().getName());
});
Thread.sleep(1000); InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal();
threadLocal.set("start");
System.out.println(threadLocal.get()); // 后续,[pool-1-thread-1]线程的ThreadLocal值永远是null
executorService.submit(() -> {
System.out.println(threadLocal.get() + " -> " + Thread.currentThread().getName());
});
executorService.submit(() -> {
System.out.println(threadLocal.get() + " -> " + Thread.currentThread().getName());
});
executorService.submit(() -> {
System.out.println(threadLocal.get() + " -> " + Thread.currentThread().getName());
}); Thread.sleep(100);
System.out.println(threadLocal.get());
executorService.shutdown();
} // 输出结果
pool-1-thread-1
start
start -> pool-1-thread-2
start -> pool-1-thread-2
null -> pool-1-thread-1
start

尤其是现在都是基于框架开发,线程池一般在项目启动的时候,就创建好了。业务代码提交执行任务的时候,如果复用之前的线程,那么值就没传到子线程中去!

像这种情况,我们至少要求 把任务提交给线程池时 的ThreadLocal值传递到执行线程中。TransmittableThreadLocal的出现就是为了解决这个问题。

TransmittableThreadLocal

TransmittableThreadLocal是Alibaba开源的一个类,它继承了InheritableThreadLocal。能实现在线程池和主线程之间传递,需要配合TtlRunnable 和 TtlCallable使用。

使用示例

public static void main(String[] args) throws InterruptedException {
// 线程池提前创建好
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 提前创建了一个子线程 [pool-1-thread-1]
executorService.submit(() -> {
System.out.println(Thread.currentThread().getName());
});
Thread.sleep(1000); TransmittableThreadLocal<String> threadLocal = new TransmittableThreadLocal();
threadLocal.set("start");
System.out.println(threadLocal.get()); // 每次提交时都需要通过修饰操作(即TtlRunnable.get(task))以抓取这次提交时的TransmittableThreadLocal上下文的值
executorService.submit(TtlRunnable.get(() -> {
System.out.println(threadLocal.get() + " -> " + Thread.currentThread().getName());
}));
executorService.submit(TtlRunnable.get(() -> {
System.out.println(threadLocal.get() + " -> " + Thread.currentThread().getName());
}));
executorService.submit(TtlRunnable.get(() -> {
System.out.println(threadLocal.get() + " -> " + Thread.currentThread().getName());
})); Thread.sleep(100);
System.out.println(threadLocal.get());
executorService.shutdown();
} // 输出结果
pool-1-thread-1
start
start -> pool-1-thread-1
start -> pool-1-thread-2
start -> pool-1-thread-1
start

整个过程的完整时序图

修饰线程池

使用TTL的时候,每次提交任务时,都需要用TtlRunnable 或者 TtlCallable对任务修饰一下。这个修饰逻辑可以再线程池中完成。

通过工具类com.alibaba.ttl.threadpool.TtlExecutors完成,有下面的方法:

  • getTtlExecutor:修饰接口Executor
  • getTtlExecutorService:修饰接口ExecutorService
  • getTtlScheduledExecutorService:修饰接口ScheduledExecutorService

示例代码:

ExecutorService executorService = ...
// 额外的处理,生成修饰了的对象executorService
executorService = TtlExecutors.getTtlExecutorService(executorService); TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>(); // ===================================================== // 在父线程中设置
context.set("value-set-in-parent"); Runnable task = new RunnableTask();
Callable call = new CallableTask();
executorService.submit(task);
executorService.submit(call); // ===================================================== // Task或是Call中可以读取,值是"value-set-in-parent"
String value = context.get();

FastThreadLocal

前面分析了ThreadLocal的get和set,当遇到hash冲突的时候,会以nextIndex计算下一个位置的方式来解决hash冲突。

使用线性探测的方式解决hash冲突的问题,如果没有找到空闲的slot,就不断往后尝试,直到找到一个空闲的位置,插入entry,这种方式在经常遇到hash冲突时,影响效率。

鉴于此,netty提供了FastThreadLocal。与之配套的还有FastThreadLocalThread和FastThreadLocalRunnable。

创建FastThreadLocal对象的时候,直接把位置index(使用AtomicInteger实现)确定下来。每个FastThreadLocal都能获取到一个不重复的下标

public FastThreadLocal() {
index = InternalThreadLocalMap.nextVariableIndex();
} public static int nextVariableIndex() {
int index = nextIndex.getAndIncrement();
if (index < 0) {
nextIndex.decrementAndGet();
throw new IllegalStateException("too many thread-local indexed variables");
}
return index;
}

不过,FastThreadLocal需要配合FastThreadLocalThread使用,才能发挥它的效率。

public final void set(V value) {
if (value != InternalThreadLocalMap.UNSET) {
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
setKnownNotUnset(threadLocalMap, value);
} else {
remove();
}
}
public final V get() {
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
Object v = threadLocalMap.indexedVariable(index);// 直接定位
if (v != InternalThreadLocalMap.UNSET) {
return (V) v;
}
return initialize(threadLocalMap);
}
public Object indexedVariable(int index) {
Object[] lookup = indexedVariables;
return index < lookup.length? lookup[index] : UNSET;
} // InternalThreadLocalMap.get()
public static InternalThreadLocalMap get() {
Thread thread = Thread.currentThread();
// 判断当前Thread类型
if (thread instanceof FastThreadLocalThread) {
return fastGet((FastThreadLocalThread) thread);
} else {
return slowGet();
}
} private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {
// FastThreadLocalThread继承Thread,额外有InternalThreadLocalMap类型属性
InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();
if (threadLocalMap == null) {
thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
}
return threadLocalMap;
} private static InternalThreadLocalMap slowGet() {
// 普通Thread无InternalThreadLocalMap,但有ThreadLocal属性,在它里面存InternalThreadLocalMap等于间接有了InternalThreadLocalMap
ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = UnpaddedInternalThreadLocalMap.slowThreadLocalMap;
InternalThreadLocalMap ret = slowThreadLocalMap.get();
if (ret == null) {
ret = new InternalThreadLocalMap();
slowThreadLocalMap.set(ret);
}
return ret;
}

也就是说,如果是普通Thread使用FastThreadLocal,则需要先拿到ThreadLocal对象,然后再get到里面存的InternalThreadLocalMap。这一get过程完全是ThreadLocal的get,也需要执行hash碰撞&getEntryAfterMiss等逻辑。(有的地方称之为退化

深入理解ThreadLocal及其变种的更多相关文章

  1. 【Java】深入理解ThreadLocal

    一.前言 要理解ThreadLocal,首先必须理解线程安全.线程可以看做是一个具有一定独立功能的处理过程,它是比进程更细度的单位.当程序以单线程运行的时候,我们不需要考虑线程安全.然而当一个进程中包 ...

  2. 理解ThreadLocal背后的概念

    介绍 我之前在任何场合都没有使用过thread local,因此没有注意到它,直到最近用到它的时候. 前提信息 线程可以理解为一个单独的进程,它有自己的调用栈.在java中每一个线程都有一个调用栈或者 ...

  3. 简单理解ThreadLocal原理和适用场景

    https://blog.csdn.net/qq_36632687/article/details/79551828?utm_source=blogkpcl2 参考文章: 正确理解ThreadLoca ...

  4. 理解ThreadLocal(之二)

    想必很多朋友对ThreadLocal并不陌生,今天我们就来一起探讨下ThreadLocal的使用方法和实现原理.首先,本文先谈一下对ThreadLocal的理解,然后根据ThreadLocal类的源码 ...

  5. 理解ThreadLocal(之一)

    ThreadLocal是什么 在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路.使用这个工具类可以很简洁地编 ...

  6. 正确理解ThreadLocal

    想必很多朋友对 ThreadLocal并不陌生,今天我们就来一起探讨下ThreadLocal的使用方法和实现原理.首先,本文先谈一下对ThreadLocal的理 解,然后根据ThreadLocal类的 ...

  7. 彻底理解ThreadLocal一

    synchronized这类线程同步的机制可以解决多线程并发问题,在这种解决方案下,多个线程访问到的,都是同一份变量的内容.为了防止在多线程访问的过程中,可能会出现的并发错误.不得不对多个线程的访问进 ...

  8. 彻底理解ThreadLocal

    ThreadLocal是什么 早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路.使用这个工具类可以很简洁地 ...

  9. 深入理解ThreadLocal

    ThreadLocal是什么 早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路.使用这个工具类可以很简洁地 ...

随机推荐

  1. 611. Valid Triangle Number

    Given an array consists of non-negative integers, your task is to count the number of triplets chose ...

  2. rsync实时备份监控命令(详细大全)

    目录 一:rsync介绍 1.rsync简介 2.rsync特性 3.rsync应用场景 4.rsync的传输方式 5.Rsync传输模式 二:RSYNC使用参数 三:参数使用案例 一:rsync介绍 ...

  3. 微服务架构 | 4.2 基于 Feign 与 OpenFeign 的服务接口调用

    目录 前言 1. OpenFeign 基本知识 1.1 Feign 是什么 1.2 Feign 的出现解决了什么问题 1.3 Feign 与 OpenFeign 的区别与对比 2. 在服务消费者端开启 ...

  4. ZooKeeper 授权访问

    ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件.它是一个为分布式应用提供一致性服务的软件,提供的功 ...

  5. shell循环ping ip的写法

    #!/bin/bash for i in `seq 1 20` do if ping -w 2 -c 1 192.168.43.$i | grep "100%" > /dev ...

  6. http中的8种请求介绍

    HTTP协议的8种请求类型介绍 HTTP协议中共定义了八种方法或者叫"动作"来表明对Request-URI指定的资源的不同操作方式,具体介绍如下: OPTIONS:返回服务器针对特 ...

  7. 布客&#183;ApacheCN 编程/大数据/数据科学/人工智能学习资源 2020.1

    公告 我们正在招募项目负责人,完成三次贡献可以申请,请联系片刻(529815144).几十个项目等你来申请和参与,不装逼的朋友,我们都不想认识. 薅资本主义羊毛的 CDNDrive 计划正式启动! 我 ...

  8. Dart 2.16 现已发布

    文 / Michael Thomsen, Dart 产品经理 Dart 2.16 正式发布 Dart 2.16 正式版已于上周发布.尽管没有新的语言特性加入,但本次版本发布包含了数个问题修复 (包括对 ...

  9. LCT 入门

    这是一份 \(\rm LCT\) 入门总结. 关于 \(\rm LCT\) 的复杂度这里不会提及,只会记录 \(\rm LCT\) 的基本操作和经典例题,但神奇的 \(\rm LCT\) 虽然常数巨大 ...

  10. select 级联选择

    转载请注明来源:https://www.cnblogs.com/hookjc/ <script   language="javascript">   <!--   ...