博文地址
我的GitHub | 我的博客 | 我的微信 | 我的邮箱 |
---|---|---|---|
baiqiantao | bqt20094 | [email protected] |
目录
- ThreadLocal
- 总结
- 使用线程池的问题
- 内存泄漏问题
- 总结
- 源码分析
- Thread
-
- 构造方法
- set() 方法
- get() 方法
- initialValue() 方法
- ThreadLocalMap
- 基础结构
- Hash 算法
- Hash 冲突
- InheritableThreadLocal
- 实现原理
- 测试代码
- 案例
ThreadLocal 为解决
多线程并发
问题提供了一种新的思路。使用它可以很简洁地编写出优美的多线程程序。当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的
变量副本
,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
使用场景
- 存储单个线程上下文信息
- 使变量线程安全:变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了
- 减少参数传递
ThreadLocal 提供了
线程独有的局部变量(本地变量、副本变量)
,可以在整个线程存活的过程中随时取用,且线程之间互不干扰。
- ThreadLocal 里 set 进去的数据,其实是存储在当前 Thread 里的(Thread 也是一个对象)
- 每个 Thread 都有一个属性
,类型为threadLocals
,它本质上是一个自定义的ThreadLocal.ThreadLocalMap
map
- 这个 map 的 entry 是
,这个 entry 继承自ThreadLocal.ThreadLocalMap.Entry
WeakReference<ThreadLocal<T>>
- 这表明:
是ThreadLocal<?>
,被弱引用对象引用的对象
(而非通过其 set 进去的数据)会在 gc 时被回收ThreadLocal<T>
- 这个 map 其实只是一个数组,map 的 get/set 方法的参数(也即象征意义的 key)是
ThreadLocal<?>
- 数组中存储的值(也即象征意义的 value)即通过
的 set 方法传过来的 Object(类型就是里面的泛型)ThreadLocal<?>
- 由于 map 中对
是通过弱引用的方式引用的,所以当ThreadLocal<?>
不再被强引用时,此ThreadLocal<?>
对象就会在 gc 时被回收ThreadLocal<?>
使用线程池可以达到线程复用的效果,但是归还线程之前记得清除
ThreadLocalMap
,要不然再取出该线程的时候,
ThreadLocal
变量还会存在。
为何存在内存泄漏的问题
-
中是以弱引用的方式引用的ThreadLocalMap
,如果一个ThreadLocal
没有外部强引用来引用它,那么 GC 的时候,这个ThreadLocal
就会被回收。ThreadLocal
-
被回收后,ThreadLocal
中就会出现ThreadLocalMap
为key
的null
,此后,这些Entry
key
null
Entry
就没有办法访问了。value
- 由于这些
key
null
Entry
存在一条强引用链:value
,这就造成这些Thread -> ThreaLocalMap -> Entry -> value
永远无法回收。value
- 如果当前线程迟迟不结束的话,就会造成内存泄漏。
如何解决内存泄漏的问题
在调用
ThreadLocal
get()、set()、remove()
的时候,都会主动清除掉
ThreadLocalMap
里所有
key
null
value
。
需要明确,使用
ThreadLocal
肯定是存在内存泄漏的问题的,上面的方案虽然解决了部分场景的内存泄漏问题,但并不彻底!
首先从
Thread
类源码入手:
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null; // 存储普通的线程数据(本地变量)
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; // 可以被子类继承
}
Thread
类中有一个
threadLocals
和一个
inheritableThreadLocals
变量,它们都是
ThreadLocalMap
类型的变量。
默认情况下这两个变量都是 null,只有当前线程调用
ThreadLocal
类的
set
或
get
方法时才创建它们,实际上调用这两个方法的时候,最终调用的是
ThreadLocalMap
类对应的方法。
ThreadLocalMap 是 ThreadLocal 的内部类,本质是一个自定义的 map。
public ThreadLocal()
protected T initialValue()
public void set(T value)
public T get()
public void remove()
private final int threadLocalHashCode = nextHashCode(); //用来在 map 中找到自己
public ThreadLocal() {}
ThreadLocal 实例的变量只有一个
threadLocalHashCode
,用来在 ThreadLocalMap 中找到自己存储的位置
public void set(T value) {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = t.threadLocals; // 返回当前线程中的成员变量 threadLocals
if (map != null) map.set(this, value); // 注意是将该 ThreadLocal 实例作为key
else t.threadLocals = new ThreadLocalMap(this, value); // 初始化线程中的成员变量,并赋值
}
set
方法很简单,主要是判断
ThreadLocalMap
是否存在,然后使用
ThreadLocalMap
中的
set
方法进行数据处理。具体逻辑在后面剖析
ThreadLocalMap
源码时再看。
通过上面的
set
方法及下面
get
方法可知,通过
ThreadLoca
set
方法存储的对象,都是存储在当前线程对象的
ThreadLocalMap
中的,其他线程访问不到,各个线程中通过
get
方法访问的是不同的对象。
public T get() {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = t.threadLocals; // 获取当前线程中的 ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // 获取 ThreadLocal 对应的键值对
if (e != null) return (T)e.value; // 如果键值对存在,则返回当前 ThreadLocal 对应的值
}
return setInitialValue(); // 如果键值对不存在,则返回 initialValue() 方法指定的初始值
}
If the variable has no value for the current thread, it is first initialized to the value returned by an invocation of the
initialValue()
method.
private T setInitialValue() {
T value = initialValue(); // 获取指定的初始值
set(value); // 中间这一块的逻辑和 set 方法完全一样
return value;
}
get
方法的逻辑也很简单,如果通过当前
ThreadLocal
能在
map
中找到非空的
Entry
,则正常返回
Entry
中的值(也即之前通过
set
方法存储的值),否则返回
initialValue()
方法指定的初始值。
核心逻辑依旧在
ThreadLocalMap
getEntry(tl)
方法中,具体逻辑同样在后面剖析
ThreadLocalMap
initialValue()
方法仅在
get
方法中被调用,用于返回
ThreadLocal
对应的初始化值,一般作为匿名内部类使用。
关于
initialValue()
的注意事项:
- Returns the current thread's "initial value" for this thread-local variable.
- This method will be invoked the first time a thread accesses the variable with the
method, unless the thread previously invoked theget
method, in which case the initialValue() method will not be invoked for the thread.set
- 注意这个方法可能不调用,也可能调用多次,不能在里面做初始化逻辑
一般没啥用的一个方法
ThreadLocalMap
类似
HashMap
的结构,只是
HashMap
是由数组+链表实现的,而
ThreadLocalMap
中并没有链表结构。
ThreadLocalMap is a customized hash map
suitable 适用于 only for maintaining 维护 thread local values.
static class ThreadLocalMap {
//The table, resized as necessary. table.length MUST always be a power of two.
private Entry[] table; // 可自动扩容的数组
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; // The value associated with this ThreadLocal
Entry(ThreadLocal<?> k, Object v) {
super(k); // key
value = v; // value
}
}
//...
}
- ThreadLocalMaps are constructed
, so we only create one when we have at least one entry tolazily
in it.put
- To help deal with very large and long-lived 长期存在 usages, the hash table entries use
forWeakReferences
.keys
- However, since
are not used, stale 过时的 entries are guaranteed 确保 to be removed only when the table starts running out of space 没有空间.ReferenceQueue
- Note that
mean that the key is no longer referenced, so the entry can be expunged 移除 from table.null keys
int i = key.threadLocalHashCode & (table.len-1); // 计算当前 key 在散列表中对应的数组下标位置
这里最关键的就是
threadLocalHashCode
值的计算,这个值其实在创建 ThreadLocal 时已经计算好了。
private final int threadLocalHashCode = nextHashCode(); // ThreadLocal 的 hash 值
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT); // Atomically adds the given value to the current value.
}
private static AtomicInteger nextHashCode = new AtomicInteger(); //The next hash code to be given out. Updated atomically. Starts at zero.
private static final int HASH_INCREMENT = 0x61c88647;
每当创建一个
ThreadLocal
对象,这个
ThreadLocal.nextHashCode
这个值就会在之前的基础上增长
0x61c88647
。这个值很特殊,它是斐波那契数也叫黄金分割数,hash 增量为这个数字的好处就是:hash 分布非常均匀。
HashMap
中解决冲突的方法是
链表法
,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成红黑树。
ThreadLocalMap
开放地址法
(
线性探测法
):插入一个
Entry
时,如果通过
hash
计算的槽位中已经有了
Entry
数据,此时就会线性向后查找,一直找到
Entry
null
的槽位才会停止查找,并将当前元素放入此槽位中
往
ThreadLocalMap
中
set
数据分为以下几种情况:
- 通过
计算后的槽位对应的hash
数据为空:直接将数据放到该槽位即可Entry
-
hash
数据不为空Entry
- 如果
值与当前key
ThreadLocal
计算获取的hash
值一致:更新该槽位的数据即可key
-
key
ThreadLocal
hash
值不一致:线性向后查找key
- 往后遍历过程中,在找到
Entry
的槽位之前,没有遇到null
过期的key
:遍历散列数组,线性往后查找,如果找到Entry
Entry
的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了null
值相等的数据,直接更新即可key
-
Entry
的槽位之前,遇到了null
key
:会进行一轮探测式清理操作,具体逻辑就不去理了,意义不大Entry
- 往后遍历过程中,在找到
- 如果
在
set()
方法其实做了很多事情,包括:添加数据、更新数据、清理数据、优化数据桶的位置、数组扩容,具体逻辑就不去理了,意义不大。
get
的逻辑和
set
类似,分为以下几种情况:
-
hash
和查找的Entry.key
一致:则直接返回key
-
hash
Entry.key
不一致:则往后迭代查找,查找过程中也会进行一轮探测式清理操作key
使用
ThreadLocal
的时候,在异步场景下是无法给
子线程
共享
父线程
中创建的线程副本数据的。使用
InheritableThreadLocal
便可以解决这个问题。
实现原理很简单:在父线程中通过
new Thread()
创建子线程时,
Thread#init
方法会在
Thread
的构造方法中被调用,在
init
方法中会拷贝父线程的
InheritableThreadLocal
中的数据到子线程中:
private void init(...boolean inheritThreadLocals) {
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
ThreadLocal<String> threadLocal = new ThreadLocal<>();
ThreadLocal<String> local = new InheritableThreadLocal<>();
threadLocal.set("白乾涛");
local.set("白乾涛");
ThreadLocal<String> threadLocal2 = ThreadLocal.withInitial(() -> "白乾涛2");
ThreadLocal<String> local2 = InheritableThreadLocal.withInitial(() -> "白乾涛2");
new Thread(() -> {
System.out.println("子线程获取父线程ThreadLocal数据:" + threadLocal.get() + "-" + threadLocal2.get()); // null-白乾涛2
System.out.println("子线程获取父线程InheritableThreadLocal数据:" + local.get() + "-" + local2.get()); // 白乾涛-白乾涛2
}).start();
public class Test {
ThreadLocal<String> mNameLocal = new ThreadLocal<>();
ThreadLocal<Long> mIdLocal = ThreadLocal.withInitial(() -> {
System.out.println(Thread.currentThread().getName() + " 调用了 get");
return -1L; //初始化数据
});
public static void main(String[] args) throws InterruptedException {
new Test().test();
}
private void test() throws InterruptedException {
mNameLocal.set(Thread.currentThread().getName()); //更新【主线程】数据
mIdLocal.set(Thread.currentThread().getId()); //更新【主线程】数据
Thread thread = new Thread(() -> {
mNameLocal.set(Thread.currentThread().getName()); //更新【子线程】数据
//mIdLocal.set(Thread.currentThread().getId()); //更新【子线程】数据
System.out.println(mNameLocal.get() + " " + mIdLocal.get()); //Thread-0 -1
});
thread.start();
thread.join(); //效果等同于同步:等 thread 执行完毕后再执行下面的逻辑
System.out.println(mNameLocal.get() + " " + mIdLocal.get()); //main 1
mNameLocal.remove();
mIdLocal.remove();
System.out.println(mNameLocal.get() + " " + mIdLocal.get()); //null -1
}
}
打印结果:
Thread-0 调用了 get
Thread-0 -1
main 1
main 调用了 get
null -1
2019-1-21
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/7257326.html