一、ThreadLocal 简介
1. ThreadLocal 是什么?
ThreadLocal 字面意思是本地线程,其实更准确来说是线程局部变量,线程类 Thread 有个变量叫做 threadLocals,其类型就是ThreadLocal.ThreadLocalMap 类型,他其实不是一个 Map 类型,但可以暂时理解它是一个Map,键为 ThreadLocal 对象,值就是要存入的value。
2. ThreadLocal 作用
ThreadLocal
就是用于线程间的数据隔离的。ThreadLocal 提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程,获取保存的值时非常方便,ThreadLocal 为变量在每个线程中都创建了一个副本
,每个线程就可以很方便的访问自己内部的副本变量。
3. ThreadLocal的两大使用场景
- 每个线程需要一个独享的对象(通常是指工具类对象),每个线程内有自己的实例副本,不与其他线程共享;
- 每个线程内需要一个变量作为全局共用(当前线程内全局共用),可以让不同的方法直接使用,避免传递参数的麻烦;
总之,就是解决多个线程的共享变量的线程安全问题;
二、使用场景案例
1. 每个线程需要一个独享的对象
下面的案例是关于 SimpleDateFormat 工具类,在多线程共享时的线程安全问题
线程不安全的代码:
public class MyThreadLocal {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static String date(int seconds) {
Date date = new Date(1000 * seconds);
return sdf.format(date);
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
int finalI = i;
executorService.execute(new Runnable() {
@Override
public void run() {
String date = date(finalI);
System.out.println(date);
}
});
}
executorService.shutdown();
}
}
这里为了提高性能,所以将 SimpleDateFormat 作为 static 属性,多线程共享,但是这样就会出现安全问题,打印结果如下:
由结果可以看出,打印出了两个相同的时间,说明发生了运行结果错误,问题代码就发生在sdf.format(date),这行代码不是线程安全的。
解决方案有两个:使用同步锁 synchronized 和使用 ThreadLocal 解决。
(1)同步锁 synchronized
public class MyThreadLocal {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static String date(int seconds) {
Date date = new Date(1000 * seconds);
String s = null;
synchronized (MyThreadLocal.class) {
s = sdf.format(date);
}
// Date date = new Date(1000 * seconds);
return s;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
int finalI = i;
executorService.execute(new Runnable() {
@Override
public void run() {
String date = date(finalI);
System.out.println(date);
}
});
}
executorService.shutdown();
}
}
(2) 用ThreadLocal解决
public class MyThreadLocal1 {
// private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
public static String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat sdf = simpleDateFormatThreadLocal.get();
return sdf.format(date);
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
int finalI = i;
executorService.execute(new Runnable() {
@Override
public void run() {
String date = date(finalI);
System.out.println(date);
}
});
}
executorService.shutdown();
}
}
打印结果:
两个方案都可以解决线程安全问题,但是synchronized加锁的方法,由于同一时刻只有一个线程执行,所以效率低下;ThreadLocal方法在多线程并行的情况下,由于每个线程内都有自己独享的对象,也不会有线程安全问题。
2. 每个线程内需要一个变量作为全局共用
在应用开发中,有些参数需要被线程内许多方法使用,如权限管理,很多的方法都需要验证当前线程用户的身份信息
案例内容:一个系统中,user对象需要在很多server中进行使用
-
方案1
将user作为参数层层传递,从service1->service2->service3以此类推。这样会导致代码冗余且难以维护 -
方案2
定义一个全局的static 的user,想要拿的时候直接获取。但这是一种错误的方案!!因为我们现在的场景是多用户的系统,每个线程对应着不同的用户,每个线程的user是不同的 -
方案3
定义一个UserMap,每次访问从Map中获取用户的信息,多线程访问下加锁或者使用ConcurrentHashMap,但是对性能有影响 -
方案4
利用ThreadLocal,不需要锁,不影响性能。ThreadLocal 主打的就是同一个线程内不同方法间的共享。
所以优选选择方案4,代码演示如下:
/**
* 避免传递参数的麻烦
* ThreadLocalan案例2
* @author Chkl
* @create 2020/3/10
* @since 1.0.0
*/
public class ThreadLocalNormalUsage06 {
public static void main(String[] args) {
new Service1().process();
}
}
class Service1 {
public void process() {
User user = new User("周星驰");
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("service2:" + user.name);
UserContextHolder.holder.remove();
UserContextHolder.holder.set(new User("古天乐"));
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("service3:" + user.name);
}
}
class UserContextHolder {
public static ThreadLocal<User> holder
= new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
打印结果:
通过以上两个案例,我们可以了解到 ThreadLocal 的两个作用:
- 让对象在线程之间隔离;
- 在任何方法中都可以直接获取到对象;
3. ThreadLocal 的两个使用方式
上面两个使用场景中,ThreadLocal 的初始化方式也是分为两种:
- 场景1:initialValue
如果在ThreadLocal第一次get的时候把对象给初始化时使用,对象的初始化时机受控制 - 场景2:set
如果需要保存到ThreadLocal的对象的生成时机不由我们随意控制,我们用set方法放进去,再用get方法取出来;
4. ThreadLocal的好处
-
线程安全
-
不需要加锁,执行效率高
-
更高效的利用内存,节省开销
相比于每个任务都新建一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销
-
避免传参的繁琐操作
无论是场景一的工具类,还是场景二的用户名,都可以在任务地方直接通过ThreadLocal拿到,再也不需要在方法的形参中再定义传入相同的参数。ThreadLocal使代码耦合度更低,更优雅。
三、ThreadLocal 原理
1. ThreadLocal 与 Thread 的关系
- 每一个Thread里面都有一个ThreadLocalMap类型的threadlocals成员变量,它可以存储很多的ThreadLocal对象,因为一个线程可能有多个ThreadLocal对象,其中对象引用名称作为key;
- ThreadLocalMap:也就是Thread.threadLocals,是Thread里的一个成员变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对;键:这个ThreadLocal;值:实际需要的成员变量;
2. ThreadLocal 源码
(1) initialValue() 方法源码
/**
* Returns the current thread's "initial value" for this
* thread-local variable. This method will be invoked the first
* time a thread accesses the variable with the {@link #get}
* method, unless the thread previously invoked the {@link #set}
* method, in which case the {@code initialValue} method will not
* be invoked for the thread. Normally, this method is invoked at
* most once per thread, but it may be invoked again in case of
* subsequent invocations of {@link #remove} followed by {@link #get}.
*
* <p>This implementation simply returns {@code null}; if the
* programmer desires thread-local variables to have an initial
* value other than {@code null}, {@code ThreadLocal} must be
* subclassed, and this method overridden. Typically, an
* anonymous inner class will be used.
*
* @return the initial value for this thread-local
*/
protected T initialValue() {
return null;
}
- 该方法返回当前线程对应的初始值,使用了延迟加载,当调用get()方法是才会触发
- 当第一次使用get()方法时会调用此方法,如果调用前用set()方法设置了值就不会调用
- 当调用remove()方法后再次调用get()方法依然会调用initialize
- 如果不重写initialValue方法,直接调用get()会返回null
(2) get 方法的实现:
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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();
}
方法里面第一行获取当前线程
,然后通过 getMap(t) 方法获取 ThreadLocal.ThreadLocalMap,所有的变量数据都存在该 map,map 的具体类型是一个 Entry 数组。
然后接着下面获取到 Entry 键值对,注意这里获取 Entry 时参数传进去的是 this,即 ThreadLocal 实例
,而不是当前线程 t。如果获取成功,则返回 value 值。
如果 map 为空,则调用 setInitialValue 方法返回一个初始 value,其实这个默认初始 value 为 null。
(3) 接着来看一下 getMap 方法做了什么:
/* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
在 getMap 中,是调用当期线程 t,返回当前线程t中的一个成员变量 threadLocals
,类型为 ThreadLocal.ThreadLocalMap。就是上面提到的每一个线程都自带一个 ThreadLocalMap 类型的成员变量。
(4) 继续来看 ThreadLocalMap 的实现:
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap 是 ThreadLocal 的一个静态内部类,其内部主要是一个 Entry 数组存储数据(并不是一个 map 类型)。
ThreadLocalMap 的 Entry 继承了 WeakReference,用来实现弱引用,被弱引用关联的对象(其实就是 ThreadLocal 对象)只能生存到下一次垃圾收集发生之前,并且使用 ThreadLocal 对象的 HashCode 的散列值计算得出的 Entry 数组的下标 i,这里不同对象可能存在相同的下标 i
,对此 set() 方法处理逻辑是:下标加一,直到第一个要插入的位置为空。
(5) set()方法
/**
* 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 map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
为这个线程设置一个新值
(6) remove()方法
/**
* 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 m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
删除线程中对应的值,remove()方法也是在ThreadlocalMap中进行操作,传入当前ThreadLocal对象的引用,删除map中的value的值,不是删除整个ThreadLocalMap对象,而是根据this(也就是当前ThreadLocal对象)来删除对应的threadLocal对象
四、ThreadLocal 注意点
1. 在使用 ThreadLocal时的注意事项
- 最后一次使用之后应该手动的调用remove()方法,防止内存泄露
- 如果可以不使用ThreadLocal就解决问题,不要强行使用(如:任务数很少时)
- 优先使用框架的支持,而不是自己创造,例如在Spring中,如果可以使用 RequestContextHolder,那么就不需要自己去维护ThreadLocal,因为自己可能会忘记调用remove方法,造成内存泄漏;
2 ThreadLocal 为什么会发生内存泄露?
内存泄漏:某个对象不再有用,但是占用的内存不能被回收;
源码:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
我们知道,ThreadLocal 是基于 ThreadLocalMap 实现的,这个 Map 的 Entry 继承了 WeakReference,而 Entry 对象中的 key 使用了 WeakReference 封装,也就是说 Entry 中的 key 是一个弱引用类型,而弱引用类型只能存活在下次 GC 之前。
如果一个线程调用 ThreadLocal 的 set 设置变量,当前 ThreadLocalMap 则新增一条记录,但发生一次垃圾回收,此时 key 值被回收,而 value 值依然存在内存中,如果线程一直存在(比如在线程池中),那么 value 值将一直被引用,不能被回收。因为存在一条引用链的关系:Thread–>ThreadLocalMap–>Entry–>Value。造成内存泄漏,甚至有可能造成内存溢出OOM
如何避免内存泄漏:当使用完了对应的ThreadLocal,主动调用remove方法删除。
3. 空指针异常问题
代码演示:
public class ThreadLocalNPE {
ThreadLocal<Long> tl = new ThreadLocal();
public void set(){
tl.set(Thread.currentThread().getId());
}
public long get(){
return tl.get();
}
public static void main(String[] args) {
ThreadLocalNPE item = new ThreadLocalNPE();
System.out.println(item.get());
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
item.set();
System.out.println(item.get());
}
});
thread.start();
}
}
打印结果:
这里的,get()方法出现了NPE异常,那为什么呢?
ThreadLocal在实例化时是指定存储的包装类型 Long (ThreadLocal tl = new ThreadLocal()), 而演示代码中的 get() 方法返回的是基本类型 long,那么他在执行 initialValue() 时返回的是 Long,然后自动拆箱,转为 long 基本类型,这里就出现了错误,因为在返回Long 类型时就是null了,对 null进行拆箱返回基本类型,就会出现空指针这异常!
通过修改get()方法的返回值 ,从long —> Long,就可以解决问题。