【JUC2022】第五章 ThreadLocal
文章目录
- 【JUC2022】第五章 ThreadLocal
- 一、是什么
- 二、案例
- 三、使用规范
- 四、源码分析
- 五、内存泄漏问题
- 六、实际应用 Demo
一、是什么
ThreadLocal 提供线程局部变量,这些变量与正常的变量不同,因为每一个线程在访问 ThreadLocal 实例的时候(通过其 get 或 set 方法)都有自己的、独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如用户 ID 或事务 ID)与线程关联起来
实现了每一个线程都有自己专属的本地变量副本,线程可以通过使用 get() 和 set() 方法,获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题
T get() 返回当前线程的此线程局部变量副本中的值
protected T initialValue() 返回此线程局部变量的当前线程的“初始值”
void remove() 删除此线程局部变量的当前线程值
void set(T value) 将此线程局部变量的当前线程副本设置为指定值
static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) 创建一个线程局部变量
二、案例
5 个销售卖房子,各自有独立的销售额,
package com.sisyphus.ThreadLocal;
import java.util.Random;
import java.util.concurrent.TimeUnit;
class House{
int saleCount = 0;
public synchronized void saleHouse(){
++saleCount;
}
// ThreadLocal<Integer> saleVolume = new ThreadLocal<Integer>(){
// @Override
// protected Integer initialValue(){
// return 0;
// }
// };
ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(()->0);
public void saleVolumeByThreadLocal(){
saleVolume.set(saleVolume.get()+1);
}
}
public class ThreadLocalDemo {
public static void main(String[] args) {
House house = new House();
for(int i = 0; i < 5; i++){
new Thread(()->{
int size = new Random().nextInt(5) + 1;
try{
for(int j = 0; j < size; j++){
house.saleHouse();
house.saleVolumeByThreadLocal();
}
}finally {
house.saleVolume.remove();
}
System.out.println(Thread.currentThread().getName() + "\t" + "号销售卖出:" + house.saleVolume.get());
},String.valueOf(i)).start();
}
//暂停毫秒
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "共计卖出多少套:" + house.saleCount);
}
}
三、使用规范
必须回收自定义的 ThreadLocal 变量,尤其是在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄漏等问题。尽量在代理中使用 try-finally 块进行回收
objectThreadLocal.set(userInfo);
try{
//...
} finally{
objectThreadLocal.remove();
}
package com.sisyphus.ThreadLocal;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyData{
ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(()->0);
public void add(){
threadLocalField.set(1 + threadLocalField.get());
}
}
public class ThreadLocalDemo2 {
public static void main(String[] args) {
MyData myData = new MyData();
ExecutorService threadPool = Executors.newFixedThreadPool(3);
try{
for(int i = 0; i < 10; i++){
threadPool.submit(()->{
try {
Integer beforeInt = myData.threadLocalField.get();
myData.add();
Integer afterInt = myData.threadLocalField.get();
System.out.println(Thread.currentThread().getName() + "\t" + "beforeInt:" + beforeInt + "\t afterInt:" + afterInt );
} finally {
myData.threadLocalField.remove();
}
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
}
四、源码分析
Thread、ThreadLocal、ThreadLocalMap 之间的关系
Entry 内部类
/**
* 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 对象的 map(其实就是以 ThreadLocal 为 Key),不过是经过了两层包装的 ThreadLocal 对象
JVM 内部维护了一个线程版的 Map<ThreadLocal, Value>(通过 ThreadLocal 对象的 set 方法,把 ThreadLocal 实例当作 key 放入 ThreadLocalMap),每个线程要用到这个 ThreadLocal 值的时候,用当前的线程去 Map 里获取,这样每个线程就都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量
ThreadLocal 是一个壳子,真正的存储结构是 ThreadLocal 里有 ThreadLocalMap 这么个内部类,每个 Thread 对象维护着一个 ThreadLocalMap 的引用,ThreadLocalMap 是 ThreadLocal 的内部类,用 Entry 来进行存储
- 调用 ThreadLocal 的 set() 方法时,实际上就是往 ThreadLocalMap 设置值,key 是 ThreadLocal 对象,值 valule 是传递进来的对象
- 调用 ThreadLocal 的 get() 方法时,实际上就是往 ThreadLocalMap 设置值,key 是 ThreadLocal 对象
ThreadLocal 本身并不存储值,它只是自己作为一个 key 来让线程从 ThreadLocalMap 获取 value
五、内存泄漏问题
不会再被使用的对象或者变量占用的内存无法被回收,就是内存泄漏
强引用
当内存不足,JVM 开始垃圾回收,对于强引用的对象,就算是出现了 OOM 也不会对该对象进行回收
强引用是我们最常见的普通对象的引用,只要还有强引用指向一个对象,就表明该对象还存活
Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用
由于即使该对象以后永远都不会被用到,JVM 也不会回收,因此强引用是造成 Java 内存泄漏的主要原因之一
对于一个普通对象,如果没有其他的引用关系,只要超过了引用变量的作用域,或者将引用变量指向 null,一般就认为可以被垃圾回收器收集
软引用
对于只有软引用的对象来说,当系统内存充足时,它不会被回收;当系统内存不足时,它会被回收。软引用通常用在对内存敏感的程序中,比如高速缓存
假如有一个应用需要读取大量的本地图片:
- 如果每次读取图片都从硬盘读取,将会严重影响性能
- 如果一次性全部加载到内存中又可能造成内存溢出
此时使用软引用,可以解决这个问题。用一个 HashMap 来保存图片的路径和图片对象关联的软引用之间的映关系,在内存不足时,JVM 会自动回收这些缓存图片对象占用的空间,从而有效地避免了 OOM 的问题
Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
弱引用
对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,都会回收该对象占用的内存
虚引用
虚引用必须和引用队列(ReferenceQueue)联合使用,虚引用顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象
PhantomReference 的 get 方法总是返回 null。虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被 finalize 以后,做某些事情的通知机制。PhantomReference 的 get 方法总是返回 null,因此无法访问对应的引用对象
设置虚引用关联对象的唯一目的,就是在这个对象被垃圾收集器回收的时候收到一个系统通知或者后续添加进一步的处理,用来实现比 finalize 机制更灵活的回收操作
ThreadLocal 为什么要用弱引用
从图中我们可以看到,ThreadLocal 对象同时被 ThreadLocalRef 和 Entry.key 引用,我们设想一个场景,如果 Entry.key 对 ThreadLocal 是强引用,并且 CurrentThread 是固定线程池中的核心线程。当 CurrentThread 和 ThreadLocal 都完成了自己的任务,CurrentThread 重新回到线程池,ThreadLocalRef 不再指向 ThreadLocal。这时就产生了一个问题,Entry.key 对 ThreadLocal 的引用无法释放,导致 ThreadLocal 无法被回收,并且 Entry.value 指向的对象也无法被回收,这就造成了内存泄漏。但如果 Entry.key 对 ThreadLocal 对象是弱引用呢?那么,在下一次 GC 的时候就会自动回收掉 ThreadLocal 对象
为什么要在 finally 代码块中调用 remove
但 Entry.value 指向的对象还是无法被回收,该怎么办呢?这个问题 ThreadLocal 的设计者们已经为我们想到了,当调用 set、get、remove 三个方法的任意一个方法时,都会先去判断 Entry.key 是否指向 null,如果指向 null,就会把 Entry.value 也指向 null。因此,只要在 ThreadLocalRef 还没有指向 null 之前调用 remove 即可
如果我们没有调用 remove 方法,那么根据 ThreadLocalRef 的指向,会产生两种不同的后果:
- CurrentThread 回到了线程池,ThreadLocalRef 仍然指向 ThreadLocal 对象。此时来了一个新的请求,线程池又将原先的 CurrentThread 分配给了这个请求,那么由于 Entry.key 并没有指向 null,get 方法也就不会先将 Entry.value 指向 null,这样就造成了“脏读”。本次请求的 ThreadLocal 初始值变成了上一个请求执行过程中修改过的值。比如说初始值本该是 0,但是变成了上一次请求执行过程中修改成的 1。我们调用 get 方法,想获取初始值 0,但是我们不知道获取到的值是 1,很可能会导致一系列我们不想看到的的连锁反应。但是我们还是有机会调用 set、get、remove 方法去释放 Entry.value 指向的对象占用的内存的
- CurrentThread 回到了线程池,ThreadLocalRef 指向了 null。那么 Entry.value 指向的对象占用的内存将永远无法被释放,直到线程池被关闭,假如这个线程池是 Tomcat,那么也就意味着随着服务请求次数的增加,ThreadLocalMap 中存在的无法被释放的 (null, Object) Entry 也会越来越来越多,最终一定会导致内存溢出,服务器宕机
六、实际应用 Demo
用户第一次发送请求,可通过 UserInterceptor 进行拦截,获取到 Request 里的 UserInfo,并保存到 UserContextHolder 工具类中,作为用户信息的上下文。当有多个用户请求 Web APP 时,每个请求都有自己的 ThreadLocal,都会去使用自己的 UserContextHolder
User
package com.sisyphus.ThreadLocal;
import java.util.Date;
public class User {
private String uuid;
private Date lastLogin;
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public Date getLastLogin() {
return lastLogin;
}
public void setLastLogin(Date lastLogin) {
this.lastLogin = lastLogin;
}
}
UserContextHolder
package com.sisyphus.ThreadLocal;
public class UserContextHolder {
private static ThreadLocal<User> userInfoHolder = new ThreadLocal<>();
public static void setCurrentUser(User user){
if(userInfoHolder.get() == null){
userInfoHolder.set(user);
}
}
public static User getCurrentUser(){
return userInfoHolder.get();
}
public static void removeCurrentUser(){
userInfoHolder.remove();
}
}
UserInterceptor
package com.sisyphus.ThreadLocal;
public class UserInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
User user = (User)request.getSession().getAttribute("user");
if(user != null){
UserContextHolder.setCurrentUser(user);
return true;
}
return false;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception{
UserContextHolder.removeCurrentUser();
}
}
UserController
package com.sisyphus.ThreadLocal;
@Controller("/user")
public class UserController {
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String login(){
User user = UserContextHolder.getCurrentUser();
String result = null;
//todo
return result;
}
}