在上家公司做spark的任务调度系统时,碰到过这么一个需求:

1.任务由一个线程执行,同时在执行过程中会创建多个线程执行子任务,子线程在执行子任务时又会创建子线程执行子任务的子任务。整个任务结构就像一棵高度为3的树。

2.每个任务在执行过程中会生成一个任务ID,我需要把这个任务ID传给子线程执行的子任务,子任务同时也会生成自己的任务ID,并把自己的任务ID向自己的子任务传递。

流程可由下图所示

解决方案有很多,比如借助外部存储如数据库,或者自己在内存中维护一个存储ID的数据结构。考虑到系统健壮性和可维护性,最后采用了jdk中的InheritableThreadLocal来实现这个需求。

来看下InheritableThreadLocal的结构

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

InheritableThreadLocal继承自ThreadLocal,ThreadLocal可以说是一个存储线程私有变量的容器(当然这个说法严格来说不准确,后面我们就知道为什么),而InheritableThreadLocal正如Inheritable所暗示的那样,它是可继承的:使用它可使子线程继承父线程的所有线程私有变量。因此我写了个工具类,底层使用InheritableThreadLocal来存储任务的ID,并且使该ID能够被子线程继承。

public class InheritableThreadLocalUtils {

    private static final ThreadLocal<Integer> local = new InheritableThreadLocal<>();

    public static void set(Integer t) {
local.set(t);
} public static Integer get() {
return local.get();
} public static void remove() {
local.remove();
}
}

可以通过这个工具类的set方法和get方法分别实现任务ID的存取。然而在Code Review的時候,有同事觉得我这代码写的有问题:原因大概是InheritableThreadLocal在这里只有一个,子线程的任务ID在存储的时候会相互覆盖掉。真的会这样吗?为此我们用代码测试下:


public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for(int i=0;i<10;i++){
executorService.execute(new TaskThread(i));
} } static class TaskThread implements Runnable{ Integer taskId; public TaskThread(Integer taskId) {
this.taskId = taskId;
} @Override
public void run() {
InheritableThreadLocalUtils.set(taskId);
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(InheritableThreadLocalUtils.get());
}
});
}
}

这段代码开启了10个线程标号从0到9,我们在每个线程中将对应的标号存储到InheritableThreadLocal,然后开启一个子线程,在子线程中获取InheritableThreadLocal中的变量。最后的结果如下

每个线程都准确的获取到了父线程对应的ID,可见并没有覆盖的问题。InheritableThreadLocal确实是用来存储和获取线程私有变量的,但是真实的变量并不是存储在这个InheritableThreadLocal对象中,它只是为我们存取线程私有变量提供了入口而已。因为InheritableThreadLocal只是在ThreadLocal的基础上提供了继承功能,为了弄清这个问题我们研究下ThreadLocal的源码。

2. ThreadLocal源码解析

ThreadLocal主要方法有两个,一个set用来存储线程私有变量,一个get用来获取线程私有变量。

2.1 set方法源码解析

/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

Thread t = Thread.currentThread()获取了当前线程实例t,继续跟进第二行的getMap方法,

/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

t是线程实例,而threadLocals明显是t的一个成员变量,进入一探究竟

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap是个什么结构?

static class ThreadLocalMap {

    /**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value; Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

ThreadLocalMap是类Thread中的一个静态内部类,看起来像一个HashMap,但和HashMap又有些不一样(关于它们的区别后面会讲),那我们就把它当一个特殊的HashMap好了。因此set方法中第二行代码

ThreadLocalMap map = getMap(t)是通过线程实例t得到一个ThreadLocalMap。接下来的代码

if (map != null)
map.set(this, value);
else
createMap(t, value);
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

如果这个threadlocalmap为null,先创建一个threadlocalmap,然后以当前threadlocal对象为key,以要存储的变量为值存储到threadlocalmap中。


2.2 get方法源码解析


/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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();
}

首先获取当前线程实例t,然后通过getMap(t)方法得到threadlocalmap(ThreadLocalMap是Thread的成员变量)。若这个map不为null,则以threadlocal为key获取线程私有变量,否则执行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;
}
protected T initialValue() {
return null;
}

首先获取threadlocal的初始化值,默认为null,可以通过重写自定义该值;如果threadlocalmap为null,先创建一个;以当前threadlocal对象为key,以初始化值为value存入map中,最后返回这个初始化值。

2.3 ThreadLocal源码总结

总的来说,ThreadLocal的源码并不复杂,但是逻辑很绕。现总结如下:

  • 1.ThreadLocal对象为每个线程存取私有的本地变量提供了入口,变量实际存储在线程实例的内部一个叫ThreadLocalMap的数据结构中。
  • 2.ThreadLocalMap是一个类HashMap的数据结构,Key为ThreadLoca对象(其实是一个弱引用),Value为要存储的变量值。
  • 3.使用ThreadLocal进行存取,其实就是以ThreadLocal对象为隐含的key对各个线程私有的Map进行存取。


可以用下图的内存图像帮助理解和记忆

3. ThreadLocalMap详解

先看源码

static class ThreadLocalMap {

    /**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value; Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

3.1 ThreadLocalMap的key为弱引用

ThreadLocalMap的key并不是ThreadLocal,而是WeakReference<ThreadLocal>,这是一个弱引用,说它弱是因为如果一个对象只被弱引用引用到,那么下次垃圾收集时就会被回收掉。如果引用ThreadLocal对象的只有ThreadLocalMap的key,那么下次垃圾收集过后该key就会变为null。

3.2 为何要用弱引用

减少了内存泄漏。试想我曾今存储了一个ThreadLocal对象到ThreadLocalMap中,但后来我不需要这个对象了,只有ThreadLocalMap中的key还引用了该对象。如果这是个强引用的话,该对象将一直无法回收。因为我已经失去了其他所有该对象的外部引用,这个ThreadLocal对象将一直存在,而我却无法访问也无法回收它,导致内存泄漏。又因为ThreadLocalMap的生命周期和线程实例的生命周期一致,只要该线程一直不退出,比如线程池中的线程,那么这种内存泄漏问题将会不断积累,直到导致系统奔溃。而如果是弱引用的话,当ThreadLocal失去了所有外部强引用的话,下次垃圾收集该ThreadLocal对象将被回收,对应的ThreadLocalMap中的key将为null。下次get和set方法被执行时将会对key为null的Entry进行清理。有效的减少了内存泄漏的可能和影响。

3.3 如何真正避免内存泄漏

  • 及时调用ThreadLocal的remove方法
  • 及时销毁线程实例

4. 总结

ThreadLocal为我们存取线程私有变量提供了入口,变量实际存储在线程实例的map结构中;使用它可以让每个线程拥有一份共享变量的拷贝,以非同步的方式解决多线程对资源的争用

ThreadLocal原理深入解析的更多相关文章

  1. ThreadLocal原理大解析

    今天呢,和大家聊一下ThreadLocal. 1. 是什么? JDK1.2提供的的一个线程绑定变量的类. 他的思想就是:给每一个使用到这个资源的线程都克隆一份,实现了不同线程使用不同的资源,且该资源之 ...

  2. java基础解析系列(七)---ThreadLocal原理分析

    java基础解析系列(七)---ThreadLocal原理分析 目录 java基础解析系列(一)---String.StringBuffer.StringBuilder java基础解析系列(二)-- ...

  3. DNS原理及其解析过程 精彩剖析

    本文章转自下面:http://369369.blog.51cto.com/319630/812889 DNS原理及其解析过程 精彩剖析 网络通讯大部分是基于TCP/IP的,而TCP/IP是基于IP地址 ...

  4. GBDT算法原理深入解析

    GBDT算法原理深入解析 标签: 机器学习 集成学习 GBM GBDT XGBoost 梯度提升(Gradient boosting)是一种用于回归.分类和排序任务的机器学习技术,属于Boosting ...

  5. ThreadLocal原理及其实际应用

    前言 java猿在面试中,经常会被问到1个问题: java实现同步有哪几种方式? 大家一般都会回答使用synchronized, 那么还有其他方式吗? 答案是肯定的, 另外一种方式也就是本文要说的Th ...

  6. 老调重弹:JDBC系列之<驱动加载原理全面解析) ----转

      最近在研究Mybatis框架,由于该框架基于JDBC,想要很好地理解和学习Mybatis,必须要对JDBC有较深入的了解.所以便把JDBC 这个东东翻出来,好好总结一番,作为自己的笔记,也是给读者 ...

  7. DNS原理及其解析过程【精彩剖析】(转)

      2012-03-21 17:23:10 标签:dig wireshark bind nslookup dns 原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否 ...

  8. C++多态的实现及原理详细解析

    C++多态的实现及原理详细解析 作者: 字体:[增加 减小] 类型:转载   C++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型 ...

  9. ThreadLocal源码解析

    主要用途 1)设计线程安全的类 2)存储无需共享的线程信息 设计思路 ThreadLocalMap原理 1)对象存储位置-->当前线程的ThreadLocalMap ThreadLocalMap ...

随机推荐

  1. FastAdmin 的 API 可以分级吗?

    FastAdmin 的 API 可以分级吗? 有小伙伴问 FastAdmin 的API 可以分别吗,使用 / 出现错误. Karson 的说明是: 完全支持的,默认是使用.进行分隔的,如果需要/,请开 ...

  2. springboot注册bean失败

    启动的主类应该放在和其他包一样的目录,不能放在一个目录里面

  3. 如何将angular-ui-bootstrap的分页组件封装成一个指令

    准备工作: (1)一如既往的我还是使用了requireJS进行js代码的编译 (2)必须引入angualrJS , ui-bootstrap-tpls-1.3.2.js , bootstrap.css ...

  4. TCP/IP概念简述

    这里所说的是广义上的TCP/IP协议群,而不是特指TCP和IP这两种具体的协议.既然是协议群,那么都有哪些协议呢?我们先不着急回答这个问题,因为要弄清楚这个问题,首先得了解另两件事,就是为啥要有这个协 ...

  5. 基于Oracle的EntityFramework的WEBAPI2的实现(一)——准备工作

    目前在.net的范围内,好的而且方便的ORM的真的不是很多,与VS集成方便的也就当属EntityFramework(以下简称EF,不知道为什么,总EF这个缩写好不专业).但是,好多公司使用的又是ORA ...

  6. OPCClient和OPCServer在Windows上运行方式的恩怨

    http://www.diangon.com/wenku/PLC/201504/00021970.html 近段时间,遇到不少人都被OPCClient与OPCServer之间的通讯搞得头大,通过几次远 ...

  7. c#通过app.manifest使程序 右键 以管理员身份运行

    c#通过app.manifest使程序以管理员身份运行 时间:2013-06-27 22:47来源:网络收集+本站整理 作者:jtydl 点击: 1175 次 微软在Windows Vista开始引入 ...

  8. Java文件的写入

    写文件与读文件类似,可以是以字节为单位写入,可以是以字符为单位写入. 对应读操作FileOutputStream是以字节为单位进行写入的: FileOutputStream fileOutputStr ...

  9. jmeter数据关联_后置处理器_正则表达式提取器

  10. DOM的基本概念

    1.DOM的基本概念 DOM是文档对象模型,这种模型为树模型:文档是指标签文档:对象是指文档中每个元素:模型是指抽象化的东西. 2.Window对象操作 一.属性和方法: 属性(值或者子对象): op ...