ThrealLocal原理讲解

时间:2023-01-19 07:13:14


ThrealLocal是面试中的一个重点,所以掌握好这部分知识点至关重要的。

你可能会用到的链接:
​ThreadLocal源码分析​​Java的强、软、弱、虚四种引用类型



文章目录

  • ​​1、ThreadLocal​​
  • ​​1.1、什么是ThreadlLocal​​
  • ​​1.2、多线程会出现的问题​​
  • ​​1.3、加锁​​
  • ​​1.4、使用ThreadLocal的方法​​
  • ​​1.5、ThreadLocal方案的好处​​
  • ​​2、ThreadLocal内部结构​​
  • ​​2.1、结构变化​​
  • ​​2.2、常用方法​​
  • ​​3、ThreadLocalMap​​
  • ​​3.1、内部细节​​
  • ​​3.2、核心方法​​
  • ​​4、弱引用和内存泄漏​​
  • ​​4.1、概念​​
  • ​​4.2、ThrealLocal中的内存泄漏​​
  • ​​5、扩容​​



1、ThreadLocal

1.1、什么是ThreadlLocal

我们查看其对应的源码,根据英文的翻译可得:

ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证 各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。

我们可以得知 ThreadLocal 的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。(在多个线程的基础之上,ThreadLocal 变量可以理解为每个线程的自己的变量,不同的线程都拥有属于自己的那一份)



1.2、多线程会出现的问题

测试代码:

package pers.mobian.ThreadLocal;

public class ThreadlLocalTest01 {
private String content;

private String getContent() {
return content;
}
private void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {

//实例化我们的对象,然后传入对应的值
ThreadlLocalTest01 t1 = new ThreadlLocalTest01();

for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
t1.setContent(Thread.currentThread().getName() + "");
System.out.println(Thread.currentThread().getName() + "的数据为:" + t1.getContent());
}
});
thread.start();
}
}
}

执行结果:

Thread-1的数据为:Thread-1
Thread-3的数据为:Thread-3
Thread-2的数据为:Thread-2
Thread-0的数据为:Thread-3
Thread-4的数据为:Thread-4

我们不难发现,我们传入对应的内容以后,取出来的值却不是对应的值。其实这一点不难理解,因为在多线程情况下,我们的线程互相的争夺资源,会出现拿到别人资源的情况。

怎么办呢?

第一反应,加锁,多线程情况下,我们总是一言不合就加锁。那么问题来了,加什么锁?



1.3、加锁

synchronized锁

synchronized (ThreadlLocalTest01.class) {
t1.setContent(Thread.currentThread().getName() + "");
System.out.println(Thread.currentThread().getName() + "的数据为:" + t1.getContent());
}

ReentrantLock锁

ReentrantLock lock = new ReentrantLock();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
t1.setContent(Thread.currentThread().getName() + "");
System.out.println(Thread.currentThread().getName() + "的数据为:" + t1.getContent());
lock.unlock();
}
});
thread.start();
}

以上就是两种加锁的方式,确实能够解决资源的冲突问题,但是随之而来的是性能的问题。

当我们使用加锁的方式的时候,总是让线程进行一种排队的方式修改并且获取资源,这是一种十分有效但是效率却十分低的修改方式(我们不难发现,直接加锁的方式是十分简单粗暴的,但是大家最终都会为了性能做出相应的妥协,当然synchronized关键字在JDK1.6的时候也进行了优化了,提高了性能,这就涉及到另一个锁升级的问题,这里就不展开了)。于是我们的ThreadLocal就来了。

我们使用ThreadLocal以后,就相当于我不限制你的访问,我只在每个线程内部做一个标记,不同的线程使用不同的标记,当我们的线程需要访问对应的资源的时候,我们就去根据当前的线程标记,找到与之匹配的标记,继而获取相同标记下的数据。使用这种方式,我们不仅能够解决资源冲突问题,还可以提高我们的并发度。



1.4、使用ThreadLocal的方法

修改后的代码:

public class ThreadlLocalTest02 {
private static String content;

private String getContent() {
//返回对应的ThreadLocal中的数据
return local.get();
}

private void setContent(String content) {
//我们在设置值的时候,将内容绑定到对应的ThreadLocal中
local.set(content);
this.content = content;
}

private static ThreadLocal<String> local = new ThreadLocal();

public static void main(String[] args) {

ThreadlLocalTest02 t2 = new ThreadlLocalTest02();

for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
t2.setContent(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName() + ":" + t2.getContent());
}
});
thread.start();
}
}
}

测试结果:

Thread-1:Thread-1
Thread-4:Thread-4
Thread-2:Thread-2
Thread-3:Thread-3
Thread-0:Thread-0

补充:我们在实际的测试过程中,很容易出现资源没有发生冲突的现象,我们可以使用将线程sleep的方式,来扩大多线程下资源的冲突问题。

ThreadLocal与synchronized的区别

synchronized

ThreadLocal

原理

同步机制采用’以时间换空间’的方式, 只提供了一份变量,让不同的线程排队访问

ThreadLocal采用’以空间换时间’的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰

侧重点

多个线程之间访问资源的同步性

多线程中让每个线程之间的数据相互隔离


1.5、ThreadLocal方案的好处

  1. 数据传递 : 保存每个线程绑定的数据,在需要的地方可以直接获取, 避免参数直接传递带来的代码耦合
  2. 线程隔离 : 各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失



2、ThreadLocal内部结构

2.1、结构变化

在JDK早期的设计:

在ThreadLocal内部维护一个map,然后将我们的线程设置为key,然后对应的参数设置为value,每一个线程去获取对应的value时,就去比照对应的key

JDK优化后的设计:

每一个Thread线程,单独维护一个ThreadLocalMap,这个对应Map的key为ThreadLocal实例本身,value为我们需要存储的值,是一个Object类型。

ThrealLocal原理讲解

优化后的好处:

  • 这样设计之后每个Map存储的Entry数量就会变少,因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。
  • 当Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。


2.2、常用方法

源码部分,建议配合使用:​​ThreadLocal源码分析​​

方法

作用

ThreadLocal()

创建ThreadLocal对象

public void set( T value)

设置当前线程绑定的局部变量

public T get()

获取当前线程绑定的局部变量

public void remove()

移除当前线程绑定的局部变量

protected T initialValue()

返回当前线程局部变量的初始值

get,set和remove逻辑是比较相似的,明白一个以后,就可以一通百通了。

get方法

//返回当前线程中保存ThreadLocal的值,
//如果当前线程没有此ThreadLocal变量(第一次添加value),则会通过initialValue进行初始化
public T get() {
Thread t = Thread.currentThread();
//获取对应的map
ThreadLocalMap map = getMap(t);
//判断是获取节点还是初始化一个节点
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}


//用于初始化值initialValue,并返回初始化后的值
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}


//获取当前线程Thread对应维护的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

//创建当前线程Thread对应维护的ThreadLocalMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

set方法

//设置当前线程对应的ThreadLocal的值(类似于初始化get下的setInitialValue方法)
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

remove方法

//删除当前线程中保存的ThreadLocal对应的实体entry
public void remove() {
hreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

initialValue方法

protected T initialValue() {
return null;
}

其实这个类的源码还是比较简单的。删除方法和初始化方法就不需要多说了。

set方法为,获取对应的ThreadLocalMap 实例,如果获取到了,就修改对应的value,没有获取到就创建一个ThreadLocalMap 实例

get方法为,首先获取对应的ThreadLocalMap 实例,没获取到就调用initialValue方法进行初始化(第一次进来),获取到了对应的实例,就返回对应节点下的信息。



3、ThreadLocalMap

ThreadLocalMap是ThreadLocal的内部类,代码占了整个类的一半多,可见其重要性

该类内部对应的类图:

ThrealLocal原理讲解

其内部包含了一个Entry节点,Entry节点内部又包含一个弱引用(Java的四种引用类型之一)

如果对Java的引用知识点不熟悉的小伙伴,可以去看我的另一篇博客:​​Java的强、软、弱、虚四种引用类型​​



3.1、内部细节

源码部分,建议配合使用:​​ThreadLocal源码分析​​

基本变量

private static final int INITIAL_CAPACITY = 16;

private Entry[] table;

private int size = 0;

private int threshold;

private void setThreshold(int len) { threshold = len * 2 / 3; }

Entry节点

static class Entry extends WeakReference<ThreadLocal> {
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}

构造方法

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//创建一个新的节点
table = new Entry[INITIAL_CAPACITY];
//计算对应的索引值
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//将我们的节点,放在对应的hash位置i上
table[i] = new Entry(firstKey, firstValue);
size = 1;
//设置阈值(默认的变量是16)
setThreshold(INITIAL_CAPACITY);
}

firstKey.threadLocalHashCode方法

//返回对应的hash值
private final int threadLocalHashCode = nextHashCode();

private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//AtomicInteger,java在JUC包下的原子类,用于解决volatile无法满足的原子性
private static AtomicInteger nextHashCode = new AtomicInteger();

//Java开发者选择的一个解决hash冲突,计算出的一个比较好的数字
private static final int HASH_INCREMENT = 0x61c88647;



3.2、核心方法

我们既然已经知道了ThreadLocalMap内部维护的是一个map,那么我们是否可以想到就会出现hash冲突?那我们的ThreadLocalMap是如何解决这个冲突的呢?我们一起来看它的set方法

ThreadLocalMap内部的set方法

private void set(ThreadLocal<?> key, Object value) {
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();

if (k == key) {
e.value = value;
return;
}

if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

ThreadLocalMap内部的set方法对应的索引方法

//获取环形数组的下一个索引
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}

//获取环形数组的上一个索引
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}

ThreadLocalMap使用开发地址-线性探测法来解决哈希冲突,线性探测法的地址增量di = 1, 2, … 其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

那么此时我们就可以将我们的table看成一个环形table



4、弱引用和内存泄漏

4.1、概念

内存溢出和内存泄漏:

  • Memory Overflow:内存溢出,没有足够的内存提供申请者使用
  • Memory leak:内存泄漏是指程序中已动态分配的堆内存由于某些原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果内存泄漏的堆积将导致内存溢出

注意不要混淆了这两个概念

弱引用和强引用

Java中的弱引用有4种类型:强、软、弱、虚。我们这里只涉及其中的两个。想要补课的小伙伴:​​Java的强、软、弱、虚四种引用类型​​

  • 强引用(Strong Reference)即我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还活着,垃圾回收器就不会回收这个对象
  • 弱引用(Weak Reference)即垃圾回收器一旦发现了只要具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存


4.2、ThrealLocal中的内存泄漏

根据我们的类图,我们可以将我们的整个调用调用过程:

ThrealLocal原理讲解

我们每启动一个线程,就会出现CurrentThread Ref --> CurrentThread --> Map --> Entey

我们每使用一次ThreadLocal就会出现ThreadLocal Ref --> ThreadLocal,key–> ThreadLocal

有人说,我们使用ThrealLocal出现的内存泄漏和弱引用有关。我们来分析一下,是这样吗?甚至想一想为什么ThrealLocal不直接使用强引用,非要使用弱引用?

使用强引用

ThrealLocal原理讲解

分析

  1. 假设在业务代码中ThreadLocal使用完毕,ThreadLocal Ref被回收了
  2. 但是因为threadLocalMap的Entry强引用了threadLocal,造成ThreadLocal无法被回收
  3. 在没有手动删除Entry以及CurrentThread依然运行的前提下,始终有强引用链threadRef --> currentThread --> Entry,Entry就不会被回收( Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏
  4. 即ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的

弱引用

ThrealLocal原理讲解

分析

  1. 假设在业务代码中使用完ThreadLocal,ThreadLocal Ref被回收了
  2. 由于threadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例,所以threadlocal就可以顺利被gc回收,此时Entry中的key = null
  3. 在没有手动删除Entry以及CurrentThread依然运行的前提下,也存在有强引用链threadRef --> currentThread --> value,value就不会被回收,而这块value永远不会被访问到了,导致value内存泄漏
  4. 也即ThreadLocalMap中的key使用了弱引用,也有可能内存泄漏。

结论:无论ThreadLocalMap中的key是使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系

要避免内存泄漏有两种方式:

  1. 使用完ThreadLocal,调用其remove方法删除对应的Entry
  2. 使用完ThreadLocal,当前Thread也随之运行结束

相对于第一种方式,第二种方法显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的,而是返回到线程池

也就是说,只要记得在使用完ThreadLocal后,及时的调用remove方法,无论key是弱引用还是强引用都不会有问题,那么为什么key还要使用弱引用呢?

事实上,在ThreadLocalMap中的set/getEntry方法中,会对key设置为null(也就是ThreadLocal为null)进行判断,如果为null的话,那么是会将value置为null的

这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用多了一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set、get、remove中的任一方法的时候会被消除,从而避免内存泄漏

补充我们的getEntry方法和remove方法:

private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
//e=null时
return getEntryAfterMiss(key, i, e);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//不会进入while循环
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];
}
//将节点设置为null
return null;
}
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}

5、扩容

对于扩容部分,请去源码分析中查看,这里就不再赘述。

​​ThreadLocal源码分析​​