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

ThreadLocal 简介 案例 源码分析 MD


目录

先来看基本用法

  1. class Person {
  2. ThreadLocal<Long> longLocal = new ThreadLocal<Long>(); //创建一个用于保存 Long 类型数据的 ThreadLocal
  3. ThreadLocal<String> stringLocal = new ThreadLocal<String>() {//创建一个用于保存 String 类型数据的 ThreadLocal
  4. @Override
  5. protected String initialValue() {
  6. return Thread.currentThread().getName(); //初始化数据第一种方式:重写 initialValue 方法(推荐方式)
  7. }
  8. };
  9. //ThreadLocal.withInitial(()-> "返回初始值"); //初始化数据第三种方式,在1.8中添加的API
  10. public void setLongValue() {
  11. longLocal.set(Thread.currentThread().getId()); //初始化数据第二种方式:调用 set 方法
  12. }
  13. public long getLong() {
  14. return longLocal.get();
  15. }
  16. public String getString() {
  17. return stringLocal.get();
  18. }
  19. }
  1. public class Test {
  2. public static void main(String[] args) throws InterruptedException {
  3. final Person person = new Person();
  4. person.setLongValue(); //设置main线程中的对象的值
  5. System.out.println("A:" + person.getLong()); //1
  6. System.out.println("A:" + person.getString()); //main
  7. Thread thread = new Thread() {
  8. public void run() {
  9. person.setLongValue(); //设置子线程中的对象的值
  10. System.out.println("B:" + person.getLong()); //10
  11. System.out.println("B:" + person.getString()); //Thread-0
  12. };
  13. };
  14. thread.start();
  15. thread.join(); //效果等同于同步
  16. System.out.println("C:" + person.getLong()); //1
  17. System.out.println("C:" + person.getString()); //main
  18. }
  19. }

打印结果:

  1. A1
  2. Amain
  3. B10
  4. BThread-0
  5. C1
  6. Cmain

对ThreadLocal的理解

参考

常用的几个 API

  1. ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
  2. protected T initialValue() //一般是用来在使用时进行重写的,它是一个延迟加载方法
  3. public void set(T value)
  4. public T get()
  5. 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的应用:

  1. private static final ThreadLocal threadSession = new ThreadLocal();
  2. public static Session getSession() throws InfrastructureException {
  3. Session s = (Session) threadSession.get();
  4. try {
  5. if (s == null) {
  6. s = getSessionFactory().openSession(); //创建一个session
  7. threadSession.set(s);
  8. }
  9. } catch (HibernateException ex) {
  10. throw new InfrastructureException(ex);
  11. }
  12. return s;
  13. }

可以看到在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

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

变量

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

构造方法

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

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

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

  1. private final int threadLocalHashCode = nextHashCode();

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

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

set 方法

看一下set()方法:

  1. //Sets the current thread's copy of this thread-local variable to the specified value.
  2. //Most subclasses will have no need to override this method, relying solely on the initialValue method to set the values of thread-locals.
  3. //@param value :the value to be stored in the current thread's copy of this thread-local.
  4. public void set(T value) {
  5. Thread t = Thread.currentThread(); //取得当前线程
  6. ThreadLocalMap map = t.threadLocals; //返回当前线程t中的一个成员变量 threadLocals
  7. if (map != null) map.set(this, value); //将该 ThreadLocal 实例(而不是当前线程)作为key,要保持的对象作为值
  8. else t.threadLocals = new ThreadLocalMap(this, value);
  9. }

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

ThreadLocalMap

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

  1. public class Thread implements Runnable {
  2. //ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class.
  3. ThreadLocal.ThreadLocalMap threadLocals = null;
  4. }

我们继续取看 ThreadLocalMap 的实现

  1. static class ThreadLocalMap {
  2. //The entries in this hash map extend WeakReference, using its main ref field as the key (which is always a ThreadLocal object).
  3. //Note that null keys mean that the key is no longer referenced, so the entry can be expunged擦去、移除 from table.
  4. //Such entries are referred to as "stale陈旧的 entries" in the code that follows.
  5. static class Entry extends WeakReference<ThreadLocal<?>> {
  6. Object value;//The value associated with this ThreadLocal.
  7. Entry(ThreadLocal<?> k, Object v) {
  8. super(k);
  9. value = v;
  10. }
  11. }
  12. //...
  13. }

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

get 方法

看一下get()方法:

  1. //Returns the value in the current thread's copy of this thread-local variable.
  2. //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.
  3. //@return the current thread's value of this thread-local
  4. public T get() {
  5. Thread t = Thread.currentThread();
  6. ThreadLocalMap map = t.threadLocals;
  7. if (map != null) {
  8. ThreadLocalMap.Entry e = map.getEntry(this); //注意这里获取键值对传进去的是 ThreadLocal 实例,而不是当前线程 t
  9. if (e != null) return (T)e.value;
  10. }
  11. return setInitialValue();
  12. }

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

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

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

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

  1. protected T initialValue() {
  2. return null;
  3. }

总结

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

一个类型转换的坑

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

  1. public class Test {
  2. public static void main(String[] args) {
  3. System.out.println(new Person().getLong());
  4. System.out.println(new Person().getLong2());
  5. }
  6. }
  7. class Person {
  8. ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
  9. public Long getLong() {
  10. return longLocal.get();
  11. }
  12. public long getLong2() {
  13. return longLocal.get();
  14. }
  15. }

空指针异常

  1. null
  2. Exception in thread "main" java.lang.NullPointerException
  3. at Person.getLong2(Test.java:17)
  4. at Test.main(Test.java:5)

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

2019-1-21

ThreadLocal 简介 案例 源码分析 MD的更多相关文章

  1. ThreadLocal和ThreadLocalMap源码分析

    目录 ThreadLocal和ThreadLocalMap源码分析 背景分析 定义 例子 源码分析 ThreadLocalMap源码分析 ThreadLocal源码分析 执行流程总结 源码分析总结 T ...

  2. ThreadLocal介绍以及源码分析

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

  3. ThreadLocal应用及源码分析

    ThreadLocal 基本使用 ThreadLocal 的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传 ...

  4. ThreadLocal与ThreadLocalMap源码分析

    ThreadLocal类 该类主要用于不同线程存储自己的线程本地变量.本文先通过一个示例简单介绍该类的使用方法,然后从ThreadLocal类的初始化.存储结构.增删数据和hash值计算等几个方面,分 ...

  5. AQS简介以及源码分析

    参考链接:https://www.jianshu.com/p/da9d051dcc3d 参考链接:https://www.cnblogs.com/waterystone/p/4920797.html

  6. 并发编程(四)—— ThreadLocal源码分析及内存泄露预防

    今天我们一起探讨下ThreadLocal的实现原理和源码分析.首先,本文先谈一下对ThreadLocal的理解,然后根据ThreadLocal类的源码分析了其实现原理和使用需要注意的地方,最后给出了两 ...

  7. 并发-ThreadLocal源码分析

    ThreadLocal源码分析 参考: http://www.cnblogs.com/dolphin0520/p/3920407.html https://www.cnblogs.com/coshah ...

  8. ThreadLocal定义、使用案例及源码分析

    原文连接:(http://www.studyshare.cn/blog/details/1165/0 ) 一.ThreadLocal定义 jdk官方文档定义是:该类提供线程局部变量. 这些变量与其正常 ...

  9. Linux内核分析(一)---linux体系简介|内核源码简介|内核配置编译安装

    原文:Linux内核分析(一)---linux体系简介|内核源码简介|内核配置编译安装 Linux内核分析(一) 从本篇博文开始我将对linux内核进行学习和分析,整个过程必将十分艰辛,但我会坚持到底 ...

随机推荐

  1. 步步为营-12-Dictionary-翻译

    说明:https://pan.baidu.com/s/1nvPqhDJ所需文件在此目录下对应的位置 1 先做一个简单的英汉翻译词典.先搭UI页面 2 将百度网盘中提供的资料放置到bin\debug目录 ...

  2. IDA Pro的patch插件 KeyPatch

    本来这个是没什么可写的,但是安这个插件的时候真是气到爆炸,安装文档写的不明不白,几万行的代码都写了就差那么点时间写个几十字的详细说明吗? 1.下载keypatch.py放到\IDA\plugins里 ...

  3. find算法

     find(beg, end, val)  :根据equal操作符,循序查找[first, last)内所有的元素,找出第一个匹配“等同条件者”.如果找到,就返回一个指向钙元素的迭代器,否者返回迭代器 ...

  4. 学习C程序设计(一)第一节总览

    1.程序设计面向的问题 一切可计算问题都可以用程序的方法解决.ps:这样程序与计算有关了 1.1程序设计的五个步骤: a.确定问题可以计算:(问题是可计算) b.建立问题的数学模型:(不懂) c.设计 ...

  5. #11 UVA 10529 Dumb Bones

    题意: 放一堆排,每放一张,有pa的概率让左边的全倒,有pb的概率让右边全倒 问在最优策略下,最少要放几张才能摆放出n张 1<=n<=1000 题解: 这题应该还是很经典的 首先是期望部分 ...

  6. openstack学习-nove计算节点部署(五)

    nova-compute一般运行在计算节点上,通过message Queue接收管理VM的生命周期 nova-compute通过Libvirt管理KVM,通过XenAPI管理Xen等 一.安装Open ...

  7. HTML编码规范 - (WEB前端命名规范)

    HTML编码规范 (一)命名规则: 头:header 内容:content/container 尾:footer 导航:nav 侧栏:sidebar 栏目:column 页面外围控制整体布局宽度:wr ...

  8. Github+阿超运算

    感谢自己寒假能够稍稍做一点努力. Github个人页面<构建之法阅读笔记二可见>: https://github.com/Heartxy8990 申请教程: http://jingyan. ...

  9. MooFest POJ1990

    题意: 一群牛参加完牛的节日后都有了不同程度的耳聋,第i头

  10. 083 HBase的完全分布式的搭建与部署,以及多master

    一:前提准备 1.设置时间同步 2.清空logs,datas 3.格式化集群 bin/hdfs namenode -format 4.重启集群 sbin/start-dfs.sh sbin/start ...