什么是ThreadLocal

ThreadLocal是线程局部变量,所谓的线程局部变量,就是仅仅只能被本线程访问,不能在线程之间进行共享访问的变量。在各个Java web的各种框架中ThreadLocal几乎已经被用烂了,spring中有使用,mybatis中也有使用,hibernate中也有使用,甚至我们写个分页也用ThreadLocal来传递参数…这也从侧面说明了ThreadLocal十分的给力。

下面看看作者Doug Lea是怎么说的,下面是jdk7.X中的注释:

也就是说这个类给线程提供了一个本地变量,这个变量是该线程自己拥有的。在该线程存活和ThreadLocal实例能访问的时候,保存了对这个变量副本的引用.当线程消失的时候,所有的本地实例都会被GC。并且建议我们ThreadLocal最好是 private static 修饰的成员

ThreadLocal和Synchonized区别

都用于解决多线程并发访问。

Synchronized用于线程间的数据共享(使变量或代码块在某一时该只能被一个线程访问),是一种以延长访问时间来换取线程安全性的策略;

而ThreadLocal则用于线程间的数据隔离(为每一个线程都提供了变量的副本),是一种以空间来换取线程安全性的策略。

ThreadLocal的简单方法

先来看一下ThreadLocal的API:

1、构造方法摘要

ThreadLocal(): 创建一个线程本地变量。

2、方法摘要

void set(T value): 将此线程局部变量的当前线程副本中的值设置为指定值。
T get(): 返回此线程局部变量的当前线程副本中的值。
void remove():移除此线程局部变量当前线程的值。
protected T initialValue():返回此线程局部变量的当前线程的“初始值”。是protected方法,是为了让子类继承而设计的。

简单代码应用

ThreadLocalTest类有两个方法,一个是start方法,一个是end方法,start记录开始时间,end方法记录结束时间,这个方法可以简单的用在统计耗时的功能上,在方法的入口前执行start,在方法被调用之后调用end方法,好处是两个方法的调用不用再一个方法或者类中,比如在aop(面向切面编程)中,在方法调用前的切入点执行start方法,在方法调用之后调用end方法,这样依旧可以得到方法执行的耗时。

ThreadLocal 类很简单,下面接着抛出两个误区,以这两个误区为起点,进行分析,逐步揭开ThreadLocal 的真面目。

一、两大误区

1、误区一 Threadlocal 的出现是为了解决多线程共享对象的问题。

网上不少的文章对ThreadLocal有着很糟糕的错误认识,认为ThreadLocal可以为每一个共享对象保持一个副本,这样就可以解决多线程并发竞争资源的问题。本人在入门并发时,也是这么认为的,但随着工作实战的经验增加,根本就不是那么一回事。
  我们来分析一下。假设ThreadLocal是能够解决多线程共享对象的问题,于是我们为每一个线程都维护一个该对象的独立副本(先不考虑内存的问题)。如果都是读线程,那么问题不大。但如果有写线程呢?写线程修改了副本,但是其他读线程读取到还是旧的值,这样线程之间无法通信,共享对象就失去意义了(共享对象是线程通信的一种方式,在一个线程修改了,另一个线程也应该看的见)。如果仅仅都是读线程,要维护这么多副本,消耗大量内存,而且在多线程的环境下,只能读取的话,可以不加锁,那么竞争就不存在,不需要额外维护多个副本。经过上面的分析,ThreadLocal 是不可能解决多线程共享对象的问题

那么 ThreadLocal 的真正作用是什么呢?

看一下JDK的源码注释:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

对应的中文应该是这样的:

该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

好像还是不太懂,那么再具体一点就是: 如果当类中的某个变量希望根据不同的线程提供不同的值,而且任意一个线程修改这个变量不会影响到其他线程,那么这个变量就应该是线程的局部变量。 一般的用法是用 private static 修饰变量,这是因为ThreadLocalMap中的key值是一个弱引用,是以ThreadLocal为key,所以要用static来延长ThreadLocal的生存时间,后续讲到。

误区二 ThreadLocal 的底层实现是一个Map,key是当前线程,value是局部变量

ThreadLocal的底层维护着一个Map,key是 Thread.currentThread(当前线程),value 则是要保持的局部变量。这种设计思路是所有人最容易想到的,也是最容易被大家所误解的方案,其实这也是早期JDK的设计方案(好像是JDK1.2)。(简称为 方案A

但后期的JDK中,改善了ThreadLocal的设计,也是本文的重点,先简单说一下设计:不同于方案A的只有一个Map的设计,此方案的每一个Thread 对象中各自维护着一个ThreadLocal.ThreadLocalMap 对象(可以看成是一个简答的Map),此Map对象是线程私有的,key是ThreadLocal对象,value是线程的局部变量。而ThreadLocal中没有维护着Map对象。(简称为 方案B

方案A的设计有什么问题?为什么被抛弃?

方案A之所以被抛弃,因为以下几点原因

  • 线程需要竞争ThreadLocal中的Map。 一般情况下,是多个线程程共享着一个ThreadLocal 对象,按照方案A的设计,意味着多个线程共享着一个Map对象,所以访问这个Map对象时,需要进行同步互斥访问,访问速度将下降。
  • 线程的局部变量在线程死亡时难以回收或者难以及时回收ThreadLocal的Map存储了多个线程的局部变量,当其中任意一个线程销毁时,其局部变量也应该跟着销毁,以释放内存。但是按照方案A中的设计,可能要遍历所有的Map,逐一判断线程(key值)的状态是否死亡,才能释放内存。如果这样做,不仅性能低,且无法及时释放内存,甚至可能会造成Map过大,内存溢出。

方案B有什么优点?方案B又是怎么解决的?

针对方案A的遇到的问题,方案B(目前方案)中都能得到解决:

  • 不需要竞争访问Map。 在方案B中,是每个Thread对象都维护着一个ThreadLocalMap,所以Map是线程私有的,不需要竞争。而且私有的Map只存储一个线程的局部变量,存储的元素的数量更少,那么hash冲突就少。这两点都大大地提高访问速度。

  • 所有局部变量随线程一起被销毁回收。 因为Map是维护在线程Thread中,当线程被销毁回收时,Map自然一起被销毁回收。

  • key值是弱引用,尽可能地释放过时的键值对Entry,回收内存。key值是指向ThreadLlocal的对象,采用了弱引用的设计,一旦此TreadLlocal对象没有了强引用指向,将会在下次的GC中被回收,那么key值就会为null,对应的Entry对象也最终会被释放,从而减少内存溢出的情况。

二、ThreadLocal的源码解析

上面仅仅简单地介绍了ThreadLocal的误区和设计思路,并没有深入去了解,也许你还是不太懂,那么接下来的部分将会通过源码,深入分析线程局部变量的机制。
1、ThreadLocalMap 与 Thread、ThreadLocal 的关系

1.1、ThreadLocalMap 类是 ThreadLocal的静态内部类

 static class ThreadLocalMap {//.....}

1.2、 ThreadLocalMap 对象是Thread的一个成员变量

//每个线程都维护着一个 存储局部变量的MapThreadLocal.ThreadLocalMap threadLocals = null;

1.3、ThreadLocalMap的几个属性

Entry[] table table数组是用来存储键值对的。键值对的key值为ThreadLocal对象、value是线程局部变量

 static class ThreadLocalMap {/*** The initial capacity -- MUST be a power of two.*/private static final int INITIAL_CAPACITY = 16;/*** The table, resized as necessary.* table.length MUST always be a power of two.*/private Entry[] table;/*** The number of entries in the table.*/private int size = 0;/*** The next size value at which to resize.*/private int threshold; // Default to 0//........}

2、ThreadLocal 分析

2.1、ThreadLocal的三个属性

ThreadLocal的属性就只有以下三个,用于计算、保存ThreadLocal对象中的 threadLocalHashCode 的值,而且每个ThreadLocal对象的 threadLocalHashCode 是不一样的,以此来区别它们,从而在 ThreadLocalMap 中减少hash冲突。

//当前的ThreadLocal 对象的hash值
private final int threadLocalHashCode = nextHashCode();//静态变量,用于计算下一个hash值
private static AtomicInteger nextHashCode = new AtomicInteger();//hash增量值,参与下一个hash值的计算
private static final int HASH_INCREMENT = 0x61c88647;/*** Returns the next hash code.*/private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}

2.2、ThreadLocal.set() 方法

 public void set(T value) {Thread t = Thread.currentThread();//获取当前线程的ThreadLocalMap 对象ThreadLocalMap map = getMap(t);if (map != null)//判断Map是否创建map.set(this, value);//this 指代当前 threadLocl对象elsecreateMap(t, value);//为当前线程创建ThreadLocalMap对象}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}

ThreadLocalset方法很简单。用当前线程中的 ThreadLocalMap 对象去存储局部变量,map.set(this, value) key值为this所指代对象,也即调用了此set方法的ThreadLocal对象。

2.3、ThreadLocal.get() 方法

    public T get() {Thread t = Thread.currentThread();//获取当前线程的ThreadLocalMap对象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();//初始的value值为null}

get()方法就更简单了,调用 ThreadLocalMap.getEntry()方法,以当前调用get方法的ThreadLocal对象为key值,获取对应的value值。

3、弱引用 与 ThreadLocalMap 的内存回收

先来看一下 Entry的源代码,Entry类是定义在 ThreadLocalMap中的静态内部类。

//继承了 WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {//创建了一个ThreadLocal对象的弱引用super(k);value = v;}}

ThreadLocalMap.Entry 继承了弱引用类 WeakReference 类,而且弱引用类包裹了key值。这意味着key值是一个弱引用。一旦key值所指向的ThreadLocal没有了强引用指向,那么便会被下一次的GC回收。然后key值便会为null,但是对应的Entry对象还在,并没有释放内存,那ThreadLocalMap是如何回收内存的呢?

** ThreadLocalMap 的内存回收:是在getEntry()、set()、remove()时遍历Map,将key值为null的Entry判断为过时的Entry,然后便释放掉这个Entry **。下面是重点讲解set()方法。

 private void set(ThreadLocal<?> key, Object value) {// We don't use a fast path as with get() because it is at// least as common to use set() to create new entries as// it is to replace existing ones, in which case, a fast// path would fail more often than not.Entry[] tab = table;int len = tab.length;//通过key值(ThreadLocal对象)的散列值threadLocalHashCode计算出 Entry的索引位置int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i]; e != null;e = tab[i = nextIndex(i, len)]) {//获取元素的key值ThreadLocal<?> k = e.get();if (k == key) {//hash命中,直接设置value值e.value = value;return;}if (k == null) {//没有命中,但找到了过时的Entry对象,即key值为null//替换掉此过时的EntryreplaceStaleEntry(key, value, i);return;}}//如果即没有命中,而且表中也没有发现过时的Entry对象,则在当前空的位置创建并插入一个新的Entry来吃存储tab[i] = new Entry(key, value);//表的大小增加int sz = ++size;//threshold = len * 2 / 3;判断是否需要重hashif (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();//重hash}

set()在设置值时,先计算出初始索引值,然后循环遍历table数组,判断table数组中的每个Entry是否匹配目标key,如果匹配则直接修改value值,如果发现有Entry过时,则调用replaceStaleEntry方法来替换掉这个过时的Entry,插入新的Entry,看一下replaceStaleEntry的源码:

private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {Entry[] tab = table;int len = tab.length;Entry e;// Back up to check for prior stale entry in current run.// We clean out whole runs at a time to avoid continual// incremental rehashing due to garbage collector freeing// up refs in bunches (i.e., whenever the collector runs).//slotToExpunge记录过时Entry的索引值int slotToExpunge = staleSlot;//以当前的过时Entry的索引staleSlot为起点,往后遍历,寻找过时的Entryfor (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len))if (e.get() == null)//判断是否是过时的EntryslotToExpunge = i;// Find either the key or trailing null slot of run, whichever// occurs first。以staleSlot为起点,继续往后遍历for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();// If we find key, then we need to swap it// with the stale entry to maintain hash table order.// The newly stale slot, or any other stale slot// encountered above it, can then be sent to expungeStaleEntry// to remove or rehash all of the other entries in run.if (k == key) {//发现hash命中e.value = value;//直接修改value值//命中的Entry与过时的Entry交换位置tab[i] = tab[staleSlot];tab[staleSlot] = e;// 判断前面的往后遍历循环是否发现新的过时的Entry对象,即slotToExpunge记录了新的索引值if (slotToExpunge == staleSlot)slotToExpunge = i;cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);//清除这个新发现的过时Entryreturn;}// If we didn't find stale entry on backward scan, the// first stale entry seen while scanning for key is the// first still present in the run.//发现了过时的Entry对象,如果前一个往后遍历的循环没有发现过时的Entry对象,才记录当前的索引值。//优先释放靠前的过时Entry对象if (k == null && slotToExpunge == staleSlot)slotToExpunge = i;}// If key not found, put new entry in stale slot//hash依旧没有命中,那么就将当前的过时Entry给替换成新的Entry对象tab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);// If there are any other stale entries in run, expunge themif (slotToExpunge != staleSlot)//判断是否发现新的过时Entry对象cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}

replaceStaleEntry()有点复杂,但是可以看出主要是两点:
  
1、以当前的staleSlot为起点分别往前往后寻找过时的Entry对象并释放;

2、无论是否找到目标key所对应的Entry,都替换掉staleSlot位置的过时Entry,换上新的Entry。

从JDK1.2版本开始,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

1.强引用

以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

2.软引用(SoftReference)
如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

3.弱引用(WeakReference)
如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

4.虚引用(PhantomReference)
"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃 圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解
被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

特别注意,在实际程序设计中一般很少使用弱引用与虚引用,使用软用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

参考资料:
http://www.cnblogs.com/jinggod/p/8486370.html
https://blog.csdn.net/u011276324/article/details/66968995

并发基础(十) 线程局部副本ThreadLocal之正解相关推荐

  1. Java并发基础(六) - 线程池

    Java并发基础(六) - 线程池 1. 概述 这里讲一下Java并发编程的线程池的原理及其实现 2. 线程池的基本用法 2.1 线程池的处理流程图 该图来自<Java并发编程的艺术>: ...

  2. java线程抢占式执行,Java并发基础(一)-线程基础

    原标题:Java并发基础(一)-线程基础 只要涉及到线程,其运行结果就是不确定的,虽然说java很早就提供了线程以及并发的支持,但是我们需要知道,线程是完全交给调度器的.有很多同学在编写书上的代码时, ...

  3. 【并发基础】线程的通知与等待:obj.wait()、obj.notify()、obj.notifyAll()详解

    目录 〇.先总结一下这三个方法带来的Java线程状态变化 一.obj.wait() 1.1 作用 1.2 wait()方法到底会让哪个线程阻塞? 1.3 wait(long timeout)方法如何实 ...

  4. 并发基础知识 — 线程安全性

    前段时间看完了<并发编程的艺术>,总感觉自己对于并发缺少一些整体的认识.今天借助<Java并发编程实践>,从一些基本概念开始,重新整理一下自己学过并发编程.从并发基础开始,深入 ...

  5. Java并发编程:线程封闭和ThreadLocal详解

    什么是线程封闭 当访问共享变量时,往往需要加锁来保证数据同步.一种避免使用同步的方式就是不共享数据.如果仅在单线程中访问数据,就不需要同步了.这种技术称为线程封闭.在Java语言中,提供了一些类库和机 ...

  6. C#高性能大容量SOCKET并发(十):SocketAsyncEventArgs线程模型

    原文:C#高性能大容量SOCKET并发(十):SocketAsyncEventArgs线程模型 线程模型 SocketAsyncEventArgs编程模式不支持设置同时工作线程个数,使用的NET的IO ...

  7. c++ 线程池_基础篇:高并发一瞥,线程和线程池的总结

    进程是执行程序的实体,拥有独属的进程空间(内存.磁盘等).而线程是进程的一个执行流程,一个进程可包含多个线程,共享该进程的所有资源:代码段,数据段(全局变量和静态变量),堆存储:但每个线程拥有自己的执 ...

  8. 线程执行完之后会释放吗_java多线程并发:CAS+AQS+HashMap+volatile+ThreadLocal,乐分享...

    CyclicBarrier.CountDownLatch.Semaphore 的用法 CountDownLatch(线程计数器 ) CountDownLatch 类位于 java.util.concu ...

  9. 第一节 并发基础概念及实现、进程、线程基本概念

    1.并发.进程.线程的基本概念和综述 并发.线程.进程要求必须掌握!!!! 1.1 并发 概念:两个或更多的任务(独立的活动)同时发生(进行):一个程序同时执行多个独立的任务: 以往的计算机通常是单核 ...

最新文章

  1. codevs1032
  2. 倒计时3天!华为畅想未来智能车大赛报名即将截止,已报名选手请提交参赛PPT!
  3. python 类-python 类如何使用
  4. 3、数据类型一:strings
  5. arcgis几何修复有作用吗_ArcGis拓扑的那些事儿(拓扑应用过程二)
  6. linux匿名页 文件页,文件页和匿名页
  7. gprs 睡眠模式_GPRS的完整形式是什么?
  8. 容器编排技术 -- Kubernetes Labels 和 Selectors
  9. VGGnet论文解读及代码实现
  10. 2021会宁三中高考成绩查询,2020白银中考分数线
  11. python三天简单学习Day2
  12. iOS学习之单例模式
  13. 助老服务机器人结构设计
  14. flash中zip/unip的实际意图
  15. 猫、路由器和交换机的区别和联系
  16. JAVA基础——循环结构(while)
  17. 自学UE4 第三天,AI攻击机制 2022/5/20
  18. 测试不同体重体型软件样子的,为什么有的人身高、体重相同,体型却不一样?这是体脂率在作祟...
  19. python编程大数据_学习Python编程挨着大数据什么事
  20. linux系统根文件系统构建

热门文章

  1. STM8学习笔记---串口uart1
  2. preempt_count详解
  3. 基于口令的密码PBE(Password Based Encryption)
  4. Python+Anaconda+PyCharm的安装和基本使用
  5. Kubernetes API 聚合开发汇总
  6. python——装饰器
  7. [羊城杯 2020]RRRRRRRSA
  8. Docker的常用管理命令Docker将数据挂载到容器的三种方式
  9. 【攻防世界003】re-for-50-plz-50
  10. 使用 pg_dump 迁移 postgresql