ThreadLocal是什么
描述
官方描述
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable.
* 此类提供线程局部变量。
* 这些变量与正常变量不同,
* 因为每个访问一个线程(通过其{@code get}或{@code set}方法)的线程都有其自己的,独立初始化的变量副本
* /
官方已经解释的很明白
- 线程局部变量
- 使得每个线程都有一个独立的变量副本
作用
通过解释不难理解 ‘线程局部变量’ 能够做到以下功能
- 线程间数据隔离
- 同线程数据传递以及管理
ThreadLocal使用方式
public class ThreadLocalTest {
public static void main(String[] args) {
final ThreadLocal<String> local = new ThreadLocal<>();
System.out.println("开始"+local.get());
local.set("这是主线程");
Random random = new Random();
for (int i = 1; i < 5; i++) {
final int finalI = i;
new Thread(()->{
System.out.println("线程"+ finalI +"开始获取 local="+local.get());
local.set("这是线程"+ finalI);
try {
Thread.sleep(1000*random.nextInt(5));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程"+ finalI +"结束获取 local="+local.get());
}).start();
}
System.out.println("结束"+local.get());
}
}
结果:
开始null
结束这是主线程
线程1开始获取 local=null
线程2开始获取 local=null
线程4开始获取 local=null
线程3开始获取 local=null
线程3结束获取 local=这是线程3
线程4结束获取 local=这是线程4
线程2结束获取 local=这是线程2
线程1结束获取 local=这是线程1
通过使用也可看到local中的数据是对当前线程可见的
ThreadLocal实例
数据隔离
SimpleDateFormat 多线程数据错乱 问题解决
private static ThreadLocal<DateFormat> threadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM"));
跨层传递
pagehelper PageHelper
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
/**
* 设置 Page 参数
*
* @param page
*/
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
/**
* 开始分页
*
* @param params
*/
public static <E> Page<E> startPage(Object params) {
Page<E> page = PageObjectUtil.getPageFromObject(params, true);
//当已经执行过orderBy的时候
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
setLocalPage(page);
return page;
}
数据库管理
druid JdbcStatManager
public final ThreadLocal<JdbcStatContext> contextLocal = new ThreadLocal<JdbcStatContext>();
public JdbcStatContext getStatContext() {
return contextLocal.get();
}
会话管理
session、cookie管理
ServletRequestAttributes
//获取request
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes");
@Nullable
public static RequestAttributes getRequestAttributes() {
RequestAttributes attributes = requestAttributesHolder.get();
if (attributes == null) {
attributes = inheritableRequestAttributesHolder.get();
}
return attributes;
}
ThreadLocal源码解析
结构
先宏观的看一下结构图
先总的概述一下,每个线程里都有一个ThreadLocalMap用来存储数据,ThreadLocalMap其实里面是Entry数组。Entry 里面有key和value,key是ThreadLocal,并且key是个WeakReference弱引用。
下面我们一个一个查看源码
ThreadLocal
SuppliedThreadLocal
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
SuppliedThreadLocal是JDK8新增的内部类,只包含一个扩展了ThreadLocal的初始化值一个方法而已,很明显就是可以使用JDK8新增的Lambda表达式赋值。需要注意的是,函数式接口Supplier不允许为null。
//使用方式
ThreadLocal<DateFormat> threadLocal = ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM"));
ThreadLocalMap
static class Entry extends WeakReference<ThreadLocal<?>> {}
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
可以看出 ThreadLocalMap 中有一个 Entry的数组。说明他的存储最底层还是通过数组,可以注意到 数组的注释长度必须为2的n次方。这个和hashMap一样为了均匀分布避免冲突,同时为了快速计算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;
}
}
可以看到 Entry 继承了弱引用 ,并且把key设置为弱引用
大概用图来展示就是
实现方式
上面可以发现ThreadLocalMap 里面存储数据的只是一个一维数组,并不是像我们平时使用的Map是一个链表的形式。那它是怎么实现key、value查找和赋值呢。既然是Map必然会有冲突。那又是如何解决冲突呢
接下来我们一个一个方法查看实现。
ThreadLocal
//返回当前线程中当前ThreadLocal对应的副本
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程中的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//从map中每个Entry中获取到key为当前ThreadLocal的 Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//获取初始值
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//初始化并且返回设置的初始值 不重写则为null
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
//设置初始值
map.set(this, value);
else
//初始化当前线程中的ThreadLocalMap并且设置初始值
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//创建一个长度为16的Entry数组
table = new Entry[INITIAL_CAPACITY];
//通过key的哈希值计算找到放的位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
//设置大小
size = 1;
//设置负载系数 用来扩容
setThreshold(INITIAL_CAPACITY);
}
//负载因子为2/3
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
//没有static 说明每个对象持有自己的。 final说明只在创建时执行一次
private final int threadLocalHashCode = nextHashCode();
//每个ThreadLocal都共享这个值
private static AtomicInteger nextHashCode = new AtomicInteger();
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
//注释也写了,这个值为了让哈希码能均匀的分布在2的N次方的数组里
private static final int HASH_INCREMENT = 0x61c88647;
//每当新的ThreadLocal对象获取key的哈希时这个值都会累加 0x61c88647
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//可以想到目前冲突有两种
//1.不同的ThreadLocal当做 key放在同一个线程的ThreadLocalMap中计算位置导致冲突
//2.同一个ThreadLocal放在同一个线程的ThreadLocalMap中,设置value不一致 位置计算导致冲突
//设置nextHashCode 为static 多个ThreadLocal共享并且每次都累加就是可以降低第一种冲突的概率,第二种冲突其实就是覆盖值就好了
通过上面的get可以发现都是降低冲突让节点值分布均匀的方法,具体的解决冲突应该是在set方法中
//set也很简单执行的是ThreadLocalMap中的set
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap
//获取当前位置的一下个
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be 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);
//可以看出这里就是具体解决冲突的地方
//nextIndex就是解决冲突的办法--将往后推一个单元,直到该单元为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为空,移除并且替换为新值
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//获取到位null的单元直接设置
tab[i] = new Entry(key, value);
int sz = ++size;
//判断大小和负载系数 ,扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
可以看到ThreadLocalMap中已经解决了冲突
1.不同的ThreadLocal当做 key冲突----------获取数组后一个位置
2.同一个ThreadLocal设置value不一致冲突 -----覆盖老的值
//按照set的逻辑获取
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//判断 key是否一致,不一致取后一位直至key一致
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry就比较简单,有key和value。这里重点关注Entry继承了弱引用并且把key设置为弱引用。为什么要设置为弱引用后面会说到。
整个ThreadLocal实现的逻辑可以用一张图来总结一下
ThreadLocal内存泄漏问题
这个也是老生常谈的问题。先放一张ThrealLocal的引用结构图
What 内存泄漏是什么
首先你要知道什么是内存泄露 ,简单来说就是你不需要使用,但却没有被清理的东西越来越多。对于jvm来说就是无法把这部分用户没有引用到但是内部还是有强引用连着的导致无法回收堆内存。导致堆内存不堪重负
Why 为什么会泄露
很多人把泄露原因和弱引用混为一谈。先分析一下为什么会泄露。
从ThrealLocal的引用结构图中可以看到创建的ThreadLocal、ThreadLocalMap、Entry、value、Thread都在堆中。我们使用是通过方法中引用也就是栈中创建了一个地址引用。前面结构分析也可以了解整个数据是存在Thread中的ThreadLoaclMap中的, 也就是ThreadLocal整个是伴随着线程的生命周期。
那线程使用情况其实也就两种
- 单线程
一般来说我们单线程也就是主线程。在执行完之后。线程是会被清理掉的。所以当线程被清理之后其下的ThreadLocalMap也会被清理。自然里面的内容也会被清理。所以也不会发生没办法清理的问题。当然除非你的线程执行流程非常长。那就不是内存泄露应该是内存溢出。 - 线程池
当使用线程池时就不一样了。 因为线程池时用来管理线程。所以线程一直都存在,不会被关闭清理。上一次使用这个线程用的ThreadLocal数据会导致一直传递下去。因为这个线程在线程池里放着,栈的引用都被清空了,但是堆中 ThreadLoaclMap对value对key的引用是在的。没用栈中的引用你无法获取到他们。但是他们内部的强引用还在导致jvm无法对他们进行回收。所以导致了内存泄露。同时开发者也想到了这一点。但是value是用户所创建无法去干涉控制它的回收。所以把key设置为弱引用。这样key可以在下次GC的时候被回收掉。可以说就是能补救一点是一点。但实际使用我们都是吧ThreadLoca设置为类的静态变量。所以补救的效果不是很明显
所以不是因为弱引用导致内存溢出而是因为内存溢出从而设置弱引用从而缓解。泄漏的真正原因是ThreadLocal整个伴随着线程的生命周期
How 我该怎么办
其实我们通过源码分析也可以看出在每次set、get的时候都会进行null key的数据清理处理。最大程度的及时处理,但是也不可避免。列如在方法中ThreadLocal中放了大量数据。同时又有多个线程从线程池中划分出来执行该方法。后续线程需要执行的方法短时内没有再进行set、get,这样就会导致泄漏。所以我们应该在使用之后及时的remove。
public class ThreadLocalTest {
static final ThreadLocal<String> local = new ThreadLocal<>();
public static void main(String[] args) {
try {
local.set("使用完记得remove");
System.out.println("" + local.get());
}finally {
local.remove();
}
}
}
InheritableThreadLocal番外篇
谈了ThreadLocal线程隔离就不得不谈谈InheritableThreadLocal,名字也可以看出 ‘本地可继承线程’ 。子线程可以继承拿到父线程的ThreadLocalMap变量中的值,也就是共享父线程ThreadLocalMap数据
public class ThreadLocalTest {
static final ThreadLocal<String> local = new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
System.out.println("开始" + local.get());
local.set("这是主线程");
Random r =new Random();
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> {
System.out.println("子线程"+ finalI + "开始获取 local=" + local.get());
try {
Thread.sleep(1000* r.nextInt(5));
} catch (InterruptedException e) {
e.printStackTrace();
}
local.set("这是子线程"+ finalI);
System.out.println("子线程"+ finalI + "设置值获取 local=" + local.get());
}).start();
}
Thread.sleep(30000);
System.out.println("结束" + local.get());
}
}
/结果
开始null
子线程0开始获取 local=这是主线程
子线程2开始获取 local=这是主线程
子线程1开始获取 local=这是主线程
子线程3开始获取 local=这是主线程
子线程4开始获取 local=这是主线程
子线程4设置值获取 local=这是子线程4
子线程1设置值获取 local=这是子线程1
子线程2设置值获取 local=这是子线程2
子线程0设置值获取 local=这是子线程0
子线程3设置值获取 local=这是子线程3
结束这是主线程
InheritableThreadLocal继承的ThreadLocal,重写了getMap、createMap
/**
* Get the map associated with a ThreadLocal.
*
* @param t the current thread
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* Create the map associated with a ThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the table.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
可以看到他把数据放到线程的inheritableThreadLocals 中
再看看 线程初始化时做的事,就是Thread 的init方法中
会把父线程的inheritableThreadLocals 放到子线程的inheritableThreadLocals 中从而达到共享了父线程的ThreadLocalMap中的值。