文章部分图片来自参考资料

问题 :

  • ThreadLocal 底层原理
  • ThreadLocal 需要注意什么问题,造成问题的原因是什么,防护措施是什么

ThreadLocal 概述

ThreadLocal 线程本地变量 ,是一个工具,可以让多个线程保持一个变量的副本,那么每个线程可以访问自己内部的副本变量。

ThreadLocal 结构图里面看到有两个内部类,一个 SuppliedThreadLocal , 一个ThreadLocalMap 。下面用一张图来说明线程使用的示意图。可以看到每个Thread 有个 ThreadLocalMap ,然后里面由hash值分列的的数组 Entry[] 。Entry 数据结构就是图中淡绿色框内所示。所以 ThreadLocal 里面放的 value 应该是放在Thread里面的。

 

ThreadLocal  源码分析

ThreadLocal 下文简称 TL, TL最常见的方法就是 get 和 set 了。

  1 public void set(T value) {
2 Thread t = Thread.currentThread();
3 ThreadLocalMap map = getMap(t);
4 if (map != null)
5 map.set(this, value);
6 else
7 createMap(t, value);
8 }
 
  1     public T get() {
2 Thread t = Thread.currentThread();
3 ThreadLocalMap map = getMap(t);
4 if (map != null) {
5 ThreadLocalMap.Entry e = map.getEntry(this);
6 if (e != null) {
7 @SuppressWarnings("unchecked")
8 T result = (T)e.value;
9 return result;
10 }
11 }
12 return setInitialValue();
13 }
 
  1     ThreadLocalMap getMap(Thread t) {
2 return t.threadLocals;
3 }
  1  ThreadLocal.ThreadLocalMap threadLocals = null;

可以看到thread 内部中持有TL的内部类变量。我们来看一下 ThreadLocalMap, threadLocalMap 内部定义一个类,Entry 类。这是threadLocalMap  内的变量

ThreadLocalMap  类

  1 static class ThreadLocalMap {
2 /**
3 * The initial capacity -- MUST be a power of two.
4 */
5 private static final int INITIAL_CAPACITY = 16;
6
7 /**
8 * The table, resized as necessary.
9 * table.length MUST always be a power of two.
10 */
11 private Entry[] table;
12
13 /**
14 * The number of entries in the table.
15 */
16 private int size = 0;
17
18 /**
19 * The next size value at which to resize.
20 */
21 private int threshold; // Default to 0
22 }
23
  1  static class Entry extends WeakReference<ThreadLocal<?>> {
2 /** The value associated with this ThreadLocal. */
3 Object value;
4
5 Entry(ThreadLocal<?> k, Object v) {
6 super(k);
7 value = v;
8 }
9 }

我们看到 TL 的set 方法实际就是调用了 ThreadLocalMap 的set 方法。

  1  private void set(ThreadLocal<?> key, Object value) {
2
3 // We don't use a fast path as with get() because it is at
4 // least as common to use set() to create new entries as
5 // it is to replace existing ones, in which case, a fast
6 // path would fail more often than not.
7
8 Entry[] tab = table;
9 int len = tab.length;
10 int i = key.threadLocalHashCode & (len-1);
11
12 for (Entry e = tab[i];
13 e != null;
14 e = tab[i = nextIndex(i, len)]) {
15 ThreadLocal<?> k = e.get();
16
17 //找到相同的 key
18 if (k == key) {
19 e.value = value;
20 return;
21 }
22
23 //某个key失效
24 if (k == null) {
25 replaceStaleEntry(key, value, i);
26 return;
27 }
28 }
29
30 //走到这里必定是退出了循环,即是遇到空的 entry ,直接放在空的地方,检查是否需要扩容,重新 hash
31 tab[i] = new Entry(key, value);
32 int sz = ++size;
33 if (!cleanSomeSlots(i, sz) && sz >= threshold)
34 rehash();
35 }
36
37
38 // 这个方法是替代某些失效的entry ,最终的值会放在 table[staleSlot]
39 // slotToExpunge 这个变量从名字上可以看出就是需要擦洗的 slot (指的是某个位置)
40 private void replaceStaleEntry(ThreadLocal<?> key, Object value,
41 int staleSlot) {
42 Entry[] tab = table;
43 int len = tab.length;
44 Entry e;
45
46 // Back up to check for prior stale entry in current run.
47 // We clean out whole runs at a time to avoid continual
48 // incremental rehashing due to garbage collector freeing
49 // up refs in bunches (i.e., whenever the collector runs).
50 // 向前找是否有失效节点,如果有做一下标记,即是为 slotToExpunge 赋值
51 int slotToExpunge = staleSlot;
52 for (int i = prevIndex(staleSlot, len);
53 (e = tab[i]) != null;
54 i = prevIndex(i, len))
55 if (e.get() == null)
56 slotToExpunge = i;
57
58 // Find either the key or trailing null slot of run, whichever
59 // occurs first
60 // 向后寻找是否有相同的 key
61 for (int i = nextIndex(staleSlot, len);
62 (e = tab[i]) != null;
63 i = nextIndex(i, len)) {
64 ThreadLocal<?> k = e.get();
65
66 // If we find key, then we need to swap it
67 // with the stale entry to maintain hash table order.
68 // The newly stale slot, or any other stale slot
69 // encountered above it, can then be sent to expungeStaleEntry
70 // to remove or rehash all of the other entries in run.
71 // 找到相同的值,交换位置到 tab[staleSlot]
72 if (k == key) {
73 e.value = value;
74
75 tab[i] = tab[staleSlot];
76 tab[staleSlot] = e;
77
78 // Start expunge at preceding stale entry if it exists
79 // 擦洗失效值
80 if (slotToExpunge == staleSlot)
81 slotToExpunge = i;
82 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
83 return;
84 }
85
86 // If we didn't find stale entry on backward scan, the
87 // first stale entry seen while scanning for key is the
88 // first still present in the run.
89 if (k == null && slotToExpunge == staleSlot)
90 slotToExpunge = i;
91 }
92
93 // If key not found, put new entry in stale slot
94 //找不到值会放在 tab[staleSlot] ,即原来失效值的位置上
95 tab[staleSlot].value = null;
96 tab[staleSlot] = new Entry(key, value);
97
98 // If there are any other stale entries in run, expunge them
99 // 擦洗失效值
100 if (slotToExpunge != staleSlot)
101 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
102 }
103

可以看到我们在 set 的时候,TL内会检查是否存在失效值。也可以看到 ThreadLocalMap 的Hash 中解决冲突的方式只是简单的向下寻找空的位置,即线性探测,这样的效率比较低,所以建议 :

         每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。

下面看一下 get 方法,不难。

  1 // ThreadLocalMap
2 private Entry getEntry(ThreadLocal<?> key) {
3 int i = key.threadLocalHashCode & (table.length - 1);
4 Entry e = table[i];
5 if (e != null && e.get() == key)
6 return e;
7 else
8 return getEntryAfterMiss(key, i, e);
9 }
  1        private Entry getEntry(ThreadLocal<?> key) {
2 int i = key.threadLocalHashCode & (table.length - 1);
3 Entry e = table[i];
4 if (e != null && e.get() == key)
5 return e;
6 else
7 //获取的时候出现失效的entry
8 return getEntryAfterMiss(key, i, e);
9 }
10
11
12 // 往后找,失效的值擦洗掉,没有就返回 Null
13 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
14 Entry[] tab = table;
15 int len = tab.length;
16
17 while (e != null) {
18 ThreadLocal<?> k = e.get();
19 if (k == key)
20 return e;
21 if (k == null)
22 expungeStaleEntry(i);
23 else
24 i = nextIndex(i, len);
25 e = tab[i];
26 }
27 return null;
28 }

使用ThreadLocal内存失效问题分析

为什么使用弱引用

我们来看一下weakReference 表示弱引用,java中有四种引用类型,强引用,弱引用,软引用,虚引用。

在Java语言中, 当一个对象o被创建时, 它被放在Heap里. 当GC运行的时候, 如果发现没有任何引用指向o, o就会被回收以腾出内存空间. 也就是说, 一个对象被回收, 必须满足两个条件:

  • 没有任何引用指向它

  • GC被运行.

  1 DemoA a=new DemoA();
2 DemoB b=new DemoB(a);

假如有下面代码,如果我们增加一行代码来将a对象的引用设置为null,当一个对象不再被其他对象引用的时候,是会被GC回收的,但是对于这个场景来说,即时是a=null,也不可能被回收,因为DemoB依赖DemoA,这个时候是可能造成内存泄漏的。

  1 DemoA a=new DemoA();
2 DemoB b=new DemoB(a);
3 a=null;

通过弱引用,有两个方法可以避免这样的问题。

  1 //方法1
2 DemoA a=new DemoA();
3 DemoB b=new DemoB(a);
4 a=null;
5 b=null;
6 //方法2
7 DemoA a=new DemoA();
8 WeakReference b=new WeakReference(a);
9 a=null;
10

对于方法2来说,DemoA只是被弱引用依赖,假设垃圾收集器在某个时间点决定一个对象是弱可达的(weakly reachable)(也就是说当前指向它的全都是弱引用),这时垃圾收集器会清除所有指向该对象的弱引用,然后把这个弱可达对象标记为可终结(finalizable)的,这样它随后就会被回收。

我们可以设想b就是ThreadLocal ,试想一下如果这里没有使用弱引用,意味着ThreadLocal的生命周期和线程是强绑定,只要线程没有销毁,那么ThreadLocal一直无法回收。而使用弱引用以后,当ThreadLocal被回收时,由于Entry的key是弱引用,不会影响ThreadLocal的回收防止内存泄漏,同时,在后续的源码分析中会看到,ThreadLocalMap本身的垃圾清理会用到这一个好处,方便对无效的Entry进行回收。

所以使用了弱引用的原因是为了防止 ThreadLocal 对象没有被正确回收而导致的内存泄漏。

ThreadLocalMap 使用了弱引用会导致内存泄漏

ThreadLocalMap下文简称  TLM ,是存在Thread 中的,那么它的生存周期必定和线程的生命周期一样长的。

  1 static class Entry extends WeakReference<ThreadLocal<?>> {
2 /** The value associated with this ThreadLocal. */
3 Object value;
4
5 Entry(ThreadLocal<?> k, Object v) {
6 super(k);
7 value = v;
8 }
9 }

ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。(出处

我们从源码中也可以看到在 get 和 set 等方法都有检查失效值的操作,同时当我们使用TL时,某个线程不再需要某个值的时候应该调用 remove 方法,下面代码中 e.clear() 这一句实际是调用了弱引用的 clear 方法,实现对对象的回收。

  1         private void remove(ThreadLocal<?> key) {
2 Entry[] tab = table;
3 int len = tab.length;
4 int i = key.threadLocalHashCode & (len-1);
5 for (Entry e = tab[i];
6 e != null;
7 e = tab[i = nextIndex(i, len)]) {
8 if (e.get() == key) {
9 e.clear();
10 expungeStaleEntry(i);
11 return;
12 }
13 }
14 }
  1     /**
2 * Clears this reference object. Invoking this method will not cause this
3 * object to be enqueued.
4 *
5 * <p> This method is invoked only by Java code; when the garbage collector
6 * clears references it does so directly, without invoking this method.
7 */
8 public void clear() {
9 this.referent = null;
10 }

其实我们从源码分析可以看到,ThreadLocalMap是做了防护措施的

  • 首先从ThreadLocal的直接索引位置(通过

    ThreadLocal.threadLocalHashCode & (len-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e

  • 如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry,否则,如果key值为null,则擦除该位置的Entry,否则继续向下一个位置查询

由上面的分析,我们知道了ThreadLocal 使用了弱引用后还是会导致内存泄漏,而内存泄漏的原因是 : ThreadLocalMap的生存周期和线程一样长,而不是使用弱引用导致的!!!         

Entry 的 Hash 值

如何实现一个线程多个ThreadLocal对象,每一个ThreadLocal对象是如何区分的呢?

  1 void createMap(Thread t, T firstValue) {
2 t.threadLocals = new ThreadLocalMap(this, firstValue);
3 }
  1 static class ThreadLocalMap {
2 static class Entry extends WeakReference<ThreadLocal<?>> {
3
4 /** The value associated with this ThreadLocal. */
5 Object value;
6
7 Entry(ThreadLocal<?> k, Object v) {
8 super(k);
9 value = v;
10 }
11 }
12
13 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
14 //构造一个Entry数组,并设置初始大小
15 table = new Entry[INITIAL_CAPACITY];
16 //计算Entry数据下标
17 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
18 //将`firstValue`存入到指定的table下标中
19 table[i] = new Entry(firstKey, firstValue);
20 size = 1;//设置节点长度为1
21 setThreshold(INITIAL_CAPACITY); //设置扩容的阈值
22 }
23 //...省略部分代码
24 }
25
26
  1 private final int threadLocalHashCode = nextHashCode();
2 private static AtomicInteger nextHashCode = new AtomicInteger();
3 private static final int HASH_INCREMENT = 0x61c88647;
4
5 private static int nextHashCode() {
6 return nextHashCode.getAndAdd(HASH_INCREMENT);
7 }

那为什么要使用到 0x61c88647 这个值呢? 我们首先要明白一点,散列的目的是使数据分布更加均匀。那么这个数字的使用必定会达到这个目的。

魔数0x61c88647的选取和斐波那契散列有关,0x61c88647对应的十进制为1640531527。而斐波那契散列的乘数可以用 (long)((1L<<31)*(Math.sqrt(5)-1)); 如果把这个值给转为带符号的int,则会得到-1640531527。也就是说(long)((1L<<31)*(Math.sqrt(5)-1));得到的结果就是1640531527,也就是魔数0x61c88647

建议

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

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

  • 在线程池中使用ThreadLocal ,有可能会出现数据混淆的情况,原因是数据没及时清理,线程放回线程池中又被拿出来使用。

总结

  • ThreadLocal 使用 弱引用的原因是为了处理非常大和生命周期非常长的线程,为了防止 ThreadLocal回收导致的内存泄漏,
    但是使用了弱引用也有可能导致内存泄漏,这次 ThreadLocal被回收了,但是value一直存活着,要是没有手动删除的话,
    依旧会导致内存泄漏,所以ThreadLocalMap 的 get ,set 中都有防护措施---检查ThreadLocal 为空的 Entry ,然后删除掉该
    Entry.
  • 在使用完ThreadLocal,记得调用remove 方法。

参考资料 :

java 并发(七)--- ThreadLocal的更多相关文章

  1. Java并发:ThreadLocal的简单介绍

    作者:汤圆 个人博客:javalover.cc 前言 前面在线程的安全性中介绍过全局变量(成员变量)和局部变量(方法或代码块内的变量),前者在多线程中是不安全的,需要加锁等机制来确保安全,后者是线程安 ...

  2. Java并发基础--ThreadLocal

    一.ThreadLocal定义 ThreadLocal是一个可以提供线程局部变量的类,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路,通过为每个线程提供一个独立的变量副本解决了变量 ...

  3. JAVA并发七(多线程环境中安全使用集合API)

    在集合API中,最初设计的Vector和Hashtable是多线程安全的.例如:对于Vector来说,用来添加和删除元素的方法是同步的.如果只有一个线程与Vector的实例交互,那么,要求获取和释放对 ...

  4. Java并发编程:深入剖析ThreadLocal (总结)

    ThreadLocal好处 Java并发编程的艺术解释好处是:get和set方法的调用可以不用在同一个方法或者同一个类中. 问答形式总结: 1. ThreadLocal类的作用 ThreadLocal ...

  5. 【Java 并发】详解 ThreadLocal

    前言 ThreadLocal 主要用来提供线程局部变量,也就是变量只对当前线程可见,本文主要记录一下对于 ThreadLocal 的理解.更多关于 Java 多线程的文章可以转到 这里. 线程局部变量 ...

  6. java基础解析系列(七)---ThreadLocal原理分析

    java基础解析系列(七)---ThreadLocal原理分析 目录 java基础解析系列(一)---String.StringBuffer.StringBuilder java基础解析系列(二)-- ...

  7. Java并发编程:ThreadLocal

    Java并发编程:深入剖析ThreadLocal   Java并发编程:深入剖析ThreadLocal 想必很多朋友对ThreadLocal并不陌生,今天我们就来一起探讨下ThreadLocal的使用 ...

  8. Java并发编程:深入剖析ThreadLocal(转载)

    Java并发编程:深入剖析ThreadLocal(转载) 原文链接:Java并发编程:深入剖析ThreadLocal 想必很多朋友对ThreadLocal并不陌生,今天我们就来一起探讨下ThreadL ...

  9. (转)Java并发编程:深入剖析ThreadLocal

    Java并发编程:深入剖析ThreadLoca Java并发编程:深入剖析ThreadLocal 说下自己的理解:使用ThreadLocal能够实现空间换时间,重在理解ThreadLocal是如何复制 ...

随机推荐

  1. Spring-JDBDTamplate 的操作

    基本的    增,删,改:(只演示增加 因为他们调用的方法都是update方法): package com.hxzy.spring_jdbc_template; import org.springfr ...

  2. 【bzoj 2716】[Violet 3]天使玩偶 (CDQ+树状数组)

    题目描述 Ayu 在七年前曾经收到过一个天使玩偶,当时她把它当作时间囊埋在了地下.而七年后 的今天,Ayu 却忘了她把天使玩偶埋在了哪里,所以她决定仅凭一点模糊的记忆来寻找它. 我们把 Ayu 生活的 ...

  3. AsyncTask的工作原理

    AsyncTask是Android本身提供的一种轻量级的异步任务类.它可以在线程池中执行后台任务,然后把执行的进度和最终的结果传递给主线程更新UI.实际上,AsyncTask内部是封装了Thread和 ...

  4. ElasticSearch NEST搜索

    var client = ElasticsearchHelper.GetElasticClient("order");QueryContainer termQuery = new ...

  5. [Swift]遍历集合类型(数组、集合和字典)

    Swift提供了三种主要的集合类型,称为数组,集合和字典,用于存储值集合. 数组是有序的值集合. 集是唯一值的无序集合. 字典是键值关联的无序集合. Swift中无法再使用传统形式的for循环. // ...

  6. multiprocess(上)

    仔细说来,multiprocess不是一个模块而是python中一个操作.管理进程的包. 之所以叫multi是取自multiple的多功能的意思,在这个包中几乎包含了和进程有关的所有子模块.由于提供的 ...

  7. 在Myeclipse中查看android源码就是这么easy

    在开发android 时不能查看源码必是很不爽的一件事,看过网上一些文章后(都是2.0以前的版本,跟我的2.2最新版本的配置是不一样的)不过还是给了我启示,通过配置终于可以在myeclipse中查看源 ...

  8. http协议缓存小结

    缓存可以使用expire方式,设置到期时间,缓存的时间等于expire设置的时间减去当前的时间 也可以使用no-cache的方式进行缓存,当设置了no-cache的方式时,以no-cache的为准,e ...

  9. docker微服务部署之:二、搭建文章微服务项目

    docker微服务部署之:一,搭建Eureka微服务项目 一.新增demo_article模块,并编写代码 右键demo_parent->new->Module->Maven,选择M ...

  10. CSS03--框模型、定位position、浮动

    我们接着“CSS02”,继续学习一些新的样式属性. 1.框模型:   规定了元素框处理  元素内容.内边距(padding).边框(border).外边距(margin,可以是负值)的方式 2.内边距 ...