敞开成长之旅!这是我参与「日新计划 2 月更文挑战」的第 7 天,点击检查活动概况

概念

ThreadLocalJava的一个类,是一个本地线程,供给了一种线程安全的办法,首要用来防止同享数据(线程变量阻隔)。

有时分或许要防止同享变量,运用ThreadLocal辅佐类为各个线程供给各自的实例;就是说,每个线程都有一个伴生的空间(ThreadLocal),存储私有的数据,只需线程在,就能拿到对应线程的ThreadLocal中存储的值,实际上ThreadLocal确保线程安全是一种空间换时刻的思维。

TheadLocal的运用场景和注意事项

ThreadLocalJava开发中非常常见,一般在以下情况会运用到ThreadLocal

  • 在进行目标跨层传递的时分,能够考虑运用ThreadLocal,防止办法多次传递,打破层次间的束缚。
  • 线程间数据阻隔,比方:上下文ActionContext、ApplicationContext
  • 进行事务处理,用于存储线程事务信息。

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

在运用ThreadLocal的时分,最常用的办法就是:initialValue()、set(T t)、get()、remove()

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

创建以及供给的办法

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

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(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();
}

删去此线程局部变量的当时线程值

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

项目实例

以下是个人运用的场景:

为什么会运用它,假如在项目中想直接获取当时登录用户的信息,这个功能就能够运用ThreadLocal完成。

/**
 * 登录用户信息上下文
 *
 * @author: austin
 * @since: 2023/2/8 13:47
 */
public class UserContext {
    private static final ThreadLocal<User> USER_CONTEXT = ThreadLocal.withInitial(User::new);
    public static void set(User user) {
        if (user != null) {
            USER_CONTEXT.set(user);
        }
    }
    public static User get() {
        return USER_CONTEXT.get();
    }
    public static void remove() {
        USER_CONTEXT.remove();
    }
    public static User getAndThrow() {
        User user = USER_CONTEXT.get();
        if (user == null || StringUtils.isEmpty(user.getId())) {
            throw new ValidationException("user info not found!");
        }
        return user;
    }
}

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

有了这个用户上下文目标之后,接下来就能够在项目中运用:

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

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

Serivce完成层运用的时分,直接调用ThreadLocal中的get办法,就能够获得当时登录用户的信息:

//获取当时在线用户信息
User user = UserContext.get();

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

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    //运用完的用户信息需求删去,防止内存走漏
    UserContext.remove();
}

ThreadLocal的内存走漏问题

假如咱们在运用完该线程后不进行ThreadLocal中的变量进行删去,那么就会形成内存走漏的问题,那么该问题是怎样呈现的?

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

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

先清晰一个概念:对应在栈中保存的是目标的引证,目标的值是存储在堆中,如上图所示:其中Heap中的mapThreadLocalMap, 里面包括keyvalue, 其中value就是咱们需求保存的变量数据,key则是ThreadLocal实例,上述图片的连接有实线和虚线,实线代表强引证,虚线表示弱引证。

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

扫盲强引证、软引证、弱引证、虚引证:

不同的引证类型呢,首要体现在目标的不同的可达性状况和对废物收集的影响:

强引证Java最常见的一种引证,只需还有强引证指向一个目标,那么证明该目标必定还活着,必定为可达性状况,不会被废物收回机制收回,因而,强引证是形成Java内存走漏的首要原因。

软引证 是经过SoftReference完成的,假如一个目标只需软引证,那么在体系内存空间缺乏的时分会试图收回该引证指向的目标。

弱引证 是经过WeakReference完成的,如何一个目标只要弱引证,在废物收回线程扫描它所管辖的内存区域的时分,一旦发现只要弱引证指向的目标时分,不管当时的内存空间是否足够,废物收回器都会去收回这样的一个内存。

虚引证 形同虚设的东西,在任何情况下都或许被收回。

咱们都知道,map中的value需求key找到,key没了,那么value就会永久的留在内存中,直到内存满了,导致OOM,所以咱们就需求运用完今后进行手动删去,这样能确保不会呈现由于GC的原因形成的OOM问题;当ThreadLocal Ref显现的指定为null时,关系链就变成了下面所示的情况:

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

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

ThreadLocal处理SimpleDateFormat非线程安全问题

为了找到问题所在,咱们测验检查SimpleDateFormatformat办法的源码来排查一下问题,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 持续履行的时分就会呈现线程安全的问题了。

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

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

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

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

了解了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
 */
public class ConcurrentUnSafeDateUtil {
    private static final String date_format = "yyyy-MM-dd HH:mm:ss";
    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>();
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static Date parse(String strDate) {
        Date date = null;
        try {
            date = sdf.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(ConcurrentUnSafeDateUtil.parse("2023-02-08 11:23:56"));
            });
        }
        executorService.shutdown();
    }
}

运转后,发现会报错:

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

这是由于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流川枫,咱们下期见!