当前位置 : 主页 > 编程语言 > java >

Java并发——ThreadLocal详解

来源:互联网 收集:自由互联 发布时间:2023-02-04
引言 ThreadLocal的官方API解释为:“该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变

引言

ThreadLocal的官方API解释为: “该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。”

 

大概的意思有两点:

  • 1、ThreadLocal提供了一种访问某个变量的特殊方式:访问到的变量属于当前线程,即保证每个线程的变量不一样,而同一个线程在任何地方拿到的变量都是一致的,这就是所谓的线程隔离。
  • 2、如果要使用ThreadLocal,通常定义为private static类型,在我看来最好是定义为private static final类型。

什么是ThreadLocal变量

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

 

因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。 ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

 

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。

两大使用场景——ThreadLocal的用途

  • 1、每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)
  • 2、每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦

每个线程需要一个独享的对象

  • 每个Thread内有自己的实例副本,不共享
/** * @Description: 1000个打印日期的任务,用线程池来执行 使用ThreadLocal来解决线程安全问题 */ public class ThreadLocalTest6 { public static ExecutorService executorService = Executors.newFixedThreadPool(10); public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000; i++) { final int seconds = i; executorService.submit(new Runnable() { public void run() { String date = new ThreadLocalTest6().date(seconds); System.out.println(date); } }); // executorService.shutdown(); } } public String date(int seconds){ //参数的单位是毫秒,从1970.1.1 00:00:00开始计时 Date date = new Date(1000 * seconds); SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get(); return simpleDateFormat.format(date); } } class ThreadSafeFormatter{ public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){ @Override protected SimpleDateFormat initialValue() { //初始化SimpleDateFormat return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); }

每个线程内需要保存全局变量

当前用户信息需要被线程内所有方法共享

  • 比较繁琐的解决方案是把user作为参数层层传递,从service-1()传递到service-2(),在从service-2()传递到service-3(),以此类推,但是这样做会导致代码冗余且不容易维护

每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦

  • 在此基础上可以演进,使用UserMap

  • 当多线程同时工作时,我们需要保证线程安全,可以使用synchronized,也可以使用ConcurrentHashMap,但无论使用什么,都会对性能有所影响

  • 更好的办法就是使用ThreadLocal,这样无需synchronized,可以在不影响性能的情况下,也无需层层传递参数,就可以达到保存当前线程对应的用户信息的目的

方法

  • 使用ThreadLocal保存一些业务内容(用户权限信息,从用户系统获取到的用户名、userID等)
  • 这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不同的
  • 在线程生命周期内,都通过这个静态ThreadLocal实例的get()方法取得自己set进去的那个对象,从而避免了将这个对象(例如user对象)作为参数传递的麻烦
  • 强调的是同一个请求(同一个线程内)不同方法间的共享
  • 不需要重写initialValue()方法,但是必须手动调用set()方法
/** * @author: huangyibo * @Date: 2020/2/15 16:03 * @Description: 演示ThreadLocal用法2,避免传递参数的麻烦 */ public class ThreadLocalDemo1 { public static void main(String[] args) { Service1 service1 = new Service1(); 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); new Service3().process(); } } class Service3{ public void process(){ User user = UserContextHolder.holder.get(); System.out.println("service3:"+user.name); UserContextHolder.holder.remove();//使用完ThreadLocal之后调用remove()方法,避免内存泄漏 } } class UserContextHolder{ public static ThreadLocal<User> holder = new ThreadLocal<>(); } class User{ String name; public User(String name) { this.name = name; } }

ThreadLocal的两个作用总结

  • 1、让某个需要用到的对象在线程间隔离(每个线程都有自己独立的对象)
  • 2、在任何方法中都可以轻松获取到该对象
  • 3、根据共享对象的生成时机不同,选择initialValue或set来保存对象

场景一:initialValue

  • 在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机可以由我们控制

场景二:set

  • 如果需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set直接放到我们的ThreadLocal中去,以便后续使用

使用ThreadLocal带来的好处

  • 线程安全
  • 不需要加锁,提高执行效率
  • 更高效的利用内存,节省开销。相比与每个任务都新建一个SimpleDateFormat,显然ThreadLocal可以节省内存和开销
  • 避免传参的繁琐。无论是场景一的工具类,还是场景二的用户名,都可以在任何地方直接通过ThreadLocal拿到,再也不需要每次都传同样的参数,ThreadLocal使得代码耦合度更低,更优雅

ThreadLocal原理

首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。

 

因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 Thread 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。例如下面的 set 方法:

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

ThreadLocal主要方法

  • protected T initialValue():初始化
    • 该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候才会触发
    • 当线程第一次调用get方法访问变量时,调用此方法,如果线程先前调用了set方法,在这种请求下,不会为线程调用本initialValue方法
    • 通常,每个线程最多调用一次此方法,但如果已经调用了remove()后,再调用get(),则可以再次调用此方法
    • 如果不重写该方法,这个方法会返回null,一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象
  • public void set(T value) :为这个线程设置一个新值
  • public T get() :得到这个线程对应的value。如果是首次调用get(),则会调用initialValue()方法来得到这个值
  • public void remove():删除对应线程的值

ThreadLocal主要方法源码解析

  • public T get() :get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value
  • 注意这个map以及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中
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(); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
  • public void set(T value) :和setInitialValue()方法很类似
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
  • public void remove()方法
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }

ThreadLocalMap

  • ThreadLocalMap类,也就是Thread.threadLocals
  • ThreadLocalMap类是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个Map,键值对:
    • 键:这个ThreadLocal
    • 值:实际需要的成员变量,比如User或者SimpleDateFormat
  • ThreadLocalMap虽然类似HashMap但是处理处理Hash冲突略有不同
    • HashMap
    • ThreadLocalMap采用的是线性探测法,也就是如果发生Hash冲突,就继续寻找下一个空位,而不是采用链表拉链

ThreadLocal两种使用场景殊途同归

  • 通过源码分析可以看出,setInitialValue和直接set最后都是利用map.set()方法来设置值
  • 也就是说,最后都会对应到ThreadLocalMap的一个Entry,只是起点和入口不一样

ThreadLocal注意点

内存泄漏问题

内存泄漏:某个对象不再有用,但是占用的内存确不能被回收。

Key的泄漏 ThreadLocalMap中的Entry继承自WeakReference,是弱引用,弱引用的特点是,如果这个对象只被弱引用关联(没有任何强引用关联),那么在下一次垃圾回收的时候必然会被清理掉。

static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }

Value的泄漏

  • ThreadLocalMap的每个Entry都是一个对Key的弱引用,同时每个Entry都包含了一个对Value的强引用
  • 正常情况下,当线程终止,保存在ThreadLocal里的Value会被垃圾回收,因为没有任何强引用关联
  • 但是,如果线程始终不终止(比如线程需要保持很久或者使用线程池的时候),那么Key对应的Value就不能被回收,因为有以下的调用链:
  • 因为Value和Thread之间还存在这个强引用调用链,所以导致value无法被回收,就可会出现OOM
  • JDK已经考虑到了这个问题,所以在set()、remove()、rehash()方法中会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以被回收
  • 但是如果一个ThreadLocal不被使用,那么实际上set()、remove()、rehash()等方法也不会被调用,并且线程又不停止,那么调用链就一直存在,那么也就导致了value的内存泄漏

如何避免内存泄漏(阿里代码规约)

  • 调用remove()方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal之后,应该调用remove()方法
  • 如果使用拦截器获取用户信息,那么同样应该使用拦截器在线程请求退出之前将之前保存过得信息清除掉

空指针异常

  • 在进行get()之前,必须先进行set(),否则可能会报空指针异常
public class ThreadLocalNPE { ThreadLocal<Long> threadLocal = new ThreadLocal<Long>(); public void set(){ threadLocal.set(Thread.currentThread().getId()); } /** * 这里的返回值使用long的时候,如果没有set()就调用get()那么会报空指针异常,因为牵涉到拆箱转换(将对象类型转换为基本类型) * @return */ public Long get(){ return threadLocal.get(); } public static void main(String[] args) { ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE(); System.out.println(threadLocalNPE.get()); new Thread(() -> { threadLocalNPE.set(); long threadId = threadLocalNPE.get(); System.out.println(threadId); }).start(); } }

共享对象

  • 如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题

如果不使用ThreadLocal就可以解决问题,那么就不要强行使用

  • 例如在任务很少的时候,在局部变量中可以新建对象就可以解决问题,那么就不需要使用到ThreadLocal

优先使用框架的支持,而不是自己创造

  • 例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法,造成内存泄漏

ThreadLocal在Spring中的实例分析

  • RequestContextHolder和DateTimeContextHolder中,看到里面使用了ThreadLocal
  • 每次HTTP请求都对应一个线程,线程之间相互隔离,这就是ThreadLocal的典型应用场景

参考: https://www.cnblogs.com/luxiaoxun/p/8744826.html

http://ifeve.com/tag/threadlocal/

上一篇:Java并发JUC——synchronized和Lock
下一篇:没有了
网友评论