简介
了解java.lang.ThreadLocal<T>
类的两种常见使用和原理,并在项目中使用ThreadLocal
。
理论
ThreadLocal是Thread的局部变量,用于编多线程程序,对解决多线程程序的并发问题有一定的启示作用。
ThreadLocal 是線程的局部變量, 是每一個線程所單獨持有的,其他線程不能對其進行訪問
使用场景
介绍ThreadLocal
使用中的两种经典场景
- 每个线程需要一个独享的对象:通常为工具类,如
SimpleDateFormat
。 - 每个线程内需要保存全局变量,可以在不同地方直接使用:如在拦截器中获取用户信息。
场景一
发现问题
使用SimpleDateFormat
时,每个线程都new一个新的SimpleDateFormat
,没有出现问题,如下。
public class Solution {
static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println(date(finalI));
}
});
}
threadPool.shutdown();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(date(10000));
}
}).start();
}
static String date(int sec) {
Date data = new Date(1000L * sec);
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");//每个线程使用时都会new一个新的
return format.format(data);
}
}
复制代码
此时我们想,每个线程都创建一个新的SimpleDateFormat
对象会浪费资源,所以我们把date()
方法中的SimpleDateFormat format
变量拿出来写成一个静态变量,每个线程都使用这个静态的static SimpleDateFormat format
,如下。
public class Solution {
static ExecutorService threadPool = Executors.newFixedThreadPool(10);
static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");//静态变量
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println(date(finalI));
}
});
}
threadPool.shutdown();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(date(10000));
}
}).start();
}
static String date(int sec) {
Date data = new Date(1000L * sec);
return format.format(data);
}
}
复制代码
输出的结果中出现了相同的时间,这是有问题的,因为传入date()
函数的变量一定是不一样的,如下。
1970-01-01 08:01:37
1970-01-01 08:01:34
1970-01-01 08:01:34
1970-01-01 08:01:32
复制代码
结论:SimpleDateFormat
是线程不安全的。
SimpleDateFormat线程安全问题-极光社区 (jiguang.cn)
解决问题
- 如发现问题中的第一段代码,将
SimpleDateFormat format
定义为局部变量(即每次都new新的),而不是静态变量。 - 使用
synchronized
加锁,但因为等待排队会出现性能问题。
static String date(int sec) {
Date data = new Date(1000L * sec);
String str = null;
synchronized (Solution.class) {
str = format.format(data);
}
return str;
}
复制代码
- 利用
ThreadLocal
给每个线程分配自己的SimpleDateFormat
对象,保证了线程安全&&高效利用内存。
static String date(int sec) {
Date data = new Date(1000L * sec);
//SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
SimpleDateFormat format = ThreadSafeFormatter.dateFormatThreadLocal.get();
return format.format(data);
}
复制代码
dateFormatThreadLocal
与dateFormatThreadLocal2
等效。
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"));
}
复制代码
- 使用
DateTimeFormatter
。
场景二
发现问题
一个web server中存在一条service链:request -> service1 -> service2 -> service3 -> service4 -> ...
service1负责把用户信息userInfo从request中提取出来,之后userInfo就会从service1开始在链上传递,这样会导致代码冗余,不易维护。
补充信息
浏览器通过此连接发送HTTP请求,当请求进入Tomcat时,Tomcat会从其线程池中分配一个线程来处理请求.生成并发送响应后,线程将返回池,准备从任何客户端提供另一个请求.
我们的需求:每个线程内需要保存一个全局变量userInfo,可以在不同的地方直接使用。
解决问题
- 一个static变量存储信息可以么?
答:不可以。信息在同一个线程内相同,但是在不同的线程中不一定相同,即:用户信息在一次request中应该相同,但在不同request中不一定相同。
- 使用Map可以么?在发现问题的条件下,引入一个map,service1中将用户信息put进map,service2等后续方法可以直接从map中get到userInfo。
答:同时会有多个请求访问web server,即在多线程环境下,我们需要保证线程安全,无论是使用synchronized
,还是concurrentHashMap
,都会影响性能。
-
ThreadLocal
。
public class TestThreadLocal {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
new service1().process("绫波丽");
new service2().sayHello();
new service3().sayGoodbye();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
new service1().process("玛奇玛");
new service2().sayHello();
new service3().sayGoodbye();
}
}).start();
}
}
class UserContextHolder {
public static ThreadLocal<User> threadLocal = new ThreadLocal<>();
}
class service1 {
public void process(String name) {
User user = new User(name);
UserContextHolder.threadLocal.set(user);
}
}
class service2 {
public void sayHello() {
System.out.println("Hello," + UserContextHolder.threadLocal.get().getName() + "!");
}
}
class service3 {
public void sayGoodbye() {
System.out.println("Goodbye," + UserContextHolder.threadLocal.get().getName() + "!");
UserContextHolder.threadLocal.remove();
}
}
//User类省略
复制代码
结果如下。
Hello,绫波丽!
Hello,玛奇玛!
Goodbye,绫波丽!
Goodbye,玛奇玛!
复制代码
每次http请求都对应一个线程,线程之间相互隔离,这就是ThreadLocal
的经典应用场景。
总结
- 对象生成时机由我们控制,重写
initialValue()
方法来保存对象。 - 对象生成时机不由我们控制,调用
set()
方法来保存对象。
好处:
- 线程安全
- 不加锁,效率高
- 高效利用内存:相比于每一个线程任务都新建一个
SimpleDateFormat
,使用ThreadLocal
可以节约内存(注意线程池在此处与ThreadLocal
的关联)。 - 解决场景二的问题
成员方法
ThreadLocal - Java 11中文版 - API参考文档 (apiref.com)
initialValue()
- 返回此线程局部变量的当前线程的“初始值”。
- 这是一个延时加载方法,只有在调用第一次调用
get()
方法时才会触发。如果不set()
直接调用get()
方法,get()
方法会返回setInitialValue()
方法,setInitialValue()
方法调用initialValue()
方法。 - 如果用了
set()
,就不会再调用initialValue()
方法了。 - 通常每个线程最多调用一次此方法,但如果调用了
remove()
后,再get()
还可以再调用此方法。 -
initialValue()
方法默认返回null
,所以如果需要使用initialValue()
初始化,需要重写此方法。
成员方法setInitialValue()
源码如下。
private T setInitialValue() {
T value = initialValue();//调用initialValue()
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);//调用map.set()
else
createMap(t, value);
return value;
}
复制代码
set()、get()、remove()
- 设置新值。
- 得到线程对应value。如果没有
set()
过,且首次调用get()
,会调用initialValue()
获得初始值。 - 删除线程对应的值。
get()
方法是先取出当前线程的ThreadLocalMap map
,然后调用map.getEntry()
方法,把本ThreadLocal
的引用作为参数传入,取出map
中属于本ThreadLocal
的value,源码如下。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//map不为空,就从map中通过键(ThreadLocal)获取键值对对象Entry
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();//map为空或者map中键对应的键值对对象为空,调用setInitialValue()
}
复制代码
set()
源码如下:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);//调用map.set()
else
createMap(t, value);
}
复制代码
场景一与场景二综合:观察到setInitialValue()
(initialValue()
)和set()
都是利用map.set()
方法来设置值,最后都对应到ThreadLocalMap map
中的一个Entry e
。
ThreadLocal
部分源码如下。
public class ThreadLocal<T> {
...
static class ThreadLocalMap {
...
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
...
}
}
}
复制代码
原理
每个Thread
(线程)持有一个ThreadLocalMap
成员变量,一个ThreadLocalMap
中维护多个ThreadLocal
。
为什么map中有多个ThreadLocal
呢?
答:场景一格式化日期时使用ThreadLocal<SimpleDateFormat> dateFormatThreadLocal
,场景二保存与使用用户信息使用ThreadLocal<User> threadLoca
l。如果一个线程既要格式化日期,又要保存与使用用户信息,那么这个线程的ThreadLocalMap
中就有两个ThreadLocal
了。
怎么理解ThreadLocal.ThreadLocalMap threadLocals
?
答:ThreadLocalMap
类是每个线程Thread
类里的变量,里面最重要的是一个键值对数组Entry[] table
,可以认为是一个哈希表。
- 键:
ThreadLocal
引用。 - 值:实际需要的对象,如
SimpleDateFormat
对象,User
对象。
ThreadLocalMap 是一个定制的哈希映射,仅适用于 维护线程本地值。不导出任何操作 在 ThreadLocal 类之外。
ThreadLocalMap
的冲突处理?
答:HashMap
使用拉链法+红黑树解决冲突问题,而ThreadLocalMap
中使用线性探测法,即如果发生冲突,就继续找下一个空着的位置。
注意
内存泄露
内存泄露:某个对象不再有用,但是它占用着的内存却不能被收回。
线程活着,线程里的ThreadLocalMap
就活着,里面维护的键值对们就活着,导致无法GC。
- 為了解決這個問題,java 做了一個小優化,也就是存放在 ThreadLocalMap 中的 ThreadLocal,會使用 弱引用 來儲存,也就是說,如果一個 ThreadLocal 內存地址沒有外部強引用來引用他,只有這條 ThreadLocalMap 的弱引用來引用他時,那麼當系統 GC 時,這些 ThreadLocal 就會被回收(因為是弱引用),如此一來,ThreadLocalMap 中就會出現 key 為 null 的 Entry 們
- 這個弱引用優化只能使得 ThreadLocal 被正確回收,但是這些 key 為 null 的 Entry 們仍然會存在在 ThreadLocalMap 裡,因此 value 仍然無法被回收
- 所以 java 又做了一個優化,就是在 ThreadLocal 執行
get()
、set()
、remove()
方法時,都會將該線程 ThreadLocalMap 裡所有 key = null 的 value 也設置為 null,手動幫助 GCThreadLocal k = e.get(); if (k == null) { e.value = null; // Help the GC } 复制代码
- 但是根本上的解決辦法,還是在當前線程使用完這個 ThreadLocal 時,就即時的
remove()
掉該 value,也就是使得 ThreadLocalMap 中不要存在這個鍵值對,這樣才能確保 GC 能正確回收
空指针异常
出现问题
public class TestNPE {
public static void main(String[] args) {
IntegerHolder integerHolder = new IntegerHolder();
System.out.println(integerHolder.get());//注意此处之前没有set或初始化
new Thread(new Runnable() {
@Override
public void run() {
integerHolder.set(Thread.currentThread().getId());
System.out.println(integerHolder.get());//此处前有set
}
}).start();
}
}
class IntegerHolder {
public ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public long get() {//注意此处返回值为long而不是Long
return threadLocal.get();
}
public void set(long num) {
threadLocal.set(num);
}
}
复制代码
以上代码运行结果如下。
Exception in thread "main" java.lang.NullPointerException
at daijizai.IntegerHolder.get(TestNPE.java:29)
at daijizai.TestNPE.main(TestNPE.java:14)
复制代码
问题分析:出现问题的地方为之前没有初始化或set()
的get()
语句,因为没有初始化或set()
,所以threadLocal.get()
会返回一个值为null
的Long
,而class IntegerHolder
中get()
方法的返回值类型为long
(注意是long
而不是Long
),包装类Long
为null
自动拆箱为基本数据类型long
时,出现空指针异常。
解决问题
class IntegerHolder
中get()
方法的返回值类型改为Long
。
修改后运行结果如下。
null
12
复制代码
共享对象
如果每个线程中ThreadLocal.set()
进去的东西本来就是多个线程共享的同一个对象,如static对象,此时多个线程进行ThreadLocal.get()
取得到的还是这个共享对象本身,存在并发访问问题。
实战
ThreadLocal
HostHolder
@Component
public class HostHolder {
private ThreadLocal<User> users=new ThreadLocal<>();
public void setUser(User user){
users.set(user);
}
public User getUser(){
return users.get();
}
public void clear(){
users.remove();
}
}
复制代码
LoginTicketInterceptor
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {
@Autowired
private HostHolder hostHolder;
...
@Override
public boolean preHandle(...){
...
hostHolder.setUser(user);
...
}
...
@Override
public void afterCompletion(...){
hostHolder.clear();
...
}
}
复制代码
在任何需要user对象的地方User user = hostHolder.getUser();
。
RequestContextHolder
在Web开发中,service层或者某个工具类中需要获取到HttpServletRequest对象还是比较常见的。一种方式是将HttpServletRequest作为方法的参数从controller层一直放下传递,不过这种有点费劲,且做起来不是优雅;还有另一种则是RequestContextHolder,直接在需要用的地方使用如下方式取HttpServletRequest即可
使用AOP统一记录日志时需要获取登录用户的IP地址,这个地址保存在request请求对象中。
省略与主题无关的代码,核心代码如下。
...
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String ip = request.getRemoteHost();
...
复制代码
RequestContextHolder
部分源码如下。
public abstract class RequestContextHolder {
...
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
...
public static RequestAttributes getRequestAttributes() {
...
}
...
}