ThreadLocal详解

123 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

ThreadLocal是一个将在多线程中为每一个线程创建单独的变量副本的类; 当使用ThreadLocal来维护变量时, ThreadLocal会为每个线程创建单独的变量副本, 避免因多线程操作共享变量而导致的数据不一致的情况。如下图所示:从用户登录,在ThreadLocal中放入UserInfo,在后续的代码中可获取UserInfo

一、什么是ThreadLocal

本文基于Jdk1.8

首先看下jdk官方文档对threadLocal的描述

大概的意思这个类提供线程的局部变量,每个线程可通过get()或set方法设置或获取线程中的变量副本,不会和其他线程的局部变量冲突,线程与线程之间变量是隔离的。

在ThreadLocal中设置的变量属于当前线程,别的线程访问不到。ThreadLocal底层是通过ThreadLocalMap实现,每个Thread对象中都存在一个ThreadLocalMap,Map的Key为ThreadLocal对象,value为需要缓存的数据,比如文章开头所说的UserInfo信息。

二、.使用场景

2.1、管理数据库连接

Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。主要代码在 TransactionSynchronizationManager 这个类中

private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);

 private static final ThreadLocal<Map<Object, Object>> resources =
   new NamedThreadLocal<>("Transactional resources");

 private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
   new NamedThreadLocal<>("Transaction synchronizations");

 private static final ThreadLocal<String> currentTransactionName =
   new NamedThreadLocal<>("Current transaction name");

2.2、避免参数传递

从系统入口登录后,有很多后台方法要用到用户信息,如果把用户信息进行层层传递,或者每次都从session(前后端分离的项目可能不用session)中获取用户信息都,相当麻烦。可以在用户登录认证成功后,把用户信息存储到ThreadLocal中,当在后续用到用户信息时可从ThreadLocal中获取。如以下代码所示:

public class ThreadLocalTest {
    ThreadLocal<User> threadLocal = ThreadLocal.withInitial(
            () -> new User() ) ;

    public void UserLogin(User user){
        //如果认证成功
        threadLocal.set(user);
    }
    /**
     * @author guoyong
     * @date 获取用户菜单
     * @param
     * @return void
     **/
    public void getUserMenu(){
        User user =threadLocal.get();
        //根据用户ID获取菜单信息
    }
}

2.3、使用共享变量

每个线程都需要一个独享的对象(比如工具类,典型的就是SimpleDateFormate,每次使用都new一个对象造成资源浪费,直接放到成员变量里又是线程不安全,所以把他用ThreadLocal管理起来就很合适。)

public class ThreadLocalTest {

    public static String dateToStr(int millisSeconds) {
        Date date = new Date(millisSeconds);
        SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return simpleDateFormat.format(date);
    }

    private static final ExecutorService executorService = Executors.newFixedThreadPool(100);

    public static void main(String[] args) {
        for (int i = 0; i < 3000; i++) {
            int j = i;
            executorService.execute(() -> {
                String date = dateToStr(j * 1000);
                // 从结果中可以看出是线程安全的,时间没有重复的。
                System.out.println(date);
            });
        }
        executorService.shutdown();
    }
}

class ThreadSafeFormatter {
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
           ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
    };

}

三、ThreadLocal原理

首先看下ThreadLocal的类图结构

Thread类中有两个ThreadLocalMap类型的变量分别是threadLocals和inheritableThreadLocals,ThreadLocalMap是类似与map一样的<key,value>

格式的存储对象。默认每个线程中这个两个变量都为null,只有当线程第一次调用了ThreadLocal的set或者get方法时候才会进行创建。实际每个线程的本地变量不是存放到ThreadLocal实例里面的,而是存放到调用线程的threadLocals变量里面。也就是说ThreadLocal类型的本地变量是存放到具体的线程内存空间的。ThreadLocal就是一个工具壳,它通过set方法把value值放入调用线程的threadLocals里面,当调用线程调用它的get方法时候再从当前线程的threadLocals变量里面拿出来使用。如果调用线程一直不终止那么这个本地变量会一直存放到调用线程的threadLocals变量里面,所以当不需要使用本地变量时候可以通过调用ThreadLocal变量的remove方法,从当前线程的threadLocals里面删除该本地变量。

下面分析下ThreadLocal的set、get、remove方法

3.1set方法

public void set(T value) {
        //(1)获取当前线程
        Thread t = Thread.currentThread();
        //(2)当前线程作为key,去查找对应的线程变量threadLocals,找到则设置
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
        //(3)第一次调用则创建当前线程对应的ThreadLocalMap
            createMap(t, value);
    }
//对象上述代码第二步
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
//创建当前线程的threadLocals
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

3.2get方法

public T get() {
        //(1) 获取当前线程
        Thread t = Thread.currentThread();
        //(2)获取当前线程的threadLocals变量
        ThreadLocalMap map = getMap(t);
        //(3)如果threadLocals不为null,则返回对应本地变量值
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //(4)threadLocals为空则初始化当前线程的threadLocals成员变量
                return setInitialValue();
    }

如上代码(1)首先获取当前线程实例,如果当前线程的threadLocals变量不为null,则直接返回当前线程绑定的本地变量。否者执行代码(4)进行初始化,setInitialValue()的代码如下:

private T setInitialValue() {
        //(5)初始化为null
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //(6)如果当前线程的threadLocals变量不为空
        if (map != null)
            map.set(this, value);
        else
        //(7)如果当前线程的threadLocals变量为空
            createMap(t, value);
        return value;
    }

   protected T initialValue() {
        return null;
    }

如上代码如果当前线程的的threadLocals变量不为空则设置当前线程的本地变量值为null,否者调用createMap创建当前线程的createMap变量。

3.3 remove方法

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

如上代码,如果当前线程的 threadLocals变量不为空,则删除当前线程中指定ThreadLocal实例的本地变量。

四、内存泄漏

4.1 内存溢出

Memory overflow:内存溢出,没有足够的内存提供申请者使用。

4.2 内存泄露

Memory leak:内存泄漏,程序申请内存后,无法释放已申请的内存空间,内存泄漏的堆积终将导致内存溢出。

显然是TreadLocal在使用不规范的情况下导致了内存没有释放。

先介绍下的Java引用类型

类型回收时间使用场景
强引用一直存货,除非GC Roots不可达new的基本对象、自定义对象。使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象
软引用内存不足时会被回收一般用在对内存非常敏感的资源上,用作缓存的场景较多
弱引用只能存活到下一次GC前生命周期很短的对象,列如ThreadLocal中的key。JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。
虚引用随时会被回收,创建了很快就会被回收

4.3 内存泄漏的原因

ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本。这些对象之间的引用关系如下:

从上图中可以看出,hreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。

但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

永远无法回收,造成内存泄漏。

4.4总结

由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。

但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

五、最佳实践

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据;
  • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。