浅谈TheadLocal的使用场景和注意事项

概念

ThreadLocal 是Java的一个类,是一个本地线程,提供了一种线程安全的方式,主要用来避免共享数据(线程变量隔离)。

有时候可能要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例;就是说,每个线程都有一个伴生的空间(ThreadLocal),存储私有的数据,只要线程在,就能拿到对应线程的ThreadLocal中存储的值,实际上ThreadLocal保证线程安全是一种空间换时间的思想。

TheadLocal的使用场景和注意事项

ThreadLocal在Java开发中非常常见,一般在以下情况会使用到ThreadLocal:

  • 在进行对象跨层传递的时候,可以考虑使用ThreadLocal,避免方法多次传递,打破层次间的约束。

  • 线程间数据隔离,比如:上下文ActionContext、ApplicationContext。

  • 进行事务处理,用于存储线程事务信息。

在使用ThreadLocal的时候,最常用的方法就是:initialValue()、set(T t)、get()、remove()。

创建以及提供的方法

创建一个线程局部变量,其初始值通过调用给定的提供者(Supplier)生成;

publicstatic <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        returnnewSuppliedThreadLocal<>(supplier);
    }
    
// InitialValue()初始化方式使用Java 8提供的Supplier函数接口会更加简介ThreadLocal<String> userContext = ThreadLocal.withInitial(String::new);
复制代码

这里就列出用的比较多的方法:

将此线程局部变量的当前线程副本设置为指定值;value表示要存储在此线程本地的当前线程副本中的值

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
复制代码

返回此线程局部变量的当前线程副本中的值。 如果该变量对于当前线程没有值,则首先将其初始化为调用initialValue方法返回的值

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();
}
复制代码

删除此线程局部变量的当前线程值

publicvoidremove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
复制代码

项目实例

以下是个人使用的场景:

为什么会使用它,如果在项目中想直接获取当前登录用户的信息,这个功能就可以使用ThreadLocal实现。

/**
 * 登录用户信息上下文
 *
 * @author: austin
 * @since: 2023/2/8 13:47
 */publicclassUserContext {

    privatestatic final ThreadLocal<User> USER_CONTEXT = ThreadLocal.withInitial(User::new);

    publicstaticvoidset(User user) {
        if (user != null) {
            USER_CONTEXT.set(user);
        }
    }
    
    publicstatic User get() {
        return USER_CONTEXT.get();
    }
    
    publicstaticvoidremove() {
        USER_CONTEXT.remove();
    }

    publicstatic User getAndThrow() {
        User user = USER_CONTEXT.get();
        if (user == null || StringUtils.isEmpty(user.getId())) {
            thrownew ValidationException("user info not found!");
        }
        return user;
    }
}
复制代码

上面其实是定义了一个用户信息上下文类,关于上下文(context),我们在开发的过程中经常会遇到,比如Spring的ApplicationContext,上下文是贯穿整个系统或者阶段生命周期的对象,其中包含一些全局的信息,比如:登录后用户信息、账号信息、地址区域信息以及在程序的每一个阶段运行时的数据。

👏有了这个用户上下文对象之后,接下来就可以在项目中使用:

在该项目中个人使用的地方在登录拦截器中,当对登录的信息检查成功后,那么将当前的用户对象加入到ThreadLocal中:

User currentUser = userService.login(token.getUsername(), String.valueOf(token.getPassword()));
// 用户登录认证成功,UserContext存储用户信息
UserContext.put(currentUser);复制代码

在Serivce实现层使用的时候,直接调用ThreadLocal中的get方法,就可以获得当前登录用户的信息:

//获取当前在线用户信息
User user = UserContext.get();复制代码

资源调用完成后需要在拦截器中删除ThreadLocal资源,防止内存泄漏问题:

@OverridepublicvoidafterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throws Exception {
    //使用完的用户信息需要删除,防止内存泄露
    UserContext.remove();
}
复制代码

ThreadLocal的内存泄露问题🤢

如果我们在使用完该线程后不进行ThreadLocal中的变量进行删除,那么就会造成内存泄漏的问题,那么该问题是怎么出现的?

首先先分析一下ThreadLocal的内部结构:

先明确一个概念:对应在栈中保存的是对象的引用,对象的值是存储在堆中,如上图所示:其中Heap中的map是ThreadLocalMap, 里面包含key和value, 其中value就是我们需要保存的变量数据,key则是ThreadLocal实例,上述图片的连接有实线和虚线,实线代表强引用,虚线表示弱引用。

即:每一个Thread维护一个ThreadLocalMap, key为使用 弱引用 的ThreadLocal实例,value为线程变量的副本。

扫盲强引用、软引用、弱引用、虚引用:😂

不同的引用类型呢,主要体现在对象的不同的可达性状态和对垃圾收集的影响:

强引用 是Java最常见的一种引用,只要还有强引用指向一个对象,那么证明该对象一定还活着,一定为可达性状态,不会被垃圾回收机制回收,因此,强引用是造成Java内存泄漏的主要原因。

软引用 是通过SoftReference实现的,如果一个对象只要软引用,那么在系统内存空间不足的时候会试图回收该引用指向的对象。

弱引用 是通过WeakReference实现的,如何一个对象只有弱引用,在垃圾回收线程扫描它所管辖的内存区域的时候,一旦发现只有弱引用指向的对象时候,不管当前的内存空间是否足够,垃圾回收器都会去回收这样的一个内存。

虚引用 形同虚设的东西,在任何情况下都可能被回收。

我们都知道,map中的value需要key找到,key没了,那么value就会永远的留在内存中,直到内存满了,导致OOM,所以我们就需要使用完以后进行手动删除,这样能保证不会出现因为GC的原因造成的OOM问题;当ThreadLocal Ref显示的指定为null时,关系链就变成了下面所示的情况:

当ThreadLocal被显示显的指定为null之后,JVM执行GC操作,此时堆内存中的Thread-Local被回收,同时ThreadLocalMap中的Entry.key也成为了null,但是value将不会被释放,除非当前线程已经结束了生命周期的Thread引用被垃圾回收器回收。

ThreadLocal解决SimpleDateFormat非线程安全问题

为了找到问题所在,我们尝试查看SimpleDateFormat中format方法的源码来排查一下问题,format方法源码如下:

private StringBuffer format(Date date, StringBuffer toAppendTo,
                            FieldDelegate delegate) {
    
    // 注意到此行setTime()方法代码
    calendar.setTime(date);

    boolean useDateFormatSymbols = useDateFormatSymbols();

    for (int i = 0; i < compiledPattern.length; ) {
        int tag = compiledPattern[i] >>> 8;
        int count = compiledPattern[i++] & 0xff;
        if (count == 255) {
            count = compiledPattern[i++] << 16;
            count |= compiledPattern[i++];
        }

        switch (tag) {
        case TAG_QUOTE_ASCII_CHAR:
            toAppendTo.append((char)count);
            break;

        case TAG_QUOTE_CHARS:
            toAppendTo.append(compiledPattern, i, count);
            i += count;
            break;

        default:
            subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
            break;
        }
    }
    return toAppendTo;
}
复制代码

从上述源码可以看出,在执行SimpleDateFormat.format()方法时,会使用calendar.setTime()方法将输入的时间进行转换,那么我们想象一下这样的场景:

  • 线程 1 执行了calendar.setTime(date) 方法,将用户输入的时间转换成了后面格式化时所需要的时间;

  • 线程 1 暂停执行,线程 2 得到CPU时间片开始执行;

  • 线程 2 执行了calendar.setTime(date)方法,对时间进行了修改;

  • 线程 2 暂停执行,线程 1 得出CPU时间片继续执行,因为线程 1 和线程 2 使用的是同一对象,而时间已经被线程 2 修改了,所以此时当线程 1 继续执行的时候就会出现线程安全的问题了。

正常情况下,程序执行是这样的:

非线程安全的执行流程是这样的:

了解了ThreadLocal的使用之后,接下来我们将使用ThreadLocal来实现多线程并发下时间的格式化,看看ThreadLocal如何保证线程安全的,具体演示代码如下:

  • 线程不安全时间工具类

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 线程不安全时间工具类
 *
 * @author: austin
 * @since: 2023/2/8 15:36
 */publicclassConcurrentUnSafeDateUtil {

    privatestaticfinalStringdate_format="yyyy-MM-dd HH:mm:ss";
    privatestatic ThreadLocal<DateFormat> threadLocal = newThreadLocal<DateFormat>();

    privatestaticfinalSimpleDateFormatsdf=newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    publicstatic Date parse(String strDate) {
        Datedate=null;
        try {
            date = sdf.parse(strDate);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }

    publicstaticvoidmain(String[] args) {
        ExecutorServiceexecutorService= Executors.newFixedThreadPool(8);
        for (inti=0; i < 8; i++) {
            executorService.execute(()->{
                System.out.println(ConcurrentUnSafeDateUtil.parse("2023-02-08 11:23:56"));
            });
        }
        executorService.shutdown();
    }
}
复制代码

运行后,发现会报错:

这是因为SimpleDateFormat不是线性安全的,它以共享变量出现时,并发多线程场景下即会报错。

  • 线程安全时间工具类(采用ThreadLocal改造后的线程安全类)

public class ConcurrentSafeDateUtil {

    private static final String date_format = "yyyy-MM-dd HH:mm:ss";
    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>();

    public static DateFormat getDateFormat() {
        DateFormat df = threadLocal.get();
        if (df == null) {
            df = new SimpleDateFormat(date_format);
            threadLocal.set(df);
        }
        return df;
    }

    public static Date parse(String strDate) {
        Date date = null;
        try {
            date = getDateFormat().parse(strDate);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }


    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(8);
        for (int i = 0; i < 8; i++) {
            executorService.execute(() -> {
                System.out.println(ConcurrentSafeDateUtil.parse("2023-02-08 11:23:56"));
            });
        }
        executorService.shutdown();
    }
}
复制代码

运行后,控制台正常输出:

"C:\Program Files\Java\jdk1.8.0_311\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2022.2.3\lib\idea_rt.jar=65019com.layblog.utils.ConcurrentSafeDateUtil
Wed Feb 08 11:23:56 CST 2023
Wed Feb 08 11:23:56 CST 2023
Wed Feb 08 11:23:56 CST 2023
Wed Feb 08 11:23:56 CST 2023
Wed Feb 08 11:23:56 CST 2023
Wed Feb 08 11:23:56 CST 2023
Wed Feb 08 11:23:56 CST 2023
Wed Feb 08 11:23:56 CST 2023
复制代码

当然也可以使用:

  • Apache commons包的DateFormatUtils或者FastDateFormat实现,宣称是既快又线程安全的SimpleDateFormat,并且更高效。

  • 使用Joda-Time类库来处理时间相关问题。

总结

本文简单的介绍了ThreadLocal的应用场景,其主要用在需要每个线程独占的元素上,例如SimpleDateFormat。然后,就是介绍了ThreadLocal的实现原理,详细介绍了set()和get()方法,介绍了ThreadeLocalMap的数据结构,最后就是说到了ThreadLocal的内存泄露以及避免的方式。

作者:austin流川枫

链接:https://juejin.cn/post/7197673814179070010

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值