前言

  在高并发的环境下,当我们使用一个公共的变量时如果不加锁会出现并发问题,例如SimpleDateFormat,但是加锁的话会影响性能,对于这种情况我们可以使用ThreadLocal。ThreadLocal是将公共变量copy一份到线程私有内存中以消除并发问题,ThreadLocal是JDK内部提供的高效解决并发问题的工具类之一,本文介绍ThreadLocal的重要方法的源码实现以及相关问题的分析。

数据结构

  由上图可以看出,在Thread中维护了一个Entry的列表,Entry存储的是公共变量的copy,这个列表是由ThreadLocal维护,每次从ThreadLocal中获取的是一个公共变量的副本,所以避免了并发问题,接下来在看实现方法之前我们先看一下上面提到的类的定义。

变量

  首先来看一下ThreadLocal重要的成员变量。

 public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}

  threadLocalHashCode是ThreadLocal实例的ID,其主要用于做散列(后面会讲到),从上面可以看出来threadLocalHashCode是所有ThreadLocal贡献的原子变量nextHashCode加上一个固定的HASH_INCREMENT生成的。为什么

  HASH_INCREMENT的值是0x61c88647?

  前面说了,ThreadLocal是使用散列做存储的,而这个数字可以让生成出来的ThreadLocal的ID较为均匀地分布在大小为2的N次方的数组中。

  接下来看一下ThreadLocal中真正存储数据的Entry类。

 static class ThreadLocalMap {

         static class Entry extends WeakReference<ThreadLocal<?>> {

             Object value;

             Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
} private static final int INITIAL_CAPACITY = 16; private Entry[] table; private int size = 0; private int threshold;
}

  ThreadLocalMap是ThreadLocal的内部类,Entry继承WeakReference,前面提到Entry是K-V形式,key就是WeakReference的成员变量referent,value就是Entry的value。我们可以发现,key是一个弱引用,它的生命周期到下一个GC就结束了,为什么要这样设计呢?在后面的内存泄漏中会提到。

  table是一个大小为2的N次方的数组,threshold是数组扩容的临界点(与HashMap一个作用),默认是数组的大小的2/3,数组每次扩容是将长度扩大1倍。

方法

  ThreadLocal重要的方法如下:

 public T get(); //获取值

 public void set(T value); //设置值

 public void remove(); //删除值

  首先来看set()方法:

 public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //获取当前Thread中的ThreadLocalMap
if (map != null)
map.set(this, value); //设置值
else
createMap(t, value); //创建一个ThreadLocalMap
}

  在Thread类中有 ThreadLocal.ThreadLocalMap threadLocals = null; 这样的成员变量,就是用来存储当前线程设置的值,所有的ThreadLocal都是在操作这个成员变量。

  接下来看一下map.set()这个方法:

 private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len - 1); // 计算hash散列 // 从i开始遍历列表
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到了key值
if (k == key) {
e.value = value;
return;
}
// key=null表示该key值已被回收
if (k == null) {
replaceStaleEntry(key, value, i); return;
}
}
// 当遍历到一个可以插入数据的空位置时
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) // 清理列表中已经被GC回收的
&& sz >= threshold) // 判断是否需要扩容
rehash();// 扩容并重新计算hash散列
}

  ThreadLocal通过遍历Entry数组,如果当前key已存在则覆盖,没有则新增,如果在遍历过程中遇到已经被GC回收的key,则会清除掉无效的key对应的值。

  从上面的循环可以看出来,ThreadLocal采用hash散列的线性探测存储,这种方式实现简单但是无法存储大量的数据,所以不建议用ThreadLocal存储大量的数据。这里还有一个问题是,nextIndex()方法是循环的获取数组下标,所以如果table满的就会导致无限循环,所以threshold的值是小于table的大小并且每次set之后都会清理一次数组的无效数据。

  我们先看一下cleanSomeSlots()方法:

 private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i); // 从下标i开始清除数组中的无效元素
}
} while ( (n >>>= 1) != 0); // 该循环会循环log2(n)次
return removed;
}

  该方法是通过线性查找,从下标为i开始查找已经被GC清理的key对应的值,一般情况下会查找log2(n)次,expungeStaleEntry()是真正的清理方法,源码如下:

 private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length; // 清理对应数据
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--; // 重新计算hash散列,直到遇见null
Entry e;
int i;
for (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;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

  前面提到,ThreadLocal存储数据的方式是使用线性探测法,所以expungeStaleEntry()方法在清理掉一个数据之后会将该下标之后的所有非null位置重新计算一次hash散列,经过这样的操作之后,数组中的元素最终会符合hash散列的要求,如果不重新计算一次hash散列,那么最终数组结果可能不符合hash散列的要求,比如:元素a、b计算后得到存储位置冲突,通过线性探测法,最终结果是元素a在下标0,元素b在下标1,如果删除a之后,b就应该放在下标0而不是下标1上。

  我们在回过头来看看map.set()方法中的replaceStaleEntry()方法:

 private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// slotToExpunge用来记录清除无效key的开始位置,初始值等于staleSlot,staleSlot的值是无效key的下标
int slotToExpunge = staleSlot;
// 向前查找无效的key
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i; // 向后查找无效的key,直到遍历到一个有效key
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get(); // 如果在查找的过程中找到了我们需要找的key,则将无效key与该值替换
if (k == key) {
e.value = value; tab[i] = tab[staleSlot];
tab[staleSlot] = e; // 判断在向前查找无效key的过程中有没有找到
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 清理无效key
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果向前没有查找到无效key并且当前key为无效key
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
} // 将需要插入的数据插入到staleSlot位置
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value); // 如果向前查找到了无效key或者向后查找到了无效key,则清理无效key
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

  该方法时在set数据值遍历到了一个无效key执行的方法,ThreadLocal认为,出现无效key的位置附近也会出现无效key,所以在清理该无效key的时候回查找附近连续的无效key。

  值得注意的是,代码中找到了我们需要找的key的时候,将无效key与有效key交换的原因是,清除方法expungeStaleEntry()只能够清除连续的无效的key,如果向前没有找到无效key,向后找到了无效key的情况下,会出现如下情况:

  SS是staleSlot,SE是slotToExpunge,如果不交换,则无法清理到SS位置的无效key,而且需要找的key后面同样也可能会出现无效key,同样无法清除到。

  除了这种情况,还有一种情况是向前找到了无效key,会出现如下情况:

  这种情况下,交换了SS跟需要找的key之后会出现无法清理SS位置的无效key的请款,所以代码中不止调用了一次expungeStaleEntry(),还会调用cleanSomeSlots(),源码在前面已经分析过了,这样就可以把SS以及之后的无效key都清掉。

  以上set()方法的源码就分析完了,接下来是get()方法,get()方法是调用ThreadLocalMap的getEntry()方法,所以我们直接分析getEntry()方法:

 private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e); // hash散列之后的位置不是需要找的key,即发生了hash碰撞
} private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length; while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i); // 清理无效key
else
i = nextIndex(i, len); // 向后遍历
e = tab[i];
}
return null;
}

  get()方法较为简单,这里不做过多赘述,接下来是remove()方法,同样该方法是直接调用的ThreadLocalMap的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)]) {
if (e.get() == key) {
e.clear();// 将Entry的key置为null
expungeStaleEntry(i); // 清理该key
return;
}
}
}

为什么会内存泄漏

  我们已经知道Entry的key是一个弱引用,当他被GC之后,那么这个Entry就是一个无效的对象,无法被外部获取到。而在Thread对象中是持有了ThreadLocalMap的引用,而ThreadLocalMap又持有了Entry数组的引用,所以该无效的Entry是无法被回收的,如果不手动的删除就会发生内存泄漏,好在ThreadLocal在set(),get()方法中都会主动去清理无效的Entry防止内存泄漏,但是依然会存在内存泄漏的风险,所以在编程中需要注意remove()不需要的值

为什么使用弱引用

  弱引用可以防止内存泄漏。试想,如果一个线程中的ThreadLocal已经不需要了,所以将指向ThreadLocal的强引用删掉,希望GC清理调它。如果Entry是强引用的话GC就无法清理该对象,因为还存在一个Thread->ThreadLocalMap->Entry->ThreadLocal的强引用链,导致整个Entry都不会被回收,从而发生内存泄漏。

线程池与ThreadLocal共有引发的问题

  为了方便分析,先假设线程池是固定大小的,当我们在从线程池中拿了一个线程运行如下逻辑:

 if(threadLocal.get()!=null){
//doSomething
}else{
threadLocal.set(data);
}

  或许你在最开始程序可以正常运行,但是后面你会发现代码 threadLocal.get() 永远都不为null,这是因为线程池中的线程时复用的,所以Thread就不会被回收,Thread不回收那么对应ThreadLocal也不会被回收,所以最后就出现了上述问题。在使用Tomcat等使用线程池的技术时需要注意这一点。

ThreadLocal源码及相关问题分析的更多相关文章

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

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

  2. Java多线程学习之ThreadLocal源码分析

    0.概述 ThreadLocal,即线程本地变量,是一个以ThreadLocal对象为键.任意对象为值的存储结构.它可以将变量绑定到特定的线程上,使每个线程都拥有改变量的一个拷贝,各线程相同变量间互不 ...

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

    ## 1 一句话概括ThreadLocal<font face="微软雅黑" size=4>  什么是ThreadLocal?顾名思义:线程本地变量,它为每个使用该对象 ...

  4. ThreadLocal详解,ThreadLocal源码分析,ThreadLocal图解

    本文脉路: 概念阐释 ---->  原理图解  ------> 源码分析 ------>  思路整理  ----> 其他补充. 一.概念阐述. ThreadLocal 是一个为 ...

  5. 【JAVA】ThreadLocal源码分析

    ThreadLocal内部是用一张哈希表来存储: static class ThreadLocalMap { static class Entry extends WeakReference<T ...

  6. 并发-ThreadLocal源码分析

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

  7. Java -- 基于JDK1.8的ThreadLocal源码分析

    1,最近在做一个需求的时候需要对外部暴露一个值得应用  ,一般来说直接写个单例,将这个成员变量的值暴露出去就ok了,但是当时突然灵机一动(现在回想是个多余的想法),想到handle源码里面有使用过Th ...

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

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

  9. Threadlocal源码分析以及其中WeakReference作用分析

    今天在看Spring 3.x企业应用开发实战,第九章 Spring的事务管理,9.2.2节ThreadLocal的接口方法时,书上有提到Threadlocal的简单实现,我就去看了下JDK1.8的Th ...

随机推荐

  1. python爬虫案例:使用XPath爬网页图片

    用XPath来做一个简单的爬虫,尝试爬取某个贴吧里的所有帖子,并且将该这个帖子里每个楼层发布的图片下载到本地. # -*- coding:utf-8 -*- import urllib import ...

  2. 这42个Python小例子,太走心

    告别枯燥,60秒学会一个Python小例子.奔着此出发点,我在过去1个月,将平时经常使用的代码段换为小例子,分享出来后受到大家的喜欢. 一.基本操作 1 链式比较 i = 3print(1 <  ...

  3. IfcWallStandardCase 构件吊装模拟

    wall_node = (osg::Node*)(index_node->clone(osg::CopyOp::DEEP_COPY_ALL));vc_mobileCrane->tranMo ...

  4. opengl读取灰度图生成三维地形并添加光照

    转自:https://www.cnblogs.com/gucheng/p/10152889.html 准备第三方库 glew.freeglut.glm.opencv 准备一张灰度图 最终效果 代码如下 ...

  5. SpringBoot系列教程web篇之Freemaker环境搭建

    现在的开发现状比较流行前后端分离,使用springboot搭建一个提供rest接口的后端服务特别简单,引入spring-boot-starter-web依赖即可.那么在不分离的场景下,比如要开发一个后 ...

  6. Maven专题

    Maven 教程之 settings.xml 详解

  7. LeetCode 513. 找树左下角的值(Find Bottom Left Tree Value)

    513. 找树左下角的值 513. Find Bottom Left Tree Value 题目描述 给定一个二叉树,在树的最后一行找到最左边的值. LeetCode513. Find Bottom ...

  8. ES6新增的一些特性

    1.let关键字,用来代替 var的关键字,特点: 1.变量不允许被重复定义 2.不会进行变量声明提升 3.保留块级作用域中i的 2.const定义常量,特点:1.常量值不允许被改变 2.不会进行变量 ...

  9. [转帖]Redis性能解析--Redis为什么那么快?

    Redis性能解析--Redis为什么那么快? https://www.cnblogs.com/xlecho/p/11832118.html echo编辑整理,欢迎转载,转载请声明文章来源.欢迎添加e ...

  10. python 之 Django框架(路由系统、include、命名URL和URL反向解析、命名空间模式)

    12.36 Django的路由系统 基本格式: from django.conf.urls import url urlpatterns = [ url(正则表达式, views视图函数,参数,别名) ...