Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱
MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina.com

ThreadLocal 简介 案例 源码分析 MD


目录

目录
先来看基本用法
对ThreadLocal的理解
ThreadLocal 详解
正确理解 ThreadLocal
ThreadLocal 源码分析
构造方法
set 方法
ThreadLocalMap
get 方法
总结
一个类型转换的坑

先来看基本用法

class Person {ThreadLocal<Long> longLocal = new ThreadLocal<Long>(); //创建一个用于保存 Long 类型数据的 ThreadLocalThreadLocal<String> stringLocal = new ThreadLocal<String>() {//创建一个用于保存 String 类型数据的 ThreadLocal@Overrideprotected String initialValue() {return Thread.currentThread().getName(); //初始化数据第一种方式:重写 initialValue 方法(推荐方式)}};//ThreadLocal.withInitial(()-> "返回初始值"); //初始化数据第三种方式,在1.8中添加的APIpublic void setLongValue() {longLocal.set(Thread.currentThread().getId()); //初始化数据第二种方式:调用 set 方法}public long getLong() {return longLocal.get();}public String getString() {return stringLocal.get();}
}

public class Test {public static void main(String[] args) throws InterruptedException {final Person person = new Person();person.setLongValue(); //设置main线程中的对象的值System.out.println("A:" + person.getLong()); //1System.out.println("A:" + person.getString()); //mainThread thread = new Thread() {public void run() {person.setLongValue(); //设置子线程中的对象的值System.out.println("B:" + person.getLong()); //10System.out.println("B:" + person.getString()); //Thread-0};};thread.start();thread.join(); //效果等同于同步System.out.println("C:" + person.getLong()); //1System.out.println("C:" + person.getString()); //main}
}

打印结果:

A:1
A:main
B:10
B:Thread-0
C:1
C:main

对ThreadLocal的理解

参考

常用的几个 API

ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
protected T initialValue() //一般是用来在使用时进行重写的,它是一个延迟加载方法public void set(T value)
public T get()
public void remove()

ThreadLocal提供了线程独有的局部变量,可以在整个线程存活的过程中随时取用,极大地方便了一些逻辑的实现。常见的ThreadLocal用法有:

  • 存储单个线程上下文信息
  • 使变量线程安全:变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了
  • 减少参数传递

原理
ThreadLocal里类型的变量,其实是放入了当前Thread里。每个Thread都有一个threadLocals,它是一个map,这个map的entry是ThreadLocal.ThreadLocalMap.Entry,具体的key和value类型分别是ThreadLocal和Object。

注:实际是ThreadLocal的弱引用 WeakReference<ThreadLocal<?>>,但可以先简单理解为ThreadLocal。

对于一个普通的map,取其中某个key对应的值分两步:

  • 找到这个map;
  • 在map中,给出key,得到value。

想取出我们存放在当前线程里的map里的值同样需要这两步,但是,我们不需要告诉jvm map在哪儿,因为jvm知道当前线程,也知道其局部变量map。所以最终的get操作只需要知道key(即ThreadLocal)就行了:longLocal.get()

为什么key使用弱引用
不妨反过来想想,如果使用强引用,当ThreadLocal对象(假设为ThreadLocal@123456)的引用(即longLocal,是一个强引用,指向ThreadLocal@123456)被回收了,ThreadLocalMap本身依然还持有ThreadLocal@123456的强引用,如果没有手动删除这个key,则ThreadLocal@123456不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收,可以认为这导致Entry内存泄漏。

那使用弱引用的好处呢?

如果使用弱引用,那指向ThreadLocal@123456对象的引用就两个:longLocal强引用,和ThreadLocalMap中Entry的弱引用。一旦longLocal被回收,则指向ThreadLocal@123456的就只有弱引用了,在下次gc的时候,这个ThreadLocal@123456就会被回收。

那么问题来了,ThreadLocal@123456对象只是作为ThreadLocalMap的一个key而存在的,现在它被回收了,但是它对应的value并没有被回收,内存泄露依然存在!而且key被删了之后,变成了null,value更是无法被访问到了!针对这一问题,ThreadLocalMap类的设计本身已经有了这一问题的解决方案,那就是在每次get()/set()/remove()ThreadLocalMap中的值的时候,会自动清理key为null的value。如此一来,value也能被回收了。

为什么不对value使用弱引用
答案显而易见,假设往ThreadLocalMap里存了一个value,gc过后value便消失了,那就无法使用ThreadLocalMap来达到存储全线程变量的效果了。

内存泄漏问题
弱引用一定程度上回收了无用对象,但前提是开发者手动清理掉ThreadLocal对象的强引用(如longLocal)。只要线程一直不死,ThreadLocalMap的key-value一直在涨。
解决方法是,当某个ThreadLocal变量不再使用时,调用 remove() 方法删除该key。

使用线程池的问题
使用线程池可以达到线程复用的效果,但是归还线程之前记得清除ThreadLocalMap,要不然再取出该线程的时候,ThreadLocal变量还会存在。这就不仅仅是内存泄露的问题了,整个业务逻辑都可能会出错。

所以ThreadLocal最好还是不要和线程池一起使用。

ThreadLocal 详解

参考

正确理解 ThreadLocal

首先,ThreadLocal不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set()到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的,各个线程中访问的是不同的对象。

另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new对象的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的

如果ThreadLocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。

下面来看一个hibernate中典型的ThreadLocal的应用:

private static final ThreadLocal threadSession = new ThreadLocal();public static Session getSession() throws InfrastructureException {Session s = (Session) threadSession.get();try {if (s == null) {s = getSessionFactory().openSession(); //创建一个sessionthreadSession.set(s);}} catch (HibernateException ex) {throw new InfrastructureException(ex);}return s;
}

可以看到在getSession()方法中,首先判断当前线程中有没有放进去session,如果还没有,那么通过sessionFactory().openSession()来创建一个session,再将session set到线程中,实际是放到当前线程的ThreadLocalMap这个map中,这时,对于这个session的唯一引用就是当前线程中的那个ThreadLocalMap,而threadSession作为这个值的key,要取得这个session可以通过threadSession.get()来得到,里面执行的操作实际是先取得当前线程中的ThreadLocalMap,然后将threadSession作为key将对应的值取出。

这个session相当于线程的私有变量,而不是public的。显然,其他线程中是取不到这个session的,他们也只能取到自己的ThreadLocalMap中的东西。要是session是多个线程共享使用的,那还不乱套了。

试想如果不用ThreadLocal怎么来实现呢?可能就要在action中创建session,然后把session一个个传到service和dao中,这可够麻烦的。或者可以自己定义一个静态的map,将当前thread作为key,创建的session作为值,put到map中,应该也行,这也是一般人的想法。但事实上,ThreadLocal的实现刚好相反,它是在每个线程中有一个map,而将ThreadLocal实例作为key,这样每个map中的项数很少,而且当线程销毁时相应的东西也一起销毁了

总之,ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。

归纳了两点:

  • 每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
  • 将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。

当然如果要把本来线程共享的对象通过ThreadLocal.set()放到线程中也可以,可以实现避免参数传递的访问方式,但是要注意get()到的是那同一个共享对象,并发访问问题要靠其他手段来解决。但一般来说线程共享的对象通过设置为某类的静态变量就可以实现方便的访问了,似乎没必要放到线程中。

ThreadLocal的应用场合,我觉得最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。

ThreadLocal 源码分析

API

ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
protected T initialValue() //一般是用来在使用时进行重写的,它是一个延迟加载方法public void set(T value)
public T get()
public void remove()

变量

private final int threadLocalHashCode = nextHashCode();  //唯一的实例变量,而且还是不可变的
private static int nextHashCode = 0;  //静态变量,表示即将分配的下一个ThreadLocal实例的threadLocalHashCode的值
private static final int HASH_INCREMENT = 0x61c88647; //常量,表示了连续分配的两个ThreadLocal实例的threadLocalHashCode值的增量

构造方法

可以来看一下创建一个 ThreadLocal 实例即 new ThreadLocal() 时做了哪些操作

//Creates a thread local variable. @see #withInitial(java.util.function.Supplier)
public ThreadLocal() {
}

从上面看到构造函数 ThreadLocal() 里什么操作都没有,唯一的操作是这句:

private final int threadLocalHashCode = nextHashCode();  

而 nextHashCode() 就是将 ThreadLocal 类的下一个 hashCode 值即 nextHashCode 的值赋给实例的 threadLocalHashCode,然后 nextHashCode 的值增加 HASH_INCREMENT 这个值。

因此,ThreadLocal实例的变量只有这个threadLocalHashCode,而且是final的,用来区分不同的ThreadLocal实例,ThreadLocal类主要是作为工具类来使用,那么ThreadLocal.set()进去的对象是放在哪儿的呢?

set 方法

看一下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 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 = t.threadLocals; //返回当前线程t中的一个成员变量 threadLocalsif (map != null) map.set(this, value); //将该 ThreadLocal 实例(而不是当前线程)作为key,要保持的对象作为值else t.threadLocals = new ThreadLocalMap(this, value);
}

也就是将该 ThreadLocal 实例作为key,要保持的对象作为值,设置到当前线程的 ThreadLocalMap 中

ThreadLocalMap

这个 ThreadLocalMap 类是 ThreadLocal 中定义的内部类,但是它的实例却用在Thread类中:

public class Thread implements Runnable {  //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 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<?>> {Object value;//The value associated with this ThreadLocal.Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}//...
}

可以看到 ThreadLocalMap 的 Entry 继承了 WeakReference,并且使用 ThreadLocal 作为键值。

get 方法

看一下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 initialValue method.
//@return the current thread's value of this thread-local
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = t.threadLocals;if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this); //注意这里获取键值对传进去的是 ThreadLocal 实例,而不是当前线程 tif (e != null) return (T)e.value;}return setInitialValue();
}

再看 setInitialValue() 方法的具体实现:

//Variant of set() to establish initialValue. Used instead of set() in case user has overridden the set() method.
//@return the initial value
private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = t.threadLocals; if (map != null) map.set(this, value);else t.threadLocals = new ThreadLocalMap(this, value);return value;
}

可以看到,除了添加第一行和最后一行外,其他逻辑和 set 方法完全一样。

第一行是调用 初始化方法 initialValue() 获取初始值,中间就是保持的这个值,最后一行就是返回这个值。

protected T initialValue() {return null;
}

总结

  • 通过 ThreadLocal 创建的对象是存储在每个线程自己的 threadLocals 集合中的
  • 集合 threadLocals 的类型为 ThreadLocalMap,存储的实体为WeakReference<ThreadLocal<?>>,键为 ThreadLocal 对象
  • set 方法就是将该 ThreadLocal 实例作为 key,将要保持的对象作为值,设置到当前线程的 threadLocals 中
  • 在进行 get 之前,必须先 set,或者重写 initialValue() 方法,否则返回的是 null

一个类型转换的坑

如下代码的执行结果是什么:

public class Test {public static void main(String[] args) {System.out.println(new Person().getLong());System.out.println(new Person().getLong2());}
}class Person {ThreadLocal<Long> longLocal = new ThreadLocal<Long>();public Long getLong() {return longLocal.get();}public long getLong2() {return longLocal.get();}
}

空指针异常

null
Exception in thread "main" java.lang.NullPointerExceptionat Person.getLong2(Test.java:17)at Test.main(Test.java:5)

第二种写法(返回基本类型 long 而非包装类型 Long)是不是感觉很坑?

2019-1-21

转载于:https://www.cnblogs.com/baiqiantao/p/10300624.html

ThreadLocal 简介 案例 源码分析 MD相关推荐

  1. Java Review - 线程池中使用ThreadLocal不当导致的内存泄漏案例源码分析

    文章目录 概述 Why 内存泄露 ? 在线程池中使用ThreadLocal导致的内存泄漏 概述 ThreadLocal的基本使用我们就不赘述了,可以参考 每日一博 - ThreadLocal VS I ...

  2. Java Review - 线程池资源一直不被释放案例源码分析

    文章目录 概述 问题复现 源码分析 小结 概述 在日常开发中为了便于线程的有效复用,经常会用到线程池,然而使用完线程池后如果不调用shutdown关闭线程池,则会导致线程池资源一直不被释放. 下面通过 ...

  3. ThreadLocal介绍以及源码分析

    ThreadLocal 线程主变量 前面部分引用其他优秀博客,后面源码自己分析的,如有冒犯请私聊我. 用Java语言开发的同学对 ThreadLocal 应该都不会陌生,这个类的使用场景很多,特别是在 ...

  4. threadlocal用法_ThreadLocal源码分析

    java.lang.ThreadLocal类在平时的开发中很少用到,勾勾工作5年多一直没有用过.最近在学习spring源码时发现很多地方用到,并且这个类是面试高频题目,不明白为什么大厂喜欢考察这个类的 ...

  5. netty-客户端.group()方法 源码分析.md

    应用程序代码 官方demo /** Copyright 2012 The Netty Project** The Netty Project licenses this file to you und ...

  6. 《黑马旅游网》综合案例源码+分析

    1 前言 为了巩固web基础知识,提升综合运用能力,故而讲解此案例.要求,每位同学能够独立完成此案例. 2 项目导入 点击绿色+按钮 选择travel项目的pom.xml文件,点击ok,完成项目导入. ...

  7. 【Java】Java中ThreadLocal简介以及源码

    文章目录 1.概述 1.1 什么是ThreadLocal变量 2.API 方法 4.3 ThreadLocalMap 4.1 ThreadLocal 4.1.1 set 4.1.2 get 4.1.2 ...

  8. Netty工作笔记0044---Netty案例源码分析

    技术交流QQ群[JAVA,C++,Python,.NET,BigData,AI]:170933152 来看看前面写的案例 可以看到这个bossgroup还有workergroup的底层代码 Multi ...

  9. JAVA单元测试——常用注解(案例源码分析)

    添加配置junit5 <!-- org.junit.jupiter/junit-jupiter-engine --> <dependency><groupId>or ...

最新文章

  1. keras 等深度学习官网文档 API
  2. SQL SERVER中的三种获得自增长ID的方法
  3. [转贴]漫谈计算机图书翻译的四种境界
  4. 新版本idea的run/debug configuration
  5. case when then的用法
  6. 51单片机定时器初值的计算
  7. [Machine Learning]朴素贝叶斯(NaiveBayes)
  8. iredmail开源邮件系统部署
  9. 瑜伽扭身祈祷式动作教程
  10. 你说你懂计算机网络,那这些你都知道吗
  11. AI 场景存储优化:云知声超算平台基于 JuiceFS 的存储实践
  12. win7计算机盘共享,win7电脑如何共享文件夹 win7电脑共享文件夹操作方法
  13. 百分点数据科学实验室:产品生命周期管理创新应用落地实践
  14. Transact使用
  15. TensorFlow-gpu使用方法
  16. LINKERD 2.11 中文实战手册
  17. 软件发明专利实例_软件发明专利思路
  18. 程序员坐牢会被安排写代码吗?
  19. PHP连接MySQL服务器
  20. 智能控制——模糊数学及控制

热门文章

  1. 【python教程入门】Python的机器学习库有哪些?
  2. 【3D游戏建模】3D建模师的发展前景
  3. 次世代3d游戏建模,零基础的小白可以学吗?
  4. 【TensorFlow】TensorFlow从浅入深系列之五 -- 教你详解滑动平均模型
  5. 【资源下载】512页IBM沃森研究员Charu最新2018著作《神经网络与深度学习》(附下载链接)
  6. DEKR 解构式关键点回归(一):算法思想与原理
  7. 数据预处理 | 机器学习之特征工程
  8. 学习l1图做图像分析
  9. android冒烟测试自动化,安卓自动化测试总结(五)——测试结果发送及内容展示...
  10. 三阶段dea模型 matlab源程序,三阶段DEA模型SFA二阶段剔除过程