一、ThreadLocal是什么
ThreadLocal 用于解决多线程环境下的线程安全问题。ThreadLocal为每个线程访问的变量提供了一个独立的副本,线程在访问这个变量时,访问的都是自己的副本数据,从而线程安全,即ThreadLocal为变量提供了线程隔离。
参考:https://juejin.cn/post/7336822616386846754
https://blog.csdn.net/qq_52173163/article/details/125529524
二、ThreadLocal使用
2.1 使用场景
1. 线程间数据隔离
ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。
2. 上下文传递
ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。
2.2 如何使用
ThreadLocal() 构造函数,创建ThreadLocal对象
get():T 获取当前线程绑定的局部变量
set(value:T) 设置当前线程绑定的局部变量
remove() 移除当前线程绑定的局部变量
2.3 使用demo
1. 线程数据隔离demo
下面代码使用了 ThreadLocal 帮每个线程去生成它自己的 simpleDateFormat 对象,对于每个线程而言,这个对象是独享的。但与此同时,这个对象就不会创造过多,一共只有 16 个,因为线程只有 16 个
代码语言:java复制 public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i ) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalDemo1().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
static class ThreadSafeFormatter {
static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("mm:ss");
}
};
}
2. 上下文传递demo
每个线程内需要保存类似于全局变量的信息(例如订单信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的订单信息不一样)。
例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。
在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 order 对象)作为参数传递的麻烦。
代码语言:java复制public class CashierOrderContextHolder {
public static ThreadLocal<Order> holder = new ThreadLocal<>();
}
代码语言:java复制@Data
public class Order {
/**
* 订单Id
*/
private String orderId;
/**
* 用户id
*/
private String userId;
}
代码语言:java复制public class Service1 {
public void method1() {
Order order = new Order();
order.setOrderId(orderId);
order.setUserId(userId);
CashierOrderContextHolder.holder.set(order);
}
}
代码语言:java复制public class Service2 {
public void method2() {
Order order = CashierOrderContextHolder.holder.get();
System.out.println("Service2拿到订单Id:" order.getOrderId());
}
}
三、ThreadLocal和Sychronized
类 | 原理 | 特点 |
---|---|---|
ThreadLocal | ThreadLocal 采用以 空间换时间 的方式,为每个线程都提供一份变量副本,避免了资源的竞争 | 数据互相隔离,可避免传参,不适合数据共享场景 |
synchronized | 同步机制采用以 时间换空间 的方式,只有一份变量,让不同的线程排队访问,主要用于临界资源的分配 | 多线程访问资源同步,效率低一些,花费内存少 |
四、Thread、ThreadLocal和ThreadLocalMap关系
用一张图展示ThreadLocal、ThreadLocalMap、Thread的关系
4.1 Thread类源码分析
Thread类里面有一个ThreadLocalMap类型的变量
代码语言:java复制public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
4.2 ThreadLoca源码分析
ThreadLocalMap
ThreadLocal类,可以看到里面有一个ThreadLocalMap静态内部类,也就是上面Thread里面threadLocals变量将要指向的对象。
从 ThreadLocal 的常用函数可以发现,Thread 并没有在初始化的时候创建 ThreadLocalMap 对象,而是在 ThreadLocal 调用 set()/get() 时,通过 createMap(Thread, T) 创建;
为什么要这样设计呢?
ThreadLocal通过给ThreadLocalMap使用默认的权限修饰符,使得ThreadLocalMap无法被其他包的类引用,最终将ThreadLocalMap完美地隐藏起来,同时ThreadLocal提供了一系列操作容器ThreadLocalMap的方法(get、set等),供外界使用。
代码语言:java复制public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
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();
}
}
}
}
ThreadLocal.set方法
ThreadLocal set流程本质上就是设置当前线程对应的ThreadLocal的值:
1、 获取当前线程 Thread;
2、获取当前线程对应的 ThreadLocalMap;
3、赋值:
threadLocals 非空: 将value 存储到 ThreadLocalMap中(调用了ThreadLocalMap.set方法),key 为 ThreadLocal;
threadLocals 为空,先调用createMap创建 ThreadLocalMap 对象,并赋初始值 value;
代码语言:java复制 public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//这里调用的是ThreadLocalMap.set
map.set(this, value);
} else {
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocal.get方法
先获取当前线程对象,然后拿到当前线程对象的ThreadLocalMap,在根据键(即当前对象ThreadLocal)找到值
如果我们在获取的时候当前线程对象的ThreadLocalMap为null时,则会执行setInitialValue()方法。
做了三件事:
1.创建map
2.给map设置一个键值对{threadLocal : initialValue}
3.返回initialValue,默认null
通过上面的源码我们可以知道,ThreadLocalMap以ThreadLocal作为键,值是我们调用ThreadLocal的set方法传进来的。
代码语言:java复制public T get() {
Thread t = Thread.currentThread();
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();
}
代码语言:java复制 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);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
4.3 总结
Thread类里面有一个ThreadLocalMap类型的变量,但是外界无法直接操作这个ThreadLocalMap,提供了一个工具箱ThreadLocal帮助我们操作ThreadLocal
1、 ThreadLocal的作用有两个
1)工具类,提供一系列方法操作ThreadLocalMap,比如get/set/remove
2)隔离Thread和ThreadLocalMap,防止程序员直接创建ThreadLocalMap。
自身的get/set内部会判断当前线程是否已经绑定一个ThreadLocalMap,有就继续用,没有就为其绑定。
2、虽然ThreadLocalMap是ThreadLocal的静态内部类,但它们的实例对象并不存在继承或者包裹关系。完全可以当成两个独立的实例;
3、一个Thread只能有一个ThreadLocalMap;
4、ThreadLocalMap以ThreadLocal为键存储数据。
五、ThreadLocal内存泄漏问题
5.1 什么是内存泄漏
内存泄漏指的是,当某一个对象不再有用的时候,占用的内存却不能被回收,这就叫作内存泄漏。
通常情况下,如果一个对象不再有用,那么我们的垃圾回收器 GC,就应该把这部分内存给清理掉。这样的话,就可以让这部分内存后续重新分配到其他的地方去使用;否则,如果对象没有用,但一直不能被回收,这样的垃圾对象如果积累的越来越多,则会导致我们可用的内存越来越少,最后发生内存不够用的 OOM 错误。
ThreadLocal为啥会内存泄漏
从引用关系图可以看到 ThreadLocalMap 作为 Thread 的属性,其生命周期是跟 Thread 一样长,假设 ThreadLocal 被回收,而线程还未结束,那么 ThreadLocalMap 中对应的 Entry.key 会被置为 null,此时这个 entry.value 在线程生命周期内不会再次被访问,如果线程是复用的,那么该 ThreadLocalMap 内部就会存在一个或多个 entry(null, value) 对象,从而导致内存泄漏;
怎么防范
调用 ThreadLocal 的 remove 方法。调用这个方法就可以删除对应的 value 对象,可以避免内存泄漏
代码语言:java复制public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}