ThreadLocal

发布时间 2024-01-12 19:38:49作者: wangzhilei

ThreadLocal类

ThreadLocal叫做线程变量,它是一个线程的本地变量,意味着这个变量是线程独有的,是不能与其他线程共享的。这样就可以避免资源竞争带来的多线程的问题。即 ThreadLocal类用来提供线程内部的局部变量,不同的线程之间不会相互干扰。

ThreadLocal类的常用方法

ThreadLocal threadLocal = new ThreadLocal();:创建ThreadLocal对象(即一个线程本地变量)。

initialValue():返回此线程局部变量的当前线程的"初始值" 。

  1. 它是一个延迟加载的方法,只有调用get时,才会触发。
  2. 当线程第一次使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法
  3. 通常,每个线程最多调用一次此方法,但如果已经调用了remove()后,再调用get(),则可以再次调用此方法
  4. 如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。

set(T value):将此线程局部变量的当前线程副本中的值设置为value。

get():返回此线程局部变量的当前线程副本中的值 。如首次调用,则会调用initialize来得到这个值

remove():移除当前线程绑定的局部变量,该方法可以帮助JVM进行GC

两大使用场景

1. 每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)

每个Thread内有自己的实例副本,不共享

SimpleDateFormat进化之路

  1)2个线程分别用自己的SimpleDateFormat,这没问题

  2)后来延伸出10个,那就有10个线程和10个SimpleDateFormat,这虽然写法不优雅(应该复用对象),但勉强可以接受

  3)但是当需求变成了1000个,那么必然要用线程池(否则消耗内存太多)

  4)所有的线程都共用同一个simpleDateFormat对象,加synchronized(性能损耗)

利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存

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 Supplier<SimpleDateFormat>() {
        @Override
        public SimpleDateFormat get() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    });
}        

 

  1. 每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦

保证线程安全,可用synchronized,ConcurrentHashMap,但都会对性能有所影响。

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

方法

  • 用ThreadLocal保存一些业务内容(用户权限信息、从用户系统获取到的用户名、user ID等)
  • 这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的
  • 在线程生命周期内,都通过这个静态ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象(例如user对象)作为参数传递的麻烦
  • 强调的是同一个请求内(同一个线程内)不同方法间的共享
  • 不需重写initialValue()方法,但是必须手动调用set()方法
/**
* 描述: 演示ThreadLocal用法2:避免传递参数的麻烦
*/
public class ThreadLocalNormalUsage06 {

  public static void main(String[] args) {
    new Service1().process("");
  }
}
class Service1 {   public void process(String name) {   User user = new User("超哥");   UserContextHolder.holder.set(user);   new Service2().process();   } }
class Service2 {   public void process() {     User user = UserContextHolder.holder.get();     ThreadSafeFormatter.dateFormatThreadLocal.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();   } }
class UserContextHolder {   public static ThreadLocal<User> holder = new ThreadLocal<>(); }

 

原理

ThreadLocalMap类,也就是Thread.threadLocals

ThreadLocalMap类是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[]table,可以认为是一个map,键值对:

  键:这个ThreadLocal

  值:实际需要的成员变量,比如user或者simpleDateFormat对象

内存泄露

Key的泄漏:

  ThreadLocalMap中的Entry继承自WeakReference,是弱引用,弱引用的特点是,如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收,所以弱引用不会阻止GC,因此这是弱引用的机制

value的泄露:

  ThreadLocalMap的每个Entry都是一个对key的弱引用,同时,每个Entry都包含了一个对value的强引用,正常情况下,当线程终止保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了,但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下的调用链:

    Thread→ThreadLocalMap → Entry(key为null )→Value

因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM

JDK已经考虑到了这个问题,所以在set,remove,rehash方法中会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以被回收

但是如果一个ThreadLocal不被使用,那么实际上set,remove,rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄漏

如何避免内存泄露(阿里规约)

调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal之后,应该调用remove方法

空指针异常

public class ThreadLocalNPE {

  ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>();

  public void set() {
    longThreadLocal.set(Thread.currentThread().getId());
  }

  public long get() {
    return longThreadLocal.get();
  }

  public static void main(String[] args) {
    ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
    System.out.println(threadLocalNPE.get()); //空指针异常,没有初始化和set
    Thread thread1 = new Thread(new Runnable() {
      @Override
      public void run() {
        threadLocalNPE.set();
        System.out.println(threadLocalNPE.get());
      }
    });
    thread1.start();
  }
}

 

共享对象

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

Spring应用

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

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

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

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

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