1. 概述
- ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。
- ThreadLocal的作用:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。
- 线程并发:在多线程并发的场景下。
- 传递数据:可以通过ThreadLocal在同一线程,不同组件中传递公共变量。保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题。
- 线程隔离:每个线程的变量都是独立的,不会相互影响。各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。
- 在项目中需要进行数据传递和线程隔离的场景,可以用ThreadLocal来解决。
2. 案例分析1
package com.java.threadlocal;
/**
* @author rrqstart
* @Description 线程安全问题
*/
public class ThreadlLocalTest1 {
private String content;
private String getContent() {
return content;
}
private void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
ThreadlLocalTest1 t1 = new ThreadlLocalTest1();
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-0的数据为:Thread-1
Thread-4的数据为:Thread-4
Thread-3的数据为:Thread-3
Thread-1的数据为:Thread-1
Thread-2的数据为:Thread-2
2.1 ThreadLocal解决线程安全问题
package com.java.threadlocal;
/**
* @author rrqstart
* @Description 使用ThreadLocal解决线程安全问题
*/
public class ThreadLocalTest4 {
private static ThreadLocal<String> tl = new ThreadLocal<String>();
private String content;
private String getContent() {
return tl.get();
}
private void setContent(String content) {
tl.set(content);
}
public static void main(String[] args) {
ThreadLocalTest4 t4 = new ThreadLocalTest4();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
t4.setContent(Thread.currentThread().getName() + "");
System.out.println(Thread.currentThread().getName() + "的数据为:" + t4.getContent());
}).start();
}
}
}
2.2 synchronized解决线程安全问题
package com.java.threadlocal;
/**
* @author rrqstart
* @Description 加锁解决线程安全问题:synchronized
*/
public class ThreadLocalTest2 {
private String content;
private String getContent() {
return content;
}
private void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
ThreadLocalTest2 t2 = new ThreadLocalTest2();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (ThreadLocalTest2.class) {
t2.setContent(Thread.currentThread().getName() + "");
System.out.println(Thread.currentThread().getName() + "的数据为:" + t2.getContent());
}
}).start();
}
}
}
2.3 ReentrantLock解决线程安全问题
package com.java.threadlocal;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author rrqstart
* @Description 加锁解决线程安全问题:ReentrantLock
*/
public class ThreadLocalTest3 {
private String content;
private String getContent() {
return content;
}
private void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
ThreadLocalTest3 t3 = new ThreadLocalTest3();
ReentrantLock lock = new ReentrantLock();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
lock.lock();
try {
t3.setContent(Thread.currentThread().getName() + "");
System.out.println(Thread.currentThread().getName() + "的数据为:" + t3.getContent());
} finally {
lock.unlock();
}
}).start();
}
}
}
3. ThreadLocal与synchronized的区别
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用以时间换空间的方式,只提供了一份变量,让不同的线程排队访问。 | ThreadLocal采用以空间换时间的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而互不干扰。 |
侧重点 | 多个线程之间访问资源的同步性 | 多线程中让每个线程之间的数据相互隔离 |
4. ThreadLocal的内部结构
- JDK早期设计:每个ThreadLocal类都创建一个Map,然后用线程的threadID作为Map的key,要存储的局部变量作为Map的value,这样就能达到各个线程的局部变量隔离的效果。
- JDK8的设计:每个Thread维护一个ThreadLocalMap哈希表,这个哈希表的key是ThreadLocal实例本身,value才是真正要存储的值Object。
//JDK8中Thread类的部分源码
package java.lang;
public class Thread implements Runnable {
//ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class.
ThreadLocal.ThreadLocalMap threadLocals = null;
}
- 每个Thread线程内部都有一个Map (ThreadLocalMap)。
- Map里面存储ThreadLocal对象(key)和线程的变量副本(value)。
- Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向Map获取和设置线程的变量值。
- 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
- JDK8中ThreadLocal设计的好处:
- 这样设计之后每个Map存储的Entry数量就会变少,因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。
- 当Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。
5. ThreadLocal的源码分析
5.1 ThreadLocal的主要方法
方法声明 | 描述 |
---|---|
public ThreadLocal() | 创建ThreadLocal对象 |
protected T initialValue() | 返回当前线程局部变量的初始值 |
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) | 创建ThreadLocal对象并设置线程局部变量的初始值 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
package java.lang;
public class ThreadLocal<T> {
public ThreadLocal() {
}
/**
这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。
如果想要一个除null之外的初始值,可以重写此方法。
(备注:该方法是一个protected的方法,显然是为了让子类覆盖而设计的)
*/
//返回当前线程对应的ThreadLocal的初始值
protected T initialValue() {
return null;
}
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
ThreadLocalMap.Entry e = map.getEntry(this);
// 找到对应的存储实体 e
if (e != null) {
@SuppressWarnings("unchecked")
// 获取存储实体 e 对应的 value值
// 即为我们想要的当前线程对应此ThreadLocal的值
T result = (T)e.value;
return result;
}
}
// 如果map不存在,则证明此线程没有维护的ThreadLocalMap对象
// 调用setInitialValue进行初始化
return setInitialValue();
}
private T setInitialValue() {
// 调用initialValue获取初始化的值
T value = initialValue();
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null)
// 存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将此实体entry作为第一个值存放至ThreadLocalMap中
createMap(t, value);
// 返回设置的值value
return value;
}
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null)
// 存在则调用map.set设置此实体entry
map.set(this, value); //当前ThreadLocal的引用作为key
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将此实体entry作为第一个值存放至ThreadLocalMap中
createMap(t, value);
}
//删除当前线程中保存的ThreadLocal对应的实体entry
public void remove() {
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null)
// 存在则调用map.remove
// 以当前ThreadLocal为key删除对应的实体entry
m.remove(this);
}
//获取当前线程Thread对应维护的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //返回当前线程的成员变量threadLocals
}
//创建当前线程Thread对应维护的ThreadLocalMap
void createMap(Thread t, T firstValue) {
//这里的this是调用此方法的threadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
5.2 静态内部类ThreadLocalMap
- ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。
package java.lang;
public class ThreadLocal<T> {
//ThreadLocalMap使用的是延迟初始化,在第一次调用get()或者set()方法的时候才会进行初始化。
static class ThreadLocalMap {
/*
* 在ThreadLocalMap中,用Entry来保存K-V结构数据。
* 但是Entry中key只能是ThreadLocal对象,这点被Entry的构造器限定了。
* Entry继承WeakReference,使用弱引用,可以将ThreadLocal对象的生命周期和线程生命周期解绑,
* 持有对ThreadLocal的弱引用,可以使得ThreadLocal在没有其他强引用的时候被回收掉,
* 这样可以避免因为线程得不到销毁导致ThreadLocal对象无法被回收。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
//The value associated with this ThreadLocal.
Object value;
Entry(ThreadLocal<?> k, Object v) {
//super(k) = super(tl) = new WeakReference(tl,queue)
//new出来的弱引用指向tl
super(k);
value = v;
}
}
//The initial capacity -- MUST be a power of two.
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;
//The next size value at which to resize.
private int threshold; // Default to 0
//Set the resize threshold to maintain at worst a 2/3 load factor.
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
}
6. 案例分析2
package com.java.threadlocal;
import java.util.concurrent.TimeUnit;
public class ThreadLocalTest {
static ThreadLocal<Person> tl = new ThreadLocal<Person>();
static class Person {
String name = "Curry";
}
public static void main(String[] args) {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(tl.get()); //null
}).start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Person person = new Person();
//拿到当前线程独有的map,以tl为key,以person为value,将二者装进该map中
tl.set(person);
}).start();
}
}
- 无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏。要避免内存泄漏有两种方式:
- 使用完ThreadLocal,调用其remove方法删除对应的Entry。
- 使用完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中的任一方法的时候会被清除,从而避免内存泄漏。