ThreadLocal简介
ThreadLocal的作用, 可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题。同时,也实现了线程内的资源共享。
ThreadLocal的特点:
- ThreadLocal 可以为当前线程关联一个数据(可以是普通变量,可以是对象,也可以是数组、集合)。(它可以像Map一样存取数据,key为当前线程)
- 每一个ThreadLocal对象,只能为当前线程关联一个数据,如果要为当前线程关联多个数据,就需要使用多个ThreadLocal对象实例。
- 每个ThreadLocal对象实例定义的时候,一般都是private static修饰
- ThreadLocal中保存数据。在线程销毁后,会由VM虚拟自动释放。
线程数据存储三大神器对比
特性 | ThreadLocal 线程本地 | synchronized 同步 | Lock 锁 |
---|---|---|---|
数据可见性 | 线程私有 | 共享区同步访问 | 共享区可控访问 |
锁机制 | 无锁操作 | 悲观锁 | 显式锁控制 |
性能损耗 | 接近零损耗 | 高竞争下性能衰减 | 中等控制开销 |
适用场景 | 线程上下文传递 | 临界区资源保护 | 复杂锁逻辑实现 |
ThreadLocal核心机制图解
部分源码
public class ThreadLocal<T> {
--begin 跨线程共享
//黄金分割数哈希
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
new AtomicInteger();
--end 跨线程共享
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();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
--begin 线程私有
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
.....
以空间换时间:为每个使用该变量的线程提供一个独立的的副本,所以每一个线程都可以改变自己的变量副本,而不会影响其他线程所对应的变量副本。
原理:每一个Thread对象均含有一个 ThreadLocalMap类型的成员变量threadLocals,它存储本线程中所有ThreadLocal对象及其对应的值。ThreadLocalMap由一个个Entry 对象构成,Entry继承自WeakReference<ThreadLocal<?>>,一个Entry由ThreadLocal对象和object构成。由此可见,Entry 的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该key就会被垃圾收集器回收。
1.当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。 再以当前ThreadLocal对象为key,资源对象作为value,放入当前线程的ThreadLocalMap对象中。
2.get方法执行过程类似。ThreadLocal首先会获取当前线程对象, 然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。
3.调用remove方法,就是以ThreadLocal自己作为key,移除当前线程关联的资源值。
由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。
注意:
- 用来存取数据:set()/get()
- 使用ThreadLocal存储的数据,线程安全
- 用完记得调用remove方法释放
多维度应用及常见解析
常见使用场景
使用场景
1.在进行对象跨层传递的时候,使用ThreadLoca可以避免多次传递,打破层次间的约束。
2.线程间数据隔离
3.进行事务操作,用于存储线程事务信息。
4.数据库连接,Session会话管理。
Spring框架在事务开始时会给当前线程绑定一个Jdbcconnection,在整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离。
线程间数据隔离,每个线程需要独享对象
这种通常是工具类,如SimpleDateFormat和Random
举例:SimpleDateFormat。(当多个线程共用这样一个SimpleDateFormat,但是这个类是不安全的)
- 2个线程分别用自己的SimpleDateFormat,这没问题;
- 后来延伸出10个,那就有10个线程和10个SimpleDateFormat,这虽然写法不优雅,但勉强可以接受
- 但是当需求变成了1000,那么必然要用线程池,消耗内存太多;
- 但是每一个SimpleDateFormat我们都需要创建一遍,那么太耗费new对象了,改成static共用的,所有线程都共用一个simpleDateFormat对象,但这是线程不安全的,容易出现时间一致的情况,在调用的时候,可加锁来解决,但还是不优雅;
- 用ThreadLocal来解决该问题,给每个线程分配一个simpledateformat,可这个threadlocal是安全的;
package threadlocal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 描述:利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
*/
public class ThreadLocalNormalUsage05 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
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 ThreadLocalNormalUsage05().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
// SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
跨层传递用户信息
在进行对象跨层传递的时候,使用ThreadLoca可以避免多次传递参数,打破层次间的约束。
如:拦截器中获取登陆用户id并传递
1、定义UserContext类
ublic class UserContext {
private static final ThreadLocal<Long> tl = new ThreadLocal<>();
/**
* 保存当前登录用户信息到ThreadLocal
* @param userId 用户id
*/
public static void setUser(Long userId) {
tl.set(userId);
}
/**
* 获取当前登录用户信息
* @return 用户id
*/
public static Long getUser() {
return tl.get();
}
/**
* 移除当前登录用户信息
*/
public static void removeUser(){
tl.remove();
}
}
慎用场景
第一点(线程池里线程调用ThreadLocal):因为线程池里对线程的管理都是线程复用的方法,所以在线程池里线程非常难结束,更有可能的是永远不会结束。这就意味着线程的持续时间是不可估测的,甚至会与JVM的生命周期一致。
第二点(在异步程序里):ThreadLocal的参数传递是不可靠的,因为线程将请求发送后,不会在等待远程返回结果就继续向下运行了,真正的返回结果得到以后,可能是其它的线程在处理。
第三点:在使用完ThreadLocal,推荐要调用一下remove()方法,这样会防止内存溢出这种情况的发生,因为ThreadLocal为弱引用。如果ThreadLocal在没有被外部强引用的情况下,在垃圾回收的时候是会被清理掉的,如果是强引用那就不会被清理。
ThreadLocal内存泄露原因,如何避免?
前置知识回顾:
内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
强引用:使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中, 用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。
ThreadLocalMap 为什么使用弱引用作为key?
若key使用强引用(x)
当ThreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
若key使用弱引用(true)
当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有 ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。 当key为null,在下一次ThreadLocalMap调用set()、get()、remove()方法的时候会被清除value值。
因此,Threadlocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,Thread可能需要长时间运行(如线程池中的线程),如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal正确的使用方法
- 每次使用完ThreadLocal都调用它的remove()方法清除数据
- 将ThreadLocal变量定义成private static,这样就一直存 在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。