ThreadLocal详解(java8)
本文参考黑马视频进行笔记整理
1.ThreadLoca基本介绍
1.1基本介绍
ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程上下文。不同线程间不会相互干扰,这种变量在线程的生命周期内起作用,可以减少一个线程内部的函数或组件之间变量传递的复杂度。
特点:线程并发;数据传递;线程隔离
1.2基本使用
1.2.1常用方法
方法 | 描述 |
---|---|
public ThreadLocal() | 创建一个线程局部变量 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的而局部变量 |
public void remove(T value) | 移除当前线程绑定的局部变量值 |
1.2.2基本使用
/**
* ThreadLocal简单测试类
*/
public class TestThreadLocalSimple {
// ThreadLocal 当前线程局部变量
private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
public static void main(String[] args) {
new TestThreadLocalSimple().test();
}
public void test() {
// 创建10个线程,threadLocal内存储对应线程的名称
for (int i = 0; i < 10; i++) {
new Thread(() -> {
String name = Thread.currentThread().getName();
// threadLocal内存储当前线程名称
threadLocal.set(Thread.currentThread().getName());
// threadLocal内取出之前存入的变量
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
}).start();
}
}
}
1.3ThreadLocal与synchronized关键字
两者都用于处理多线程 并发访问数据的问题。两者处理问题的角度和侧重点有多不同。
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制,采用以时间换空间的方式(以时间换同步),只提供一份变量,使不同线程排队访问 | 采用以空间换时间的方式,为每一个线程提供变量副本,实现同时访问而互相不干扰 |
侧重点 | 多线程访问资源的数据同步 | 多线程间的数据隔离 |
2.ThreadLocal工作原理
2.1基本原理
JDK8中ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal本身,value才是真正存储的值对象。原理图如下
-
每个Thread线程内部都有一个Map(ThreadLocalMap)
-
Map里面存储ThreadLocal对象(key)和线程变量副本value
-
Thread内部的ThreadLocalMap是ThreadLocal维护的,由ThreadLocal负责像这个map存取值
-
对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成数据隔离
3.核心源码分析
3.1核心方法
方法 | 描述 |
---|---|
protected T initialValue | 返回当前thread-local的初始值 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 返回当前线程的局部变量值 |
public void remove() | 移除当前线程的局部变量值 |
3.2ThreadLocalMap核心源码分析
3.2.1基本结构
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,个性化实现了Map功能,其内部的Entry也是独立实现。
3.2.2成员变量
/**
* The initial capacity -- MUST be a power of two.
* 初始容量 --必须是2的整数次幂
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
* 存放数据的table(Entry数组),长度lenght必须是2的证书次幂
*/
private Entry[] table;
/**
* The number of entries in the table.
* table中元素(entry)的个数
*/
private int size = 0;
/**
* The next size value at which to resize.
* 扩容阈值,默认为len * (2/3)
*/
private int threshold; // Default to 0
基本结构与HashMap结构类似,可类比学习。
3.2.3存储结构Entry
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
* Entry继承WeakReference,使用threadlocak作为key,若key为null则意味着key不在被引用,entry 可被清除
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap中,也是用Entry来保存K-V结构数据。Entry中的key必须是ThreadLocal对象,构造方法强制限定。Entry继承WeakReference,key(threadlocal)是弱引用,目的是讲ThreadLocal对象的生命周期和线程生命周期绑定。值得注意的是多引用在空间不足事JVM minorGC清除最新引存入对象,Full GC时会清除所有的弱引用。
3.2.4hash冲突
再来看一下ThreadLocal的set方法与createMap方法
ThreadLocal#set
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的threadlocal
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// threadlocal实例为null,就创建threadlocal
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocal#ThreadLocalMap(this, firstValue)
// firstKey:当前ThreadLocal实例
// firstValue:要保存的数据
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化table(INITIAL_CAPACITY:16)
table = new Entry[INITIAL_CAPACITY];
// 计算索引(重要)。
// hasdCode & (len - 1)相当于取模运算,比hasdCode % (len - 1)效率更高。
// 前提len必须是2的整数次幂
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 在table数组内存入数据
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 设置阈值
setThreshold(INITIAL_CAPACITY);
}
计算索引:Key.threadLocalHashCode
private final int threadLocalHashCode = nextHashCode();
// 特殊的hash值和斐波那契数列(黄金分割数)有关,目的是让hash码均匀分布在2的n次方-1范围内
private static final int HASH_INCREMENT = 0x61c88647;
// 原子类型Integer适应高并发
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
ThreadLocalMap#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);
// 使用线性探测法插入元素(核心)
// 由计算的索引确定下表位置,若不为null,则下表后移继续探测直到找到一个合适的位置进行元素插入
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,说明之前的threadlocal已经被回收了。
if (k == null) {
// 则替换陈旧的entry,清理部分垃圾,防止内存泄露
replaceStaleEntry(key, value, i);
return;
}
}
// key不存在且没又陈旧entry,则新建一个entry
tab[i] = new Entry(key, value);
int sz = ++size;
//没有清除任何entry,size也达到2/3 len,则进行rehash扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
4.内存泄漏分析
4.1名词解释
4.1.1内存泄漏与内存溢出
- 内存溢出:没有足够的内存供申请者使用
- 内存泄漏:程序中动态分配的内存由于某种原因无法正确释放,造成系统内存浪费,导致程序运行速度变慢或统崩溃。内存泄露的堆积终将导致内存溢出。
4.1.2引用
java中引用4种类型:强、软、弱、需。当前问题主要涉及强引用与弱引用。
- 强引用:最常见的普通对象引用,只要还有强引用指向某对象,就表明次对象尚在存活,GC就不会收集这样的对象
- 弱引用:GC一旦发现只具有弱引用的对象,不管当前内存空间是否足够,都会回收这种对象的内存
4.2内存泄漏
4.2.1内存泄漏原因
ThreadLocal内存泄露的根源:ThreadLocal的生命周期与Thread生命周期一样长,如果没有手动删除对应的entry就会导致内存泄露。
优化方案:使用完ThreadLocal,调用remove方法删除对应entry
事实上ThreadLocalMap中的set/getEntry方法会对key为null进行判断,如果key为null会将value也置为null。这意味着使用完ThreadLocal,CurrentThread依然存在的前提下,即使未进行过remove操作,弱引用比强引用多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次set/get/remove中的任一方法都会被清除。
4.2.2强引用产生的内存泄漏
当业务代码中Thread使用完ThreadLocal,threadLocalRef被回收。因ThreadLocalMap持有threadLocal Strong Ref,造成threadLocal无法被回收。但在没有手动删除Entry及CurrentThread依然运行的前提下,又存在强引用链threadRef->currentThread->threadLocalMap->entry。entry不会被回收了,产生内存泄漏。
4.2.3弱引用产生的内存泄漏
当业务代码中Thread使用完ThreadLocal,threadLocalRef被回收,因ThreadLocalMap仅仅持有threadLocal Weak Ref,没有任何引用指向threadlocal实例所以threadlocal可被gc回收,此时Entry中的key=null。但在没有手动删除Entry及CurrentThread依然运行的前提下,又存在强引用链threadRef->currentThread->threadLocalMap->entry->value。因entry的key是null,这块value永远不会被回收了,产生内存泄漏。