一、ThreadLocal简介

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

  • 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。

  • 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景

下图可以增强理解:

二、ThreadLocal与Synchronized的区别

ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。

但是ThreadLocal与synchronized有本质的区别:

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

2、Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

一句话理解ThreadLocal,threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。

三、ThreadLocal的简单使用

直接上代码:

public class ThreadLocaDemo {private static ThreadLocal<String> localVar = new ThreadLocal<String>();static void print(String str) {//打印当前线程中本地内存中本地变量的值System.out.println(str + " :" + localVar.get());//清除本地内存中的本地变量localVar.remove();}public static void main(String[] args) throws InterruptedException {new Thread(new Runnable() {public void run() {ThreadLocaDemo.localVar.set("local_A");print("A");//打印本地变量System.out.println("after remove : " + localVar.get());}},"A").start();Thread.sleep(1000);new Thread(new Runnable() {public void run() {ThreadLocaDemo.localVar.set("local_B");print("B");System.out.println("after remove : " + localVar.get());}},"B").start();}
}A :local_A
after remove : null
B :local_B
after remove : null

从这个示例中我们可以看到,两个线程分表获取了自己线程存放的变量,他们之间变量的获取并不会错乱。这个的理解也可以结合图1-1,相信会有一个更深刻的理解。

四、ThreadLocal的原理

要看原理那么就得从源码看起。

4.1 ThreadLocal的set()方法:

public void set(T value) {//1、获取当前线程Thread t = Thread.currentThread();//2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,//则直接更新要保存的变量值,否则创建threadLocalMap,并赋值ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);else// 初始化thradLocalMap 并赋值createMap(t, value);}

从上面的代码可以看出,ThreadLocal set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。

那么ThreadLocalMap又是什么呢,还有createMap又是怎么做的,我们继续往下看。大家最后自己再idea上跟下源码,会有更深的认识。

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是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。详细内容要大家自己去跟。

//这个是threadlocal 的内部方法
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}//ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}

4.2 ThreadLocal的get方法

public T get() {//1、获取当前线程Thread t = Thread.currentThread();//2、获取当前线程的ThreadLocalMapThreadLocalMap map = getMap(t);//3、如果map数据不为空,if (map != null) {//3.1、获取threalLocalMap中存储的值ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为nullreturn setInitialValue();}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;}

4.3 ThreadLocal的remove方法

public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}

remove方法,直接将ThrealLocal 对应的值从当前相差Thread中的ThreadLocalMap中删除。为什么要删除,这涉及到内存泄露的问题。

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。举个例字,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。

4.4、ThreadLocal与Thread,ThreadLocalMap之间的关系

图4-1 Thread、THreadLocal、ThreadLocalMap之间啊的数据关系图

从这个图中我们可以非常直观的看出,ThreadLocalMap其实是Thread线程的一个属性值,而ThreadLocal是维护ThreadLocalMap

这个属性指的一个工具类。Thread线程可以拥有多个ThreadLocal维护的自己线程独享的共享变量(这个共享变量只是针对自己线程里面共享)

五、ThreadLocal 常见使用场景 如上文所述,ThreadLocal 适用于如下两种场景

1、每个线程需要有自己单独的实例 2、实例需要在多个方法中共享,但不希望被多线程共享         对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。

对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅。

场景

1)存储用户Session

一个简单的用ThreadLocal来存储Session的例子:

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();threadSession.set(s);}} catch (HibernateException ex) {throw new InfrastructureException(ex);}return s;}

场景二、数据库连接,处理数据库事务

场景三、数据跨层传递(controller,service, dao)

每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。

例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。

在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。

比如说我们是一个用户系统,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就会依次调用service-1()、service-2()、service-3()、service-4(),这4个方法可能是分布在不同的类中的。这个例子和存储session有些像。

package com.kong.threadlocal;public class ThreadLocalDemo05 {public static void main(String[] args) {User user = new User("jack");new Service1().service1(user);}}class Service1 {public void service1(User user){//给ThreadLocal赋值,后续的服务直接通过ThreadLocal获取就行了。UserContextHolder.holder.set(user);new Service2().service2();}
}class Service2 {public void service2(){User user = UserContextHolder.holder.get();System.out.println("service2拿到的用户:"+user.name);new Service3().service3();}
}class Service3 {public void service3(){User user = UserContextHolder.holder.get();System.out.println("service3拿到的用户:"+user.name);//在整个流程执行完毕后,一定要执行removeUserContextHolder.holder.remove();}
}class UserContextHolder {//创建ThreadLocal保存User对象public static ThreadLocal<User> holder = new ThreadLocal<>();
}class User {String name;public User(String name){this.name = name;}
}执行的结果:service2拿到的用户:jack
service3拿到的用户:jack

场景四、Spring使用ThreadLocal解决线程安全问题

我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。

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

这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。

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

代码清单9-5  TopicDao:非线程安全

public class TopicDao {//①一个非线程安全的变量private Connection conn; public void addTopic(){//②引用非线程安全变量Statement stat = conn.createStatement();…}

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

代码清单9-6  TopicDao:线程安全

import java.sql.Connection;
import java.sql.Statement;
public class TopicDao {//①使用ThreadLocal保存Connection变量
private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
public static Connection getConnection(){//②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,//并将其保存到线程本地变量中。
if (connThreadLocal.get() == null) {Connection conn = ConnectionManager.getConnection();connThreadLocal.set(conn);return conn;}else{//③直接返回线程本地变量return connThreadLocal.get();}}public void addTopic() {//④从ThreadLocal中获取线程对应的Statement stat = getConnection().createStatement();}

不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否为null,如果为null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其他线程的Connection。因此,这个TopicDao就可以做到singleton共享了。

当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在Dao只能做到本Dao的多个方法共享Connection时不发生线程安全问题,但无法和其他Dao共用同一个Connection,要做到同一事务多Dao共享同一个Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。但这个实例基本上说明了Spring对有状态类线程安全化的解决思路。在本章后面的内容中,我们将详细说明Spring如何通过ThreadLocal解决事务管理的问题。

六、ThreadLocal 使用原理

前文我们讲过ThreadLocal的主要用途是实现线程间变量的隔离,表面上他们使用的是同一个ThreadLocal, 但是实际上使用的值value却是自己独有的一份。用一图直接表示threadlocal 的使用方式。

从图中我们可以当线程使用threadlocal 时,是将threadlocal当做当前线程thread的属性ThreadLocalMap 中的一个Entry的key值,实际上存放的变量是Entry的value值,我们实际要使用的值是value值。value值为什么不存在并发问题呢,因为它只有一个线程能访问。threadlocal我们可以当做一个索引看待,可以有多个threadlocal 变量,不同的threadlocal对应于不同的value值,他们之间互不影响。ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

七、ThreadLocal 内存泄露的原因

Entry将ThreadLocal作为Key,值作为value保存,它继承自WeakReference,注意构造函数里的第一行代码super(k),这意味着ThreadLocal对象是一个「弱引用」。可以看图1。

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

主要两个原因 1 . 没有手动删除这个 Entry 2 . CurrentThread 当前线程依然运行

第一点很好理解,只要在使用完下 ThreadLocal ,调用其 remove 方法删除对应的 Entry ,就能避免内存泄漏。         第二点稍微复杂一点,由于ThreadLocalMap 是 Thread 的一个属性,被当前线程所引用,所以ThreadLocalMap的生命周期跟 Thread 一样长。如果threadlocal变量被回收,那么当前线程的threadlocal 变量副本指向的就是key=null, 也即entry(null,value),那这个entry对应的value永远无法访问到。实际私用ThreadLocal场景都是采用线程池,而线程池中的线程都是复用的,这样就可能导致非常多的entry(null,value)出现,从而导致内存泄露。

综上, ThreadLocal 内存泄漏的根源是:         由于ThreadLocalMap 的生命周期跟 Thread 一样长,对于重复利用的线程来说,如果没有手动删除(remove()方法)对应 key 就会导致entry(null,value)的对象越来越多,从而导致内存泄漏。

八、为什么不将key设置为强引用

8.1 、key 如果是强引用

那么为什么ThreadLocalMap的key要设计成弱引用呢?其实很简单,如果key设计成强引用且没有手动remove(),那么key会和value一样伴随线程的整个生命周期。

1、假设在业务代码中使用完ThreadLocal, ThreadLocal ref被回收了,但是因为threadLocalMap的Entry强引用了threadLocal(key就是threadLocal), 造成ThreadLocal无法被回收。在没有手动删除Entry以及CurrentThread(当前线程)依然运行的前提下, 始终有强引用链CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)-> entry, Entry就不会被回收( Entry中包括了ThreadLocal实例和value), 导致Entry内存泄漏也就是说: ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。请结合图1看。

8.2、那么为什么 key 要用弱引用

事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的.这就意味着使用threadLocal , CurrentThread 依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收.对应value在下一次 ThreadLocaI 调用 get()/set()/remove() 中的任一方法的时候会被清除,从而避免内存泄漏.

8.3、如何正确的使用ThreadLocal

1、将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露

2、每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

参考文章:

ThreadLocal的应用场景 - sw_kong - 博客园

面试官:知道ThreadLocal嘛?谈谈你对它的理解?(基于jdk1.8)

ThreadLocal使用场景分析 - 简书

ThreadLocal原理分析与使用场景 - 阿凡卢 - 博客园

(转)spring里面对ThreadLocal的使用_迷茫之欲的博客-CSDN博客_spring threadlocal

史上最全ThreadLocal 详解(一)_倔强的不服的博客-CSDN博客_threadlocal

史上最全ThreadLocal 详解(二)_倔强的不服的博客-CSDN博客_threadlocal可以存多个值吗

Java中ThreadLocal详解相关推荐

  1. Java中CAS详解

    转载自  Java中CAS详解 在JDK 5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁 锁机制存在以下问题: (1)在多线程竞争下,加锁.释放锁会导致比较多的上下文切换 ...

  2. Java中LinkedList详解

    Java中LinkedList详解 LinkedList底层是双向链表 单向链表 双向链表 LinkedList新增的方法 主要增加了针对头结点与尾结点进行操作的方法, 即针对第一个元素和最后一个元素 ...

  3. Java中super详解

    目录 Java中super详解 super的作用: 1.     通过super可以访问父类的构造方法 2.   通过super可以访问父类的属性(非私有) 3.        通过super可以访问 ...

  4. java中匿名内部类详解_java 中匿名内部类的实例详解

    搜索热词 java 中匿名内部类的实例详解 原来的面貌: class TT extends Test{ void show() { System.out.println(s+"~~~哈哈&q ...

  5. java中priorityqueue_详解JAVA中priorityqueue的具体使用

    Java中PriorityQueue通过二叉小顶堆实现,可以用一棵完全二叉树表示.本文从Queue接口函数出发,结合生动的图解,深入浅出地分析PriorityQueue每个操作的具体过程和时间复杂度, ...

  6. java中implement_详解JAVA中implement和extends的区别

    详解JAVA中implement和extends的区别 extends是继承父类,只要那个类不是声明为final或者那个类定义为abstract的就能继承,Java中不支持多重继承,但是可以用接口来实 ...

  7. java中final详解_Java中final用法与详解

    Java中final用法与详解 final作为Java中经常用到的关键字,了解final的使用方法是非常有必要的.这里从final关键字在数据域.方法和类中三个方面分析final关键字的主要用法. f ...

  8. java中getclass_详解java中this.getClass()和super.getClass()的实例

    详解java中this.getClass()和super.getClass()的实例 前言: 遇到this.getClass()和super.getClass()的返回值感到疑惑,经过探索豁然开朗. ...

  9. Java中Random详解

    目录 伪随机 什么是伪随机数? Java随机数产生原理: Java中常见生成随机数的几种方式 Math.random() Random Random的两种构造方法: 种子的作用数什么? 小结 Thre ...

  10. java中File详解

    #第六部分:IO流 ##1. File类的作用 File类是Java.io包中唯一代表磁盘我呢见本身的对象.File类定义类一些与平台无关的方法 来操作文件,File类主要用来获取或处理与磁盘文件相关 ...

最新文章

  1. Finalize/Dispose/Destructor
  2. DBUtils (30)
  3. linux ubuntu如何解压rar文件?unrar
  4. python通过DictReader实现两个csv文件的映射查找lookup之代码详解
  5. 窗口分析函数_17_计算百分位数
  6. 动态开点线段树(多棵线段树)的内存分配与回收
  7. 解读阿里巴巴集团的“大中台、小前台”组织战略
  8. 计算机网络应用答题卡,2013-2014学年第2学期11级计算机网络技术毕业考试试卷
  9. addEventListener事件监听传递参数
  10. HDU2502 月之数(解法三)【废除!!!】
  11. python 数据结构之顺序列表的实现
  12. 小D课堂-SpringBoot 2.x微信支付在线教育网站项目实战_1-4.在线教育后台数据库设计...
  13. 2. main函数和启动例程
  14. 存储过程中进行循环处理数据
  15. 基于Kinetis系列微控制器K60芯片的I2C接口函数程序说明1
  16. IDEA左侧project导航栏设置背景色
  17. firefox登录国际账户
  18. 2019大学生电子设计竞赛
  19. Emacs快捷键 转自:http://www.engr.uvic.ca/~dastone/emacs-keys.html
  20. 流体仿真前处理,Fluent工程师的”基本功”和”看家本领”

热门文章

  1. 设置selected选中的多种方法(总结)
  2. 微信公众号上传素材并回复关键词推送该素材:thinphp开发微信公众号如何上传临时素材库
  3. 介绍5款非常棒的移动自动化测试工具
  4. 【错误率、精度、查准率、查全率和F1度量】详细介绍
  5. 植物大战僵尸开发公司创始人自述:从0到1亿
  6. wpf/sl下的复合程序-CAG入门
  7. Fireball真相:一个菜鸟级流氓软件竟让老外如临大敌
  8. 如何查看计算机关闭原因,电脑总是自动重启关机怎么样查找原因
  9. 赛锐信息:5个方面帮您应对 SAP License 审计
  10. 【Nodejs】使用http.request批量下载MP3,发现网络文件大于1000K时下载文件为0K