什么是ThreadLocal
ThreadLocal提供线程局部变量,每个线程都有自己独立初始化的变量副本,这些变量副本只与其所在的线程独享,不同的线程之间的变量副本互不干扰。ThreadLocal实例通常是希望将变量状态与当前线程相关联(例如,用户ID或事务ID)。
例如,下面的类生成每个线程本地的唯一标识符。 线程的ID在第一次调用ThreadId.get()时被分配,并在后续调用中保持不变。
ThreadLocal用法
- 用法一: 在ThreadLocal第一次get的时候使用initialValue()把对象初始化出来,
对象的初始化时机完全由自己控制,不受外部因素的影响
。
package threadlocal;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author pyh
* @date 2021/4/22 23:42
*/
public class ThreadId01 implements Runnable {
private static final AtomicInteger nextId = new AtomicInteger(0);
// threadId变量初始化一个和当前线程相关联的变量副本
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// 只要是在同一个线程内,通过threadId 就能获取到这个线程的同一个副本
public static int get() {
return threadId.get();
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 1000; i++) {
executorService.execute(new ThreadId01());
}
executorService.shutdown();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":" + ThreadId01.get());
}
}
部分结果如下:
由此得知,不同线程之间的结果不一样,相同线程拿到的都是同一份副本。
- 用法二:ThreadLocal对象由外部调用set方法设置进去,
对象的生成时机由外部因素控制
。
package threadlocal;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author pyh
* @date 2021/4/23 00:29
*/
public class ThreadId02 implements Runnable {
private static final AtomicInteger nextId = new AtomicInteger(0);
// threadId变量初始化一个和当前线程相关联的变量副本
private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() ;
// 由外部调用该方法之后,ThreadLocal才会有对象
public static void set() {
threadId.set(nextId.getAndIncrement());
}
// 只要是在同一个线程内,通过threadId 就能获取到这个线程的同一个副本
public static int get() {
// 只有每个线程第一次进来的时候才会调用,因为第一次进来get获取的数据是空的
if (threadId.get() == null) {
set();
}
return threadId.get();
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 1000; i++) {
executorService.execute(new ThreadId02());
}
executorService.shutdown();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":" + ThreadId02.get());
}
}
执行结果如下:
结论和用法一一样。
ThreadLocal的使用场景
- 每个线程需要一个独享的对象,通常是非线程安全工具类,如:SimpleDateFormat和Random。
package threadlocal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author pyh
* @date 2021/4/24 22:05
*/
public class ThreadLocalNormalUsage03 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
// dateFormat为非线程安全的共享资源
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
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 ThreadLocalNormalUsage03().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);
return dateFormat.format(date);
}
}
SimpleDateFormat为共享资源,在多线程并发资源竞争的情况下的情况下,即使每次调用format格式日期的参数不一样,由于其并非线程安全类,也会出现格式化日期出现相同的结果。结果如下:
对于此问题,可以使用ThreadLocal来保证每个线程都拥有一份SimpleDateFormat副本,保证了线程安全,当然也可以在date方法加锁保证其线程安全,但是加锁会相对ThreadLocal更加消耗性能。
package threadlocal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author pyh
* @date 2021/4/24 22:19
*/
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() {
// 利用i作为SimpleDateFormat格式化日期参数,每次调用格式化的时间都不同
String date = new ThreadLocalNormalUsage05().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 08:00:00 GMT计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
/**
* ThreadLocal初始化用法1
*/
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
/**
* ThreadLocal初始化用法2
*/
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
即使多次反复执行都不会出现格式化日期相同的结果,部分结果截图如下:
- 每个线程内需要保存全局变量,例如在拦截器中获取用户信息,可以让不同方法直接获取,避免参数传递的麻烦。
package threadlocal;
import org.omg.PortableInterceptor.Interceptor;
/**
* 描述: 演示ThreadLocal用法2:避免传递参数的麻烦
* @author pyh
* @date 2021/4/24 22:28
*/
public class ThreadLocalNormalUsage06 {
public static void main(String[] args) {
new InterceptorService().process();
}
}
/**
* 模拟用户登录后,拦截器将用户信息保存到ThreadLocal
*/
class InterceptorService {
public void process() {
User user = new User("超哥");
UserContextHolder.holder.set(user);
new OrderService().process();
}
}
/**
* 模拟用户下单需要到当前登录人信息
*/
class OrderService {
public void process() {
User user = UserContextHolder.holder.get();
ThreadSafeFormatter.dateFormatThreadLocal.get();
System.out.println("OrderService拿到用户名:" + user.name);
new PaymentService().process();
}
}
/**
* 模拟下单后调用付款服务时需要到当前登录人信息
*/
class PaymentService {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("PaymentService拿到用户名:" + user.name);
UserContextHolder.holder.remove();
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
由于User对象在大多数服务中经常要用到的对象信息,如果把他作为形参传递,那大多数方法形参都需要添加User对象形参。虽然把User定义为全局变量也能解决形参问题,但是需求是不同登录用户(即不同线程)拿到的对象应该是不一样的,相同用户(相同线程)拿到的对象要求一样的,全局变量就无法解决这个问题了。结果如下:
ThreadLocal作用
- 让需要用到的对象在
线程间隔离
。 - 在任何方法中都可以轻松获取到对象。
- 不需要加锁,也能达到线程安全,提高了执行效率。
- 相比于每个线程都新创建一个对象,ThreadLocal可以更高效地利用内存,节省开销。
- 免去传参的麻烦,使得代码更优雅,耦合度更低。
ThreadLocal底层原理
- Thread、ThreadLocal和ThreadLocalMap的关系如下:
每个线程(Thread)都有一个ThreadLocalMap成员属性,该属性本质是一个Map,key是ThreadLocal,value是调用ThreadLocal.initialValue()或ThreadLocal.set()设置的值。一个ThreadLocalMap可以存储过个ThreadLocal。
class Thread{
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
- ThreadLocal.get()
class ThreadLocal{
/**
* get获取initialValue()或set()设置进去的值
*
* @return the current thread's value of this thread-local
*/
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
/*
获取当前线程的ThreadLocalMap,key是ThreadLocal,
value是initialValue()返回的值
*/
ThreadLocalMap map = getMap(t);
if (map != null) {
// 获取当前ThreadLocal对象使用set()方法设置进去的值
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果e为null,表示没有调用当前ThreadLocal对象的set方法,则尝试从重写的initialValue()获取值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 从重写的initialValue()获取值
return setInitialValue();
}
}
- ThreadLocal.initialValue()
class ThreadLocal{
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
// 获取子类重写方法返回的值,默认返回null
T value = initialValue();
// 获取当前线程
Thread t = Thread.currentThread();
/*
获取当前线程的ThreadLocalMap,key是ThreadLocal,
value是initialValue()返回的值
*/
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else {
// ThreadLocalMap具有延迟加载的效果,第一次使用的时候才创建
createMap(t, value);
}
return value;
}
}
- ThreadLocal.set()
class ThreadLocal{
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
/*
获取当前线程的ThreadLocalMap,key是ThreadLocal,
value是initialValue()返回的值
*/
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else {
// ThreadLocalMap具有延迟加载的效果,第一次使用的时候才创建
createMap(t, value);
}
}
}
通过源码发现,setInitialValue()方法和set方法逻辑类似,只不过setInitialValue()方法设置到ThreadLocalMap的值是initialValue()方法的返回值,而set()方法设置进去的是则是通过形参设置进去的。
- Thread.remove()
class ThreadLocal{
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
// 获取当前线程的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
// 移除当前ThreadLocal存储的值,再次调用get()方法的时候获取该值就为null,如果没有调用set()方法之前就调用get()方法,则还会触发调用initialValue()方法
m.remove(this);
}
}
}
- 总结:
- 当线程第一次调用get()方法访问变量时,将调用initialValue(),除非线程先前调用了set()方法。通常每个线程至多只会调用一次initialValue()方法,但如果已经调用了remove()方法,再调用get()方法,可以再次触发调用initialValue()方法。
- ThreadLocalMap是最终存储数据的存储结构,我们每次使用ThreadLocal的时候总会先new一个ThreadLocal对象,本质就是new一个ThreadLocalMap的键,然后通过该键获取相应的值。
ThreadLocal注意事项
- 内存泄漏
即某个对象不再使用,但是占用的内存却不能回收。
- 导致内存泄漏的原因:
class ThreadLocal{
static class ThreadLocalMap {
// 继承了弱引用父类
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
// key为弱引用
super(k);
// value为强引用
value = v;
}
}
}
由前文可知,最终存储数据的是ThreadLocalMap,key是ThreadLocal,value是使用设置进去的值,而key最终保存在Entry的父类WeakReference(super(k)),所以key是以弱引用的方式存在,而value则是保存在Entry的成员属性中(value = v),所以value是以强引用的方式存在。
弱引用的特点是,如果这个对象只被弱引用关联(没有任何强关联),那么这个对象就可以被GC回收。
正常情况下,当线程终止,保存在ThreadLocalMap.Entry的value就会被GC回收,因为没有任何强引用了。
线程池设置初衷之一就是为了避免频繁创建销毁线程,如果使用了线程池,线程始终不终止,虽然ThreadLocalMap.Entry的key为弱引用可以被回收,但是ThreadLocalMap.Entry的value为强引用就不会被回收,如果不能被回收的对象存在很多,那么久可能引发OOM异常。
其实JDK设计者在ThreadLocalMap.resize()方法中也对相应问题做了处理,即如果弱引用key被回收了,则将value置为null,则value在相应的堆内存就可以被回收了。resize()是私有方法,该方法法作用是扩容,触发resize()方法调用的只有通过ThreadLocalMap.set(),并且并不是每次调用set()都会触发resize()方法,所以这并不不能作为解决内存泄漏的办法。
- 解决内存泄漏办法
阿里规约中解决ThreadLocal内存泄漏的办法就是,在使用完ThreadLocal里的对象后,手动调用ThreadLocal.remove()方法。
- 空指针异常
public class ThreadLocalNPE {
ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>();
public void set() {
longThreadLocal.set(Thread.currentThread().getId());
}
public Long get() {
return longThreadLocal.get();
}
public static void main(String[] args) {
ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(threadLocalNPE.get());
}
});
thread1.start();
}
}
正常情况,调用get()之前如果没有重写initialValue()方法或者调用set()方法,会返回一个null,不至于报空指针异常。但是如果将以上代码的get()改写成如下:
public long get() {
return longThreadLocal.get();
}
则会报空指针异常,这是因为基本类型在自动拆箱的时候所导致的。
- 共享对象
如果每个线程中ThreadLocal.set()进去的对象本来就是一个共享对象,比如static对象,那么多线程情况下调用ThreadLocal.get()得到的还是一个这个共享对象本身,还是由并发访问问题。