先总述,后分析

  深挖过threadLocal之后,一句话概括:Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。所以ThreadLocal的应用场合,最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。

  数据隔离的秘诀其实是这样的,Thread有个TheadLocalMap类型的属性,叫做threadLocals,该属性用来保存该线程本地变量。这样每个线程都有自己的数据,就做到了不同线程间数据的隔离,保证了数据安全。

  接下来采用jdk1.8源码进行深挖一下TheadLocal和TheadLocalMap。

ThreadLocal是什么

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

  当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

  从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。

  所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。

原理

  ThreadLocal,连接ThreadLocalMap和Thread。来处理Thread的TheadLocalMap属性,包括init初始化属性赋值、get对应的变量,set设置变量等。通过当前线程,获取线程上的ThreadLocalMap属性,对数据进行get、set等操作。

  ThreadLocalMap,用来存储数据,采用类似hashmap机制,存储了以threadLocal为key,需要隔离的数据为value的Entry键值对数组结构。

  ThreadLocal,有个ThreadLocalMap类型的属性,存储的数据就放在这儿。

ThreadLocal、ThreadLocal、Thread之间的关系

  ThreadLocalMap是ThreadLocal内部类,由ThreadLocal创建,Thread有ThreadLocal.ThreadLocalMap类型的属性。源码如下:

Thread的属性:

public
class Thread implements Runnable {/*...其他属性...*//* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null;/** InheritableThreadLocal values pertaining to this thread. This map is* maintained by the InheritableThreadLocal class.*/ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

ThreadLocal和ThreadLocalMap

public class ThreadLocal<T> {/**..其他属性和方法稍后介绍...*//*** ThreadLocalMap is a customized hash map suitable only for* maintaining thread local values. No operations are exported* outside of the ThreadLocal class. The class is package private to* allow declaration of fields in class Thread.  To help deal with* very large and long-lived usages, the hash table entries use* WeakReferences for keys. However, since reference queues are not* used, stale entries are guaranteed to be removed only when* the table starts running out of space.*/static class ThreadLocalMap {

由ThreadLocal对Thread的TreadLocalMap进行赋值

    /*** 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);}

ThreadLocal的接口方法

ThreadLocal类核心方法set、get、initialValue、withInitial、setInitialValue、remove:

   /*** Returns the current thread's "initial value" for this* thread-local variable.  This method will be invoked the first* time a thread accesses the variable with the {@link #get}* method, unless the thread previously invoked the {@link #set}* method, in which case the {@code initialValue} method will not* be invoked for the thread.  Normally, this method is invoked at* most once per thread, but it may be invoked again in case of* subsequent invocations of {@link #remove} followed by {@link #get}.** <p>This implementation simply returns {@code null}; if the* programmer desires thread-local variables to have an initial* value other than {@code null}, {@code ThreadLocal} must be* subclassed, and this method overridden.  Typically, an* anonymous inner class will be used.** @return the initial value for this thread-local*/protected T initialValue() {return null;}/*** Creates a thread local variable. The initial value of the variable is* determined by invoking the {@code get} method on the {@code Supplier}.** @param <S> the type of the thread local's value* @param supplier the supplier to be used to determine the initial value* @return a new thread local variable* @throws NullPointerException if the specified supplier is null* @since 1.8*/public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {return new SuppliedThreadLocal<>(supplier);}/*** Creates a thread local variable.* @see #withInitial(java.util.function.Supplier)*/public ThreadLocal() {}/*** 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();}/*** 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 = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;}/*** 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);elsecreateMap(t, value);}/*** Removes the current thread's value for this thread-local* variable.  If this thread-local variable is subsequently* {@linkplain #get read} by the current thread, its value will be* reinitialized by invoking its {@link #initialValue} method,* unless its value is {@linkplain #set set} by the current thread* in the interim.  This may result in multiple invocations of the* {@code initialValue} method in the current thread.** @since 1.5*/public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}
  • initialValue返回该线程局部变量的初始值。该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
  • withInitial提供一个Supplier的lamda表达式用来当做初始值,java8引入。
  • setInitialValue设置初始值。在get操作没有对应的值时,调用此方法。private方法,防止被覆盖。过程和set类似,只不过是用initialValue作为value进行设置。
  • set设置当前线程对应的线程局部变量的值。先取出当前线程对应的threadLocalMap,如果不存在则用创建一个,否则将value放入以this,即threadLocal为key的映射的map中,其实threadLocalMap内部和hashMap机制一样,存储了Entry键值对数组,后续会深挖threadLocalMap。
  • get该方法返回当前线程所对应的线程局部变量。和set类似,也是先取出当前线程对应的threadLocalMap,如果不存在则用创建一个,但是是用inittialValue作为value放入到map中,且返回initialValue,否则就直接从map取出this即threadLocal对应的value返回。
  • remove将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。需要注意的是,如果remove之后又调用了get,会重新初始化一次,即再次调用initialValue方法,除非在get之前调用set设置过值。

ThreadLocalMap简介

  看名字就知道是个map,没错,这就是个hashMap机制实现的map,用Entry数组来存储键值对,key是ThreadLocal对象,value则是具体的值。值得一提的是,为了方便GC,Entry继承了WeakReference,也就是弱引用。里面有一些具体关于如何清理过期的数据、扩容等机制,思路基本和hashmap差不多,有兴趣的可以自行阅读了解,这边只需知道大概的数据存储结构即可。

    /*** ThreadLocalMap is a customized hash map suitable only for* maintaining thread local values. No operations are exported* outside of the ThreadLocal class. The class is package private to* allow declaration of fields in class Thread.  To help deal with* very large and long-lived usages, the hash table entries use* WeakReferences for keys. However, since reference queues are not* used, stale entries are guaranteed to be removed only when* the table starts running out of space.*/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;}}

Thread同步机制的比较

  ThreadLocal和线程同步机制相比有什么优势呢?

  Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

  在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

  而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

  概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

  Spring使用ThreadLocal解决线程安全问题我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。

  一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程。

  同一线程贯通三层这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。

  下面的实例能够体现Spring对有状态Bean的改造思路:

代码清单3 TestDao:非线程安全

package com.test;import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;public class TestDao {private Connection conn;// ①一个非线程安全的变量public void addTopic() throws SQLException {Statement stat = conn.createStatement();// ②引用非线程安全变量// …}
}

由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:

代码清单4 TestDao:线程安全

package com.test;import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;public class TestDaoNew {// ①使用ThreadLocal保存Connection变量private static ThreadLocal<Connection> connThreadLocal = ThreadLocal.withInitial(Test::createConnection);// 具体创建数据库连接的方法private static Connection createConnection() {Connection result = null;/*** create a real connection...* such as :* result = DriverManager.getConnection(dbUrl, dbUser, dbPwd);*/return result;}// ③直接返回线程本地变量public static Connection getConnection() {return connThreadLocal.get();}// 具体操作public void addTopic() throws SQLException {// ④从ThreadLocal中获取线程对应的ConnectionStatement stat = getConnection().createStatement();//....any other operation}
}

  不同的线程在使用TopicDao时,根据之前的深挖get具体操作,判断connThreadLocal.get()会去判断是有map,没有则根据initivalValue创建一个Connection对象并添加到本地线程变量中,initivalValue对应的值也就是上述的lamba表达式对应的创建connection的方法返回的结果,下次get则由于已经有了,则会直接获取已经创建好的Connection,这样,就保证了不同的线程使用线程相关的Connection,而不会使用其它线程的Connection。因此,这个TopicDao就可以做到singleton共享了。

  当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在DAO只能做到本DAO的多个方法共享Connection时不发生线程安全问题,但无法和其它DAO共用同一个Connection,要做到同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。

ConnectionManager.java

package com.test;import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;public class ConnectionManager {private static ThreadLocal<Connection> connectionHolder = ThreadLocal.withInitial(() -> {Connection conn = null;try {conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "username","password");} catch (SQLException e) {e.printStackTrace();}return conn;});public static Connection getConnection() {return connectionHolder.get();}
}

线程隔离的秘密

秘密就就在于上述叙述的ThreadLocalMap这个类。ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。

为了加深理解,我们接着看上面代码中出现的getMap和createMap方法的实现:

    /*** 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;}/*** 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* @param map the map to store.*/void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}

小结

  ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

后记

  看到网友评论的很激烈,甚至关于ThreadLocalMap不是ThreadLocal里面的,而是Thread里面的这种评论都出现了,于是有了这个后记,下面先把jdk源码贴上,源码最有说服力了。

/*** ThreadLocalMap is a customized hash map suitable only for* maintaining thread local values. No operations are exported* outside of the ThreadLocal class. The class is package private to* allow declaration of fields in class Thread.  To help deal with* very large and long-lived usages, the hash table entries use* WeakReferences for keys. However, since reference queues are not* used, stale entries are guaranteed to be removed only when* the table starts running out of space.*/static class ThreadLocalMap {...}

  源码就是以上,这源码自然是在ThreadLocal里面的,有截图为证。

  本文是自己在学习ThreadLocal的时候,一时兴起,深入看了源码,思考了此类的作用、使用范围,进而联想到对传统的synchronize共享变量线程安全的问题进行比较,而总结的博文,总结一句话就是一个是锁机制进行时间换空间,一个是存储拷贝进行空间换时间。

(全文完)

深挖ThreadLocal相关推荐

  1. 深挖数据价值 阿里云栖开年大会报道

    本文讲的是深挖数据价值 阿里云栖开年大会报道[IT168 云计算]经历风雨,转身看到彩虹.在这个"化云为雨"的时节,造云大咖们角色扮演也逐步渐入佳境,或随需而动,或引领潮流.阿里云 ...

  2. 深挖之后吓一跳,谷歌AI专利何止一个dropout,至少30项今日生效

    乾明 郭一璞 发自 凹非寺  量子位 报道 | 公众号 QbitAI "2030年之前,你需要给谷歌交专利费,才能构建简单的神经网络." 这不是一句调侃,而是在慢慢成为现实. 前不 ...

  3. 专访友盟CEO叶谦:深挖海量终端用户数据的价值

     专访友盟CEO叶谦:深挖海量终端用户数据的价值 发表于10小时前| 651次阅读| 来源CSDN| 0 条评论| 作者Banbury 访谈友盟CEO叶谦大数据数据挖掘移动互联网物联网移动开发开发 ...

  4. 中国移动推出企业飞信 五大亮点深挖企业服务

    今日,中国移动在北京正式宣布对外推出企业飞信,借助运营商电信能力及优势,帮助中小企业实现沟通.协作.社交. 市场调查显示,最近几年中国企业市场规模增长迅猛,2016年中国企业市场总体规模达660.3亿 ...

  5. std中稳定排序算法_敏哥:深挖亚马逊A9算法中的广告搜索排序

    大家好,我是敏哥.最近国外疫情肆虐,牵动着所有跨境电商人的心.今天美国的确诊人数已经突破了33万人,每天以3万左右的确诊数量在增加,就连老虎也未能幸免. 国外疫情肆虐,对于跨境电商人而言并不是一个很好 ...

  6. 深挖“窄带高清”的实现原理

    过去几年,又拍云一直在点播.直播等视频应用方面潜心钻研,取得了不俗的成果.我们结合点播.直播.短视频等业务中的用户场景,推出了"省带宽.压成本"系列文章,从编码技术.网络架构等角度 ...

  7. 深挖基于 CSS 的变换与动画技术

    深挖基于 CSS 的变换与动画技术 李俊才 的 CSDN 博客 邮箱 :291148484@163.com CSDN 主页:https://blog.csdn.net/qq_28550263?spm= ...

  8. 深挖洞广积粮 旷视印奇谈7.5亿美元D轮融资后的战略布局

    https://www.toutiao.com/a6691923877552980491/ 日前,中国人工智能独角兽企业旷视宣布完成了7.5亿美元的D轮融资.本轮融资后,旷视的战略布局又将发生哪些变化 ...

  9. 自动(智能)驾驶系列| 插播 深挖自动驾驶数据集

    写在前面: 最近因为新项目的事情,到外地出差,没有太多时间更新深感抱歉~后续有时间会继续更新的!之前使用数据可能掌握数据的格式和特性放到网络中即可(是darknet格式的么等等),本文不是对如何使用数 ...

最新文章

  1. 机器学习算法清单!附Python和R代码
  2. 【Eclipse 插件】JD-Eclipse
  3. 程序员究竟能干多少年?用数据说话!
  4. pythonfor循环100次_在for循环中只打印一次
  5. linux安装多路径报错,Linux操作系统配置多路径通用教程(适用于(RHEL,CentOS,SuSE等)...
  6. 中国国家高速公路网详细布局
  7. 产品经理 - 统一支付 、结算、清算
  8. stm32 串口通信数据移位寄存器_STM32串口接RS485丢码问题已解决*_*
  9. Web前端开发解耦1
  10. ubuntu16.04中ROS-Kinetic安装Arbotix
  11. 22 模块:宏伟蓝图
  12. 如何在csdn免费下载资料?
  13. 网络聊天室——低仿QQ
  14. 关于芯片或者功率模块热相关计算
  15. 站斧超级浏览器风控系统助力Lazada商家安全管理
  16. H桥和NMOS,PMOS理解
  17. 你真正了解图像金字塔吗?详细介绍拉普拉斯金字塔和高斯金字塔(pyrDown() and pyrUp()),参考《OpenCV轻松入门:面向Python》
  18. C++高并发服务器设计--共享内存封装(六)
  19. 关于前端上传文件到后台,文件大小超出限制,导致上传失败的解决办法。
  20. 纸质合同扫描存档和电子合同签字盖章的区别

热门文章

  1. linux将文件修改时间改为现在
  2. 如何在GraphPad Prism 里创建条形图?
  3. IOS图像2之jpg、png图片保存、互转、设置有损压缩因子
  4. 【MySQL基础】MySQL介绍及安装
  5. node-sass弃用背景
  6. 机器学习(1)——Python数据处理与绘图
  7. 排序算法——希尔排序的图解、代码实现以及时间复杂度分析
  8. php秒杀负库存问题,店铺商品出现负库存原因分析,负库存商品处理规范
  9. 什么是“好”系统呢?
  10. 01_03 获取答案