ThreadLocal,从名字上可以知道和线程本地有关系,这个类会为每个线程提供属于线程自己的局部变量。ThreadLocal可以通过initialValue()为每个线程赋值,也可以由线程自己调用ThreadLocal的set()进行赋值。每个线程操作对应的变量时,与其他线程不会发生冲突,改动仅对自己可见。总的来说,ThreadLocal适用于变量在线程间隔离的场景,这里也可以看出ThreadLocal采用了空间换时间的策略保证并发安全。

在jdk1.7和1.8中,ThreadLocal原理并不相同,本文主要分析jdk1.8的。

首先来看一个demo,ThreadLocal为主线程和t1线程分配id,并且在initialValue()中分配一次就让id++。

 public static void main(String[] args) {ThreadLocal<Integer> threadLocal=new ThreadLocal<Integer>(){int id=0;@Overrideprotected Integer initialValue() {return id++;}};new Thread(()->{System.out.println("t1线程获取数据:"+threadLocal.get());},"t1").start();System.out.println("主线程获取数据:"+threadLocal.get());}

在这个demo中我们重写了initialValue()方法,让每个线程都能拿到初始值,如果不重写这个方法,就需要线程自己调用set()方法向里面存值。我们从set()方法开始看起。

ThreadLocal的常用方法

set()流程

 //value:每个线程调用set存的值public void set(T value) {//获取当前线程的引用Thread t = Thread.currentThread();//获取线程保存的ThreadLocalMap//ThreadLocalMap是ThreadLocal的静态内部类,是真正用来存放数据的ThreadLocalMap map = getMap(t);//map是否被创建过if (map != null)//创建过,就调用map.set()map.set(this, value);else//第一次调用时,map并未创建,所以走这个逻辑createMap(t, value);}//getMap,就是获取每个线程自己保管的ThreadLocalMap// ThreadLocal.ThreadLocalMap threadLocals = null; 这句代码在Thread类中ThreadLocalMap getMap(Thread t) {return t.threadLocals;}//创建ThreadLocalMap,并赋值给当前线程的threadLocals字段void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}

到这里set流程就结束了,可以知道在jdk1.8中,ThreadLocalMap是由线程进行保管的。

get()流程

 public T get() {//获取当前线程引用Thread t = Thread.currentThread();//获取到线程保管的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {//ThreadLocalMap的静态内部类Entry,数据存放在这里ThreadLocalMap.Entry e = map.getEntry(this);//判断Entry是否为nullif (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;//返回数据return result;}}//map为null,说明还未调用set(),就调用get()return setInitialValue();}private T setInitialValue() {//调用initialValue(),如果用户有重写就调用重写的方法,否则是空实现T value = initialValue();//以下是set()的逻辑,最后返回初始化的值Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;}

get()的逻辑也比较简单,就是通过线程保管的ThreadLocalMap里面查找数据并返回,如果Map还未创建,也会先执行初始化逻辑。

remove()流程

 public void remove() {//获取线程保管的mapThreadLocalMap m = getMap(Thread.currentThread());if (m != null)//调用map的remove移除数据m.remove(this);}

到这里ThreadLocal常用的方法就分析完毕了,可以发现ThreadLocal仅仅是对ThreadLocalMap的操作进行了封装,最核心的逻辑还是在ThreadLocalMap内完成的,接下来就去看它的源码。

ThreadLocalMap源码分析

ThreadLocalMap就是ThreadLocal的内部类,也在ThreadLocal文件中。首先来看一下它的重要字段,方法,和构造。

重要的字段

//散列表的初始长度
private static final int INITIAL_CAPACITY = 16;
//ThreadLocalMap内的散列表
private Entry[] table;
//当前散列表的元素个数
private int size = 0;
//扩容阈值
private int threshold; // Default to 0

重要的方法

 //设置扩容阈值private void setThreshold(int len) {//扩容阈值为当前散列表长度的2/3threshold = len * 2 / 3;}//返回当前下标的前一个下标//i:下标  len:散列表长度private static int prevIndex(int i, int len) {//i-1>=0 说明当前元素下标>=1,返回i-1//i-1<0 说明当前元素下标已经为0了,在数组头部,那就返回数组末尾的下标,形成一个环式查找return ((i - 1 >= 0) ? i - 1 : len - 1);}//同上,返回当前下标的下一个下标,到数组尾就返回0private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}

构造

 //firstKey:在前面创建map的时候,会将ThreadLocal对象传入//firstValue:ThreadLocal对应的值ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//创建长度为16的散列表table = new Entry[INITIAL_CAPACITY];//寻址算法   threadLocalHashCode & 长度-1int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//将ThreadLocal和值封装成Entry放入散列表中table[i] = new Entry(firstKey, firstValue);//设置容量size = 1;//设置扩容阈值  数组长度*2/3=10setThreshold(INITIAL_CAPACITY);}

在构造中涉及到了threadLocalHashCode和Entry,现在我们来分析一下这两个知识点。

ThreadLocalHashCode

HashCode相关代码在ThreadLocal类中

 //当前ThreadLocal对象的HashCode,非静态,说明每个ThreadLocal对象独有private final int threadLocalHashCode = nextHashCode();//为每一个ThreadLocal对象分配HashCode,静态,说明ThreadLocal对象共享private static AtomicInteger nextHashCode =new AtomicInteger();//黄金分割数,每分配一个HashCode,就需要增加这个数,可以使hash算法分散的更均匀private static final int HASH_INCREMENT = 0x61c88647;//返回HashCodeprivate static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}

Entry

Entry是ThreadLocalMap的静态内部类,用来构建key-value

 //WeakReference 弱引用,如果当前对象只有弱引用指向时,gc会回收这个对象//这里的key ThreadLocal就是弱引用,如果key被回收后,就为null,对应的value是强引用,会造成内存泄漏的问题static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}

关于ThreadLocal内存泄露的问题,可以去看看这篇文章,本文不解释概念了。

测试ThreadLocal 在gc后引发的threadLocalMap的key为null,但value不为null的情况

在上面这篇文章的代码里,debug会输出一些其他值,请将上文42行的输出语句改为下面的即可

System.out.println("弱引用key:"+referenceField.get(o)+",值:"+valueField.get(o));

到这里我们可以大概清楚了ThreadLocal的结构

线程Thread内部持有ThreadLocalMap的引用,Map内部使用Entry数组进行保存以ThreadLocal为key的k-v键值对Entry对象。

接下来我们就去看看ThreadLocalMap的相关操作方法

set()流程

 private void set(ThreadLocal<?> key, Object value) {//散列表Entry[] tab = table;//长度int len = tab.length;//计算当前ThreadLocal存放的下标int i = key.threadLocalHashCode & (len-1);//e=tab[i] 将当前桶位的Entry赋值给e//e!=null 说明当前桶位已经有数据了,发生了hash冲突//e = tab[i = nextIndex(i, len)] 每一轮循环后,线性的向后查找//ThreadLocalMap和HashMap不同,HashMap解决hash冲突的方法是链地址法,而ThreadLocalMap是线性探测for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {//获取当前Entry的key ThreadLocal对象ThreadLocal<?> k = e.get();//如果传入的key和当前桶位的ThreadLocal对象相同if (k == key) {//说明是替换操作e.value = value;return;}//k==null 说明了什么//Entry不为null,而key为null,说明这是个过期数据,key已经被gc回收了,而value没有释放if (k == null) {//传入当前桶位下标和k-v,去替换当前过期数据replaceStaleEntry(key, value, i);return;}}//退出循环,说明找到了合适的桶位,创建Entry放入对应的桶位。tab[i] = new Entry(key, value);//设置容量int sz = ++size;//cleanSomeSlots 启发式清理,后面分析//!cleanSomeSlots(i, sz)成立,说明没有清理到过期数据//sz >= threshold成立,说明需要扩容if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}//staleSlot:是一个过期Entry的下标private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {//获取散列表Entry[] tab = table;//获取散列表数组长度int len = tab.length;//临时变量Entry e;//表示开始探测式清理过期数据的开始下标。默认从当前 staleSlot开始。int slotToExpunge = staleSlot;//以当前staleSlot开始 向前迭代查找,找有没有过期的数据。for循环一直到碰到null结束。for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len)){//过期数据指的是Entry不为null,而key为nullif (e.get() == null){//说明向前找到了过期数据,更新探测清理过期数据的开始下标为i//只有找到了过期数据才slotToExpungeslotToExpunge = i;}}//以当前staleSlot向后去查找,直到碰到null为止。for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {//获取当前元素 keyThreadLocal<?> k = e.get();//条件成立:说明是替换if (k == key) {//替换新数据。e.value = value;//交换位置的逻辑//将table[staleSlot]这个过期数据放到当前循环到的table[i]这个位置,table[i]就是etab[i] = tab[staleSlot];//将tab[staleSlot] 中保存为当前entry。就是将i和staleSlot的元素进行交换tab[staleSlot] = e;//条件成立:// 1.说明replaceStaleEntry 一开始时 的向前查找过期数据 并未找到过期的entry.// 2.向后检查过程中也未发现过期数据if (slotToExpunge == staleSlot)//开始探测式清理过期数据的下标修改为当前循环的islotToExpunge = i;//cleanSomeSlots :启发式清理//expungeStaleEntry:探测式清理cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}//条件1:k == null 说明当前遍历的entry是一个过期数据.//条件2:slotToExpunge == staleSlot 说明向前查找过期数据并未找到过期的entry.if (k == null && slotToExpunge == staleSlot)//因为向后查询过程中查找到一个过期数据了,更新slotToExpunge 为当前位置。//前提条件是前驱扫描时未发现过期数据 slotToExpunge = i;}//如果执行到这里//说明向后查找过程中并未发现 k == key 的entry,说明当前set操作是添加//直接将新数据添加到 table[staleSlot]对应的桶位//因为调用此方法处就判断当前桶位是过期数据,所以先清空valuetab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);//条件成立:除了当前staleSlot以外 ,还发现其它的过期桶位,所以要开启清理数据的逻辑if (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}

因为ThreadLocal是一个弱引用,会引发内存泄漏的问题,所以在设计ThreadLocal时,内部就已经做好了两种过期数据回收,这个会在后面提到。在set()的流程中可以发现,如果发生了hash冲突,就会向当前桶位的后面线性的查找空桶位进行存放数据,这种方式被称为线性探测。

get()流程

在ThreadLocal里,get()会调用到这个方法里,所以我们从这里开始分析

 private Entry getEntry(ThreadLocal<?> key) {//计算下标int i = key.threadLocalHashCode & (table.length - 1);//获取当前桶位的EntryEntry e = table[i];//如果Entry不为null,并且key相同if (e != null && e.get() == key)//返回return e;else//进入这里,说明发生过hash冲突,当前要查询的ThreadLocal对象被放在了其他的桶位return getEntryAfterMiss(key, i, e);}private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;//向后遍历数组,直到碰到空Entry为止while (e != null) {//遍历到的Entry的ThreadLocalThreadLocal<?> k = e.get();if (k == key)//相同就返回return e;if (k == null)//Entry!=null,k==null 过期数据//探测式过期数据回收expungeStaleEntry(i);else//继续向后查询i = nextIndex(i, len);e = tab[i];}//说明没有该数据return null;}

get()的逻辑还算简单,计算下标后线性查询,并且碰到过期数据就进行探测式回收。

remove()流程

接下来是remove的逻辑

private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);//线性向后遍历for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {//查找到对应的Entryif (e.get() == key) {//将Entry的key设置为nulle.clear();//从当前下标开始进行探测式清理expungeStaleEntry(i);return;}}
}

remove也很简单,线性查找到后,从当前桶位开始探测式清理。

扩容流程

在set方法里还有一个rehash()方法没有分析,这里就设计到map的扩容流程了。

private void rehash() {//过期数据回收expungeStaleEntries();//如果清理完后的容量>=扩容阈值的3/4if (size >= threshold - threshold / 4)//进行扩容resize();
}private void expungeStaleEntries() {Entry[] tab = table;int len = tab.length;//遍历散列表for (int j = 0; j < len; j++) {Entry e = tab[j];//如果是过期数据if (e != null && e.get() == null)//从当前桶位开始进行一次探测式清理expungeStaleEntry(j);}
}private void resize() {//获取当前散列表Entry[] oldTab = table;//获取旧表的长度int oldLen = oldTab.length;//新表长度为旧表长度的2倍int newLen = oldLen * 2;//创建新表Entry[] newTab = new Entry[newLen];//计算新表中元素的个数int count = 0;//遍历旧表for (int j = 0; j < oldLen; ++j) {//遍历到的元素Entry e = oldTab[j];//有数据if (e != null) {ThreadLocal<?> k = e.get();if (k == null) {//说明是一个过期数据,释放valuee.value = null; // Help the GC} else {//计算该元素在新表的下标int h = k.threadLocalHashCode & (newLen - 1);//可能发生了hash冲突,所以要找到一个合适的位置存放while (newTab[h] != null)h = nextIndex(h, newLen);newTab[h] = e;//计算数量count++;}}}//设置下一次的扩容阈值setThreshold(newLen);//设置容量size = count;//将新表赋值给tabletable = newTab;
}

扩容的流程也是比较简单的,但是有没有朋友看到这会想,在多线程环境下,扩容为什么没有加锁呢?其实很简单,这个map是由线程自己保管的,所以扩容是一个单线程的逻辑。

到这里ThreadLocalMap的常用方法就分析完成了,接下来就该分析探测式清理和启发式清理的逻辑。

探测式清理

探测式清理其实也比较好理解,和线性探测思想一样,遍历散列表清理过期数据。

//staleSlot:从调用处看,为过期数据的下标
private int expungeStaleEntry(int staleSlot) {//获取散列表Entry[] tab = table;//散列表长度int len = tab.length;//因为是过期数据,将value和Entry都设置为nulltab[staleSlot].value = null;tab[staleSlot] = null;//清理了当前过期数据,容量-1size--;Entry e;int i;//i = nextIndex(staleSlot, len) 从过期数据开始,向后遍历//(e = tab[i]) != null 直到遍历桶位为null,即table[i]==nullfor (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();//过期数据处理if (k == null) {//清空e.value = null;tab[i] = null;//容量-1size--;} else {//说明这里是正常的数据//因为前面可能清理了一部分过期数据,导致前面的桶位为null,这时要解决hash冲突带来的影响//ThreadLocalMap使用线性探测来解决hash冲突,导致有些桶位的数据会有一定的偏移//所以在这里重新计算下标,找到一个最接近hash算法计算出来的桶位进行存放//计算下标int h = k.threadLocalHashCode & (len - 1);//i是当前下标//成立:说明当前的Entry发生过冲突if (h != i) {//将当前桶位设置为nulltab[i] = null;//从计算的下标开始遍历,找到合适的桶位进行存放while (tab[h] != null)h = nextIndex(h, len);//有可能是h,也有可能是h到i中间的某个位置tab[h] = e;}}}//退出循环,下标为i的桶位就是table[i]==nullreturn i;
}

探测式清理过程也比较好理解,从过期数据开始向后遍历查找过期数据,如果碰到正常数据,就重新计算下标,减少hash冲突带来的影响。

启发式清理

//i:从调用处来看,下标为i的桶位一定不是过期数据,启发式清理从i后面开始
//n:有可能为当前元素个数,有可能为当前散列表长度,这里按长度16来举例
private boolean cleanSomeSlots(int i, int n) {//是否清理过数据boolean removed = false;//散列表Entry[] tab = table;//散列表长度int len = tab.length;do {//获取i下一个桶位i = nextIndex(i, len);Entry e = tab[i];//如果是过期数据if (e != null && e.get() == null) {//将n设置为数组长度n = len;//清理过数据,所以设置为trueremoved = true;//从当前桶位开始再进行一次探测式清理i = expungeStaleEntry(i);}//n=16//  16 >>> 1 =8//  8  >>> 1 =4//  4  >>> 1 =2//  2  >>> 1 =1//  1  >>> 1 =0//当n=16时,会进行5次搜索,如果找到过期数据,会从过期数据开始进行一次探测式清理} while ( (n >>>= 1) != 0);return removed;
}

到这里,关于ThreadLocal的一些重要方法就已经分析完毕了,还没有看懂的朋友,请打开源码,跟着流程一起走一遍,有些地方需要自己动手画一画才知道这些代码做了哪些操作。

ThreadLocal源码分析(jdk1.8)相关推荐

  1. ThreadLocal 源码分析(JDK1.8)

    ThreadLocal 为线程提供了线程本地变量,不同于其他的变量,线程本地变量是通过 get() 和 set() 方法.ThreadLocal 通常是一个私有静态域,与Thread中的某个状态相关联 ...

  2. Java并发编程之ThreadLocal源码分析

    1 一句话概括ThreadLocal   什么是ThreadLocal?顾名思义:线程本地变量,它为每个使用该对象的线程创建了一个独立的变量副本. 2 ThreadLocal使用场景   用一句话总结 ...

  3. ThreadLocal源码分析-黄金分割数的使用

    前提# 最近接触到的一个项目要兼容新老系统,最终采用了ThreadLocal(实际上用的是InheritableThreadLocal)用于在子线程获取父线程中共享的变量.问题是解决了,但是后来发现对 ...

  4. Java8 ThreadLocal 源码分析

    可参考文章: Java8 IdentityhashMap 源码分析 IdentityhashMap 与 ThreadLocalMap 一样都是采用线性探测法解决哈希冲突,有兴趣的可以先了解下 Iden ...

  5. 【阅读源码系列】ConcurrentHashMap源码分析(JDK1.7和1.8)

    个人学习源码的思路: 使用ctrl+单机进入源码,并阅读源码的官方文档–>大致的了解一下此类的特点和功能 使用ALIT+7查看类中所有方法–>大致的看一下此类的属性和方法 找到重要方法并阅 ...

  6. ConcurrentHashMap的源码分析-JDK1.7和Jdk1.8版本的变化

    ConcurrentHashMap和HashMap的实现原理是差不多的,但是因为ConcurrentHashMap需要支持并发操作,所以在实现上要比hashmap稍微复杂一些. 在JDK1.7的实现上 ...

  7. ConcurrentHashMap源码分析(jdk1.8)

    本文适用于了解过ConcurrentHashMap部分原理,但又没仔细研究过源码的同学!并且本文不涉及红黑树相关的操作,能力有限,掌握的不是很好.在看源码时,可以注意到Doug lea的编码风格,就是 ...

  8. ThreadLocal源码分析

    ThreadLocal的作用 Java对象是线程间共享的,但有时我们需要一些线程间隔离的对象,该对象只能由同一个线程读写,对其他线程不可见.ThreadLocal正式提供了这样的机制,详细使用方式请参 ...

  9. HashMap源码分析-jdk1.6和jdk1.8的区别

    2019独角兽企业重金招聘Python工程师标准>>> 在java集合中,HashMap是用来存放一组键值对的数,也就是key-value形式的数据,而在jdk1.6和jdk1.8的 ...

最新文章

  1. 九宫格抽奖转盘源码分析
  2. 【转】Android 获取本机号码(收集)
  3. 发现自己的BLOG被转载了
  4. Java服务框架分析
  5. c语言自学技巧,轻松学C语言,教给你学习技巧
  6. Http / Put 和 Post 区别
  7. 第三次学JAVA再学不好就吃翔(part38)--抽象类与接口的区别
  8. 【零基础学Java】—初识Java(一)
  9. java 日期相差年份_JAVA计算两个日期相差的实例
  10. Linux平台下使用AdventNet ManageEngine OpUtils监控网络
  11. GIS公司分布图GIS院校分布图
  12. t460p加固态硬盘 thinkpad_联想T460p加装固态硬盘
  13. 基于单片机的贪吃蛇游戏
  14. 曹汛:计算摄像学研究 | VALSE2017之十六
  15. 惠普战66prog2拆机_惠普战66二代内部做工怎么样?全新惠普战66二代拆机图解评测(含视频教程)...
  16. java 搞笑的事情_一件搞笑的事作文(精选10篇)
  17. 文件夹打包成pkg_linux如何解压tar.gz到指定文件夹或目录
  18. WPS为什么设置段前一行没反应
  19. 计算机在食品上的应用论文,文字设计在食品包装设计中的运用论文
  20. kungfu panda

热门文章

  1. php closure($this),PHP 中的Closure
  2. element UI 设置滚动条颜色
  3. mac 查看音频信息
  4. DataFrame的行数和列数计算
  5. Python基础: repr函数和str的区别
  6. 描述常见的计算机硬件系统的构成,用简单的语言描述计算机硬件系统构成
  7. SQL中如何将月份转换为英文缩写
  8. 4Gu盘格式化之后124M
  9. iframe嵌套详解
  10. linux安装oracle XE