APP下载
报价宝  ›  科技  › 

阿里架构师浅析ThreadLocal源代码——黄金分割数的使用

报价宝 来源:baojiabao.com 发布时间:2019-08-05 09:53:00 09月23日更新
报价宝综合消息阿里架构师浅析ThreadLocal源代码——黄金分割数的使用

一. 前提

最近接触到的一个专案要相容新老系统,最终采用了ThreadLocal(实际上用的是InheritableThreadLocal)用于在子执行绪获取父执行绪中共享的变数。问题是解决了,但是后来发现对ThreadLocal的理解不够深入,于是顺便把它的源代码阅读理解了一遍。在谈到ThreadLocal之前先买个关子,先谈谈黄金分割数。本文在阅读ThreadLocal源代码的时候是使用JDK8(1.8.0_181)。

二. 黄金分割数与斐波那契数列

首先复习一下斐波那契数列,下面的推导过程来自某搜索引擎的wiki:

斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …通项公式:假设F(n)为该数列的第n项(n ∈ N*),那么这句话可以写成如下形式:F(n) = F(n-1) + F(n-2)。有趣的是,这样一个完全是自然数的数列,通项公式却是用无理数来表达的。而且当n趋向于无穷大时,前一项与后一项的比值越来越逼近0.618(或者说后一项与前一项的比值小数部分越来越逼近0.618),而这个值0.618就被称为黄金分割数。证明过程如下:

黄金分割数的准确值为(根号5 - 1)/2,约等于0.618。

三. 黄金分割数的应用

黄金分割数被广泛使用在美术、摄影等艺术领域,因为它具有严格的比例性、艺术性、和谐性,蕴藏着丰富的美学价值,能够激发人的美感。当然,这些不是本文研究的方向,我们先尝试求出无符号整型和带符号整型的黄金分割数的具体值:

public static void main(String[] args) throws Exception {

//黄金分割数 * 2的32次方 = 2654435769 - 这个是无符号32位整数的黄金分割数对应的那个值

long c = (long) ((1L System.out.println(c);

//强制转换为带符号为的32位整型,值为-1640531527

int i = (int) c;

System.out.println(i);

}

通过一个线段图理解一下:

也就是2654435769为32位无符号整数的黄金分割值,而-1640531527就是32位带符号整数的黄金分割值。而ThreadLocal中的杂凑魔数正是1640531527(十六进位制为0x61c88647)。为什么要使用0x61c88647作为杂凑魔数?这里提前说一下ThreadLocal在ThreadLocalMap(ThreadLocal在ThreadLocalMap以Key的形式存在)中的杂凑求Key下标的规则:

杂凑算法:keyIndex = ((i + 1) * HASH_INCREMENT) & (length - 1)

其中,i为ThreadLocal例项的个数,这里的HASH_INCREMENT就是杂凑魔数0x61c88647,length为ThreadLocalMap中可容纳的Entry(K-V结构)的个数(或者称为容量)。在ThreadLocal中的内部类ThreadLocalMap的初始化容量为16,扩容后总是2的幂次方,因此我们可以写个Demo模拟整个杂凑的过程:

public class Main {

private static final int HASH_INCREMENT = 0x61c88647;

public static void main(String[] args) throws Exception {

hashCode(4);

hashCode(16);

hashCode(32);

}

private static void hashCode(int capacity) throws Exception {

int keyIndex;

for (int i = 0; i keyIndex = ((i + 1) * HASH_INCREMENT) & (capacity - 1);

System.out.print(keyIndex);

System.out.print(" ");

}

System.out.println();

}

}

上面的例子中,我们分别模拟了ThreadLocalMap容量为4,16,32的情况下,不触发扩容,并且分别”放入”4,16,32个元素到容器中,输出结果如下:

3 2 1 0

7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0

7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0

每组的元素经过杂凑算法后恰好填充满了整个容器,也就是实现了完美杂凑。实际上,这个并不是偶然,其实整个杂凑算法可以转换为多项式证明:证明(x - y) * HASH_INCREMENT != 2^n * (n m),在x != y,n != m,HASH_INCREMENT为奇数的情况下恒成立,具体证明可以自行完成。HASH_INCREMENT赋值为0x61c88647的API文件注释如下:

连续生成的杂凑码之间的差异(增量值),将隐式顺序执行绪本地id转换为几乎最佳分布的乘法杂凑值,这些不同的杂凑值最终生成一个2的幂次方的杂凑表。

四. ThreadLocal是什么

下面引用ThreadLocal的API注释:

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)

稍微翻译一下:ThreadLocal提供执行绪区域性变数。这些变数与正常的变数不同,因为每一个执行绪在访问ThreadLocal例项的时候(通过其get或set方法)都有自己的、独立初始化的变数副本。ThreadLocal例项通常是类中的私有静态字段,使用它的目的是希望将状态(例如,使用者ID或事务ID)与执行绪关联起来。

ThreadLocal由Java界的两个大师级的作者编写,Josh Bloch和Doug Lea。Josh Bloch是JDK5语言增强、Java集合(Collection)框架的创办人以及《Effective Java》系列的作者。Doug Lea是JUC(java.util.concurrent)包的作者,Java并发程式设计的泰斗。所以,ThreadLocal的源代码十分值得学习。

五. ThreadLocal的原理

ThreadLocal虽然叫执行绪本地(区域性)变数,但是实际上它并不存放任何的资讯,可以这样理解:它是执行绪(Thread)操作ThreadLocalMap中存放的变数的桥梁。它主要提供了初始化、set()、get()、remove()几个方法。这样说可能有点抽象,下面画个图说明一下线上程中使用ThreadLocal例项的set()和get()方法的简单流程图。

假设我们有如下的程式码,主执行绪的执行绪名字是main(也有可能不是main):

public class Main {

private static final ThreadLocal LOCAL = new ThreadLocal();

public static void main(String[] args) throws Exception{

LOCAL.set("doge");

System.out.println(LOCAL.get());

}

}

上面只描述了单执行绪的情况并且因为是主执行绪忽略了Thread t = new Thread()这一步,如果有多个执行绪会稍微复杂一些,但是原理是不变的,ThreadLocal例项总是通过Thread.currentThread()获取到当前操作执行绪例项,然后去操作执行绪例项中的ThreadLocalMap型别的成员变数,因此它是一个桥梁,本身不具备储存功能。

六. ThreadLocal源代码分析

对于ThreadLocal的源代码,我们需要重点关注set()、get()、remove()几个方法。

1. ThreadLocal的内部属性

//获取下一个ThreadLocal例项的杂凑魔数

private final int threadLocalHashCode = nextHashCode();

//原子计数器,主要到它被定义为静态

private static AtomicInteger nextHashCode = new AtomicInteger();

//杂凑魔数(增长数),也是带符号的32位整型值黄金分割值的取正

private static final int HASH_INCREMENT = 0x61c88647;

//生成下一个杂凑魔数

private static int nextHashCode() {

return nextHashCode.getAndAdd(HASH_INCREMENT);

}

这里需要注意一点,threadLocalHashCode是一个final的属性,而原子计数器变数nextHashCode和生成下一个杂凑魔数的方法nextHashCode()是静态变数和静态方法,静态变数只会初始化一次。换而言之,每新建一个ThreadLocal例项,它内部的threadLocalHashCode就会增加0x61c88647。举个例子:

//t1中的threadLocalHashCode变数为0x61c88647

ThreadLocal t1 = new ThreadLocal();

//t2中的threadLocalHashCode变数为0x61c88647 + 0x61c88647

ThreadLocal t2 = new ThreadLocal();

//t3中的threadLocalHashCode变数为0x61c88647 + 0x61c88647 + 0x61c88647

ThreadLocal t3 = new ThreadLocal();

threadLocalHashCode是下面的ThreadLocalMap结构中使用的杂凑算法的核心变数,对于每个ThreadLocal例项,它的threadLocalHashCode是唯一的。

2. 内部类ThreadLocalMap的基本结构和源代码分析

ThreadLocal内部类ThreadLocalMap使用了预设修饰符,也就是包(包私有)可访问的。ThreadLocalMap内部定义了一个静态类Entry。我们重点看下ThreadLocalMap的源代码,先看成员和结构部分:

/**

* ThreadLocalMap是一个定制的杂凑对映,仅适用于维护执行绪本地变数。

* 它的所有方法都是定义在ThreadLocal类之内。

* 它是包私有的,所以在Thread类中可以定义ThreadLocalMap作为变数。

* 为了处理非常大(指的是值)和长时间的用途,杂凑表的Key使用了弱引用(WeakReferences)。

* 引用的伫列(弱引用)不再被使用的时候,对应的过期的条目就能通过主动删除移出杂凑表。

*/

static class ThreadLocalMap {

//注意这里的Entry的Key为WeakReference>

static class Entry extends WeakReference> {

//这个是真正的存放的值

Object value;

// Entry的Key就是ThreadLocal例项本身,Value就是输入的值

Entry(ThreadLocal> k, Object v) {

super(k);

value = v;

}

}

//初始化容量,必须是2的幂次方

private static final int INITIAL_CAPACITY = 16;

//杂凑(Entry)表,必须时扩容,长度必须为2的幂次方

private Entry[] table;

//杂凑表中元素(Entry)的个数

private int size = 0;

//下一次需要扩容的阈值,预设值为0

private int threshold;

//设定下一次需要扩容的阈值,设定值为输入值len的三分之二

private void setThreshold(int len) {

threshold = len * 2 / 3;

}

// 以len为模增加i

private static int nextIndex(int i, int len) {

return ((i + 1 }

// 以len为模减少i

private static int prevIndex(int i, int len) {

return ((i - 1 >= 0) ? i - 1 : len - 1);

}

}

这里注意到十分重要的一点:ThreadLocalMap$Entry是WeakReference(弱引用),并且键值Key为ThreadLocal>例项本身,这里使用了无限定的泛型万用字元。

接着看ThreadLocalMap的建构函式:

// 构造ThreadLocal时候使用,对应ThreadLocal的例项方法void createMap(Thread t, T firstValue)

ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {

// 杂凑表预设容量为16

table = new Entry[INITIAL_CAPACITY];

// 计算第一个元素的杂凑码

int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

table[i] = new Entry(firstKey, firstValue);

size = 1;

setThreshold(INITIAL_CAPACITY);

}

// 构造InheritableThreadLocal时候使用,基于父执行绪的ThreadLocalMap里面的内容进行提取放入新的ThreadLocalMap的杂凑表中

// 对应ThreadLocal的静态方法static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap)

private ThreadLocalMap(ThreadLocalMap parentMap) {

Entry[] parentTable = parentMap.table;

int len = parentTable.length;

setThreshold(len);

table = new Entry[len];

// 基于父ThreadLocalMap的杂凑表进行拷贝

for (Entry e : parentTable) {

if (e != null) {

@SuppressWarnings("unchecked")

ThreadLocal key = (ThreadLocal) e.get();

if (key != null) {

Object value = key.childValue(e.value);

Entry c = new Entry(key, value);

int h = key.threadLocalHashCode & (len - 1);

while (table[h] != null)

h = nextIndex(h, len);

table[h] = c;

size++;

}

}

}

}

这里注意一下,ThreadLocal的set()方法呼叫的时候会懒初始化一个ThreadLocalMap并且放入第一个元素。而ThreadLocalMap的私有构造是提供给静态方法ThreadLocal#createInheritedMap()使用的。

接着看ThreadLocalMap提供给ThreadLocal使用的一些例项方法:

// 如果Key在杂凑表中找不到杂凑槽的时候会呼叫此方法

private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) {

Entry[] tab = table;

int len = tab.length;

// 这里会通过nextIndex尝试遍历整个杂凑表,如果找到匹配的Key则返回Entry

// 如果杂凑表中存在Key == null的情况,呼叫expungeStaleEntry进行清理

while (e != null) {

ThreadLocal> k = e.get();

if (k == key)

return e;

if (k == null)

expungeStaleEntry(i);

else

i = nextIndex(i, len);

e = tab[i];

}

return null;

}

// 1.清空staleSlot对应杂凑槽的Key和Value

// 2.对staleSlot到下一个空的杂凑槽之间的所有可能冲突的杂凑表部分槽进行重杂凑,置空Key为null的槽

// 3.注意返回值是staleSlot之后的下一个空的杂凑槽的杂凑码

private int expungeStaleEntry(int staleSlot) {

Entry[] tab = table;

int len = tab.length;

// expunge entry at staleSlot

// 清空staleSlot对应杂凑槽的Key和Value

tab[staleSlot].value = null;

tab[staleSlot] = null;

size--;

// Rehash until we encounter null

// 下面的过程是对staleSlot到下一个空的杂凑槽之间的所有可能冲突的杂凑表部分槽进行重杂凑,置空Key为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;

// Unlike Knuth 6.4 Algorithm R, we must scan until

// null because multiple entries could have been stale.

while (tab[h] != null)

h = nextIndex(h, len);

tab[h] = e;

}

}

}

return i;

}

// 这里个方法比较长,作用是替换杂凑码为staleSlot的杂凑槽中Entry的值

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).

int slotToExpunge = staleSlot;

// 这个循环主要是为了找到staleSlot之前的最前面的一个Key为null的杂凑槽的杂凑码

for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))

if (e.get() == null)

slotToExpunge = i;

// Find either the key or trailing null slot of run, whichever

// occurs first

// 遍历staleSlot之后的杂凑槽,如果Key匹配则用输入值替换

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) {

e.value = value;

tab[i] = tab[staleSlot];

tab[staleSlot] = e;

// Start expunge at preceding stale entry if it exists

if (slotToExpunge == staleSlot)

slotToExpunge = i;

cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

return;

}

// 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.

if (k == null && slotToExpunge == staleSlot)

slotToExpunge = i;

}

// Key匹配不了,则新建立一个杂凑槽

// If key not found, put new entry in stale slot

tab[staleSlot].value = null;

tab[staleSlot] = new Entry(key, value);

// 这里如果当前的staleSlot和找到前置的slotToExpunge不一致会进行一次清理

// If there are any other stale entries in run, expunge them

if (slotToExpunge != staleSlot)

cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

}

// 对当前杂凑表中所有的Key为null的Entry呼叫expungeStaleEntry

private void expungeStaleEntries() {

Entry[] tab = table;

int len = tab.length;

for (int j = 0; j Entry e = tab[j];

if (e != null && e.get() == null)

expungeStaleEntry(j);

}

}

// 清理第i个杂凑槽之后的n个杂凑槽,如果遍历的时候发现Entry的Key为null,则n会重置为杂凑表的长度,expungeStaleEntry有可能会重杂凑使得杂凑表长度发生变化

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);

}

} while ( (n >>>= 1) != 0);

return removed;

}

/**

* 这个方法主要给`ThreadLocal#get()`呼叫,通过当前ThreadLocal例项获取杂凑表中对应的Entry

*

*/

private Entry getEntry(ThreadLocal> key) {

// 计算Entry的杂凑值

int i = key.threadLocalHashCode & (table.length - 1);

Entry e = table[i];

if (e != null && e.get() == key)

return e;

else // 注意这里,如果e为null或者Key对不上,会呼叫getEntryAfterMiss

return getEntryAfterMiss(key, i, e);

}

// 重杂凑,必要时进行扩容

private void rehash() {

// 清理所有空的杂凑槽,并且进行重杂凑

expungeStaleEntries();

// Use lower threshold for doubling to avoid hysteresis

// 杂凑表的杂凑元素个数大于3/4阈值时候触发扩容

if (size >= threshold - threshold / 4)

resize();

}

// 扩容,简单的扩大2倍的容量

private void resize() {

Entry[] oldTab = table;

int oldLen = oldTab.length;

int newLen = oldLen * 2;

Entry[] newTab = new Entry[newLen];

int count = 0;

for (Entry e : oldTab) {

if (e != null) {

ThreadLocal> k = e.get();

if (k == null) {

e.value = null; // Help the GC

} else {

int h = k.threadLocalHashCode & (newLen - 1);

while (newTab[h] != null)

h = nextIndex(h, newLen);

newTab[h] = e;

count++;

}

}

}

setThreshold(newLen);

size = count;

table = newTab;

}

// 基于ThreadLocal作为key,对当前的杂凑表设定值,此方法由`ThreadLocal#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;

int i = key.threadLocalHashCode & (len-1);

// 变数杂凑表

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;

}

// 如果Entry的Key为null,则替换该Key为当前的key,并且设定值

if (k == null) {

replaceStaleEntry(key, value, i);

return;

}

}

tab[i] = new Entry(key, value);

int sz = ++size;

// 清理当前新设定元素的杂凑槽下标到sz段的杂凑槽,如果清理成功并且sz大于阈值则触发扩容

if (!cleanSomeSlots(i, sz) && sz >= threshold)

rehash();

}

简单来说,ThreadLocalMap是ThreadLocal真正的资料储存容器,实际上ThreadLocal资料操作的复杂部分的所有逻辑都在ThreadLocalMap中进行,而ThreadLocalMap例项是Thread的成员变数,在ThreadLocal#set()方法首次呼叫的时候设定到当前执行的执行绪例项中。如果在同一个执行绪中使用多个ThreadLocal例项,实际上,每个ThreadLocal例项对应的是ThreadLocalMap的杂凑表中的一个杂凑槽。举个例子,在主函式主执行绪中使用多个ThreadLocal例项:

public class ThreadLocalMain {

private static final ThreadLocal TL_1 = new ThreadLocal();

private static final ThreadLocal TL_2 = new ThreadLocal();

private static final ThreadLocal TL_3 = new ThreadLocal();

public static void main(String[] args) throws Exception {

TL_1.set(1);

TL_2.set("1");

TL_3.set(1L);

Field field = Thread.class.getDeclaredField("threadLocals");

field.setAccessible(true);

Object o = field.get(Thread.currentThread());

System.out.println(o);

}

}

实际上,主执行绪的threadLocals属性中的杂凑表中一般不止我们上面定义的三个ThreadLocal,因为载入主执行绪的时候还有可能在其他地方使用到ThreadLocal,笔者某次Debug的结果如下:

用PPT画图简化一下:

上图threadLocalHashCode属性一行的表是为了标出每个Entry的杂凑槽的杂凑值,实际上,threadLocalHashCode是[email protected]中的一个属性,这是很显然的,本来threadLocalHashCode就是ThreadLocal的一个成员变数。

上面只是简单粗略对ThreadLocalMap的源代码进行了流水账的分析,下文会作一些详细的图,说明一下ThreadLocal和ThreadLocalMap中的一些核心操作的过程。

3. ThreadLocal的建立

从ThreadLocal的建构函式来看,ThreadLocal例项的构造并不会做任何操作,只是为了得到一个ThreadLocal的泛型例项,后续可以把它作为ThreadLocalMap$Entry的键:

// 注意threadLocalHashCode在每个新`ThreadLocal`例项的构造同时已经确定了

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);

}

// 通过Supplier去覆盖initialValue方法

public static ThreadLocal withInitial(Supplier extends S> supplier) {

return new SuppliedThreadLocal(supplier);

}

// 预设公有建构函式

public ThreadLocal() {

}

注意threadLocalHashCode在每个新ThreadLocal例项的构造同时已经确定了,这个值也是Entry杂凑表的杂凑槽系结的杂凑值。

4. TreadLocal的set方法

ThreadLocal中set()方法的源代码如下:

public void set(T value) {

//设定值前总是获取当前执行绪例项

Thread t = Thread.currentThread();

//从当前执行绪例项中获取threadLocals属性

ThreadLocalMap map = getMap(t);

if (map != null)

//threadLocals属性不为null则覆盖key为当前的ThreadLocal例项,值为value

map.set(this, value);

else

//threadLocals属性为null,则建立ThreadLocalMap,第一个项的Key为当前的ThreadLocal例项,值为value

createMap(t, value);

}

// 这里看到获取ThreadLocalMap例项时候总是从执行绪例项的成员变数获取

ThreadLocalMap getMap(Thread t) {

return t.threadLocals;

}

// 建立ThreadLocalMap例项的时候,会把新例项赋值到执行绪例项的threadLocals成员

void createMap(Thread t, T firstValue) {

t.threadLocals = new ThreadLocalMap(this, firstValue);

}

上面的过程源代码很简单,设定值的时候总是先获取当前执行绪例项并且操作它的变数threadLocals。步骤是:

获取当前执行执行绪的例项。通过执行绪例项获取执行绪例项成员threadLocals(ThreadLocalMap),如果为null,则建立一个新的ThreadLocalMap例项赋值到threadLocals。通过threadLocals设定值value,如果原来的杂凑槽已经存在值,则进行覆盖。

5. TreadLocal的get方法

ThreadLocal中get()方法的源代码如下:

public T get() {

//获取当前执行绪的例项

Thread t = Thread.currentThread();

ThreadLocalMap map = getMap(t);

if (map != null) {

//根据当前的ThreadLocal例项获取ThreadLocalMap中的Entry,使用的是ThreadLocalMap的getEntry方法

ThreadLocalMap.Entry e = map.getEntry(this);

if (e != null) {

@SuppressWarnings("unchecked")

T result = (T) e.value;

return result;

}

}

//执行绪例项中的threadLocals为null,则呼叫initialValue方法,并且建立ThreadLocalMap赋值到threadLocals

return setInitialValue();

}

private T setInitialValue() {

// 呼叫initialValue方法获取值

T value = initialValue();

Thread t = Thread.currentThread();

ThreadLocalMap map = getMap(t);

// ThreadLocalMap如果未初始化则进行一次建立,已初始化则直接设定值

if (map != null)

map.set(this, value);

else

createMap(t, value);

return value;

}

protected T initialValue() {

return null;

}

initialValue()方法预设返回null,如果ThreadLocal例项没有使用过set()方法直接使用get()方法,那么ThreadLocalMap中的此ThreadLocal为Key的项会把值设定为initialValue()方法的返回值。如果想改变这个逻辑可以对initialValue()方法进行覆盖。

6. TreadLocal的remove方法

ThreadLocal中remove()方法的源代码如下:

public void remove() {

//获取Thread例项中的ThreadLocalMap

ThreadLocalMap m = getMap(Thread.currentThread());

if (m != null)

//根据当前ThreadLocal作为Key对ThreadLocalMap的元素进行移除

m.remove(this);

}

七. ThreadLocal.ThreadLocalMap的初始化

我们可以关注一下java.lang.Thread类里面的变数:

public class Thread implements Runnable {

//传递ThreadLocal中的ThreadLocalMap变数

ThreadLocal.ThreadLocalMap threadLocals = null;

//传递InheritableThreadLocal中的ThreadLocalMap变数

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

}

也就是,ThreadLocal需要存放和获取的资料实际上系结在Thread例项的成员变数threadLocals中,并且是ThreadLocal#set()方法呼叫的时候才进行懒载入的,可以结合上一节的内容理解一下,这里不展开。

八. 什么情况下ThreadLocal的使用会导致内存泄漏

其实ThreadLocal本身不存放任何的资料,而ThreadLocal中的资料实际上是存放线上程例项中,从实际来看是执行绪内存泄漏,底层来看是Thread物件中的成员变数threadLocals持有大量的K-V结构,并且执行绪一直处于活跃状态导致变数threadLocals无法释放被回收。threadLocals持有大量的K-V结构这一点的前提是要存在大量的ThreadLocal例项的定义,一般来说,一个应用不可能定义大量的ThreadLocal,所以一般的泄漏源是执行绪一直处于活跃状态导致变数threadLocals无法释放被回收。但是我们知道,·ThreadLocalMap·中的Entry结构的Key用到了弱引用(·WeakReference>·),当没有强引用来引用ThreadLocal例项的时候,JVM的GC会回收ThreadLocalMap中的这些Key,此时,ThreadLocalMap中会出现一些Key为null,但是Value不为null的Entry项,这些Entry项如果不主动清理,就会一直驻留在ThreadLocalMap中。也就是为什么ThreadLocal中get()、set()、remove()这些方法中都存在清理ThreadLocalMap例项key为null的程式码块。总结下来,内存泄漏可能出现的地方是:

大量地(静态)初始化ThreadLocal例项,初始化之后不再呼叫get()、set()、remove()方法。初始化了大量的ThreadLocal,这些ThreadLocal中存放了容量大的Value,并且使用了这些ThreadLocal例项的执行绪一直处于活跃的状态。ThreadLocal中一个设计亮点是ThreadLocalMap中的Entry结构的Key用到了弱引用。试想如果使用强引用,等于ThreadLocalMap中的所有资料都是与Thread的生命周期系结,这样很容易出现因为大量执行绪持续活跃导致的内存泄漏。使用了弱引用的话,JVM触发GC回收弱引用后,ThreadLocal在下一次呼叫get()、set()、remove()方法就可以删除那些ThreadLocalMap中Key为null的值,起到了惰性删除释放内存的作用。

其实ThreadLocal在设定内部类ThreadLocal.ThreadLocalMap中构建的Entry杂凑表已经考虑到内存泄漏的问题,所以ThreadLocal.ThreadLocalMap$Entry类设计为弱引用,类签名为static class Entry extends WeakReference>。之前一篇文章介绍过,如果弱引用关联的物件如果置为null,那么该弱引用会在下一次GC时候回收弱引用关联的物件。举个例子:

public class ThreadLocalMain {

private static ThreadLocal TL_1 = new ThreadLocal();

public static void main(String[] args) throws Exception {

TL_1.set(1);

TL_1 = null;

System.gc();

Thread.sleep(300);

}

}

这种情况下,TL_1这个ThreadLocal在主动GC之后,执行绪系结的ThreadLocal.ThreadLocalMap例项中的Entry杂凑表中原来的TL_1所在的杂凑槽Entry的引用持有值referent(继承自WeakReference)会变成null,但是Entry中的value是强引用,还存放着TL_1这个ThreadLocal未回收之前的值。这些被”孤立”的杂凑槽Entry就是前面说到的要惰性删除的杂凑槽。

九. ThreadLocal的最佳实践

其实ThreadLocal的最佳实践很简单:

每次使用完ThreadLocal例项,都呼叫它的remove()方法,清除Entry中的资料。呼叫remove()方法最佳时机是执行绪执行结束之前的finally程式码块中呼叫,这样能完全避免操作不当导致的内存泄漏,这种主动清理的方式比惰性删除有效。

十. 小结

ThreadLocal执行绪本地变数是执行绪例项传递和储存共享变数的桥梁,真正的共享变数还是存放线上程例项本身的属性中。ThreadLocal里面的基本逻辑并不复杂,但是一旦涉及到效能影响、内存回收(弱引用)和惰性删除等环节,其实它考虑到的东西还是相对全面而且有效的。

写在最后

第一:看完点赞,感谢您的认可;...第二:随手转发,分享知识,让更多人学习到;...第三:记得点关注,每天更新的!!!...
文章标签: 报价宝 降噪耳机价格 耳机价格 红米手机价格 华为手机价格 小米手机价格 电视机价格 笔记本电脑价格 笔记本价格 汽车价格 数码相机价格 汽车价格 笔记本电脑价格 电视机价格 小米手机价格