• ThreadLocal详细解读


    前言

    ThreadLocal是Java中的线程局部变量

    官方解释如下

    This class provides thread-local variables.
    These variables differ from their normal counterparts in that each thread
    that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable.
    {@code ThreadLocal} instances are typically private static fields in classes
    that wish to associate state with a thread (e.g.,a user ID or Transaction ID)

     意思是每一个线程都有自己的独立初始化的变量副本。

    实现原理

    ThreadLocal可以看作是一个容器,容器里面存放了属于当前线程的变量。

    ThreadLocal类提供了四个对外开放的接口方法,这也是用户操作ThreadLocal类的基本方法: 
           (1) void set(Object value)设置当前线程的线程局部变量的值。 
           (2) public Object get()该方法返回当前线程所对应的线程局部变量。 
           (3) public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。 
           (4) protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次,ThreadLocal中的缺省实现直接返回一个null。

    可以通过上述的几个方法实现ThreadLocal中变量的访问,数据设置,初始化以及删除局部变量

    例子:

    1. public class TestNum {
    2. // ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
    3. private static ThreadLocal seqNum = new ThreadLocal() {
    4. public Integer initialValue() {
    5. return 0;
    6. }
    7. };
    8. // ②获取下一个序列值
    9. public int getNextNum() {
    10. seqNum.set(seqNum.get() + 1);
    11. return seqNum.get();
    12. }
    13. public static void main(String[] args) {
    14. TestNum sn = new TestNum();
    15. // ③ 3个线程共享sn,各自产生序列号
    16. TestClient t1 = new TestClient(sn);
    17. TestClient t2 = new TestClient(sn);
    18. TestClient t3 = new TestClient(sn);
    19. t1.start();
    20. t2.start();
    21. t3.start();
    22. }
    23. private static class TestClient extends Thread {
    24. private TestNum sn;
    25. public TestClient(TestNum sn) {
    26. this.sn = sn;
    27. }
    28. public void run() {
    29. for (int i = 0; i < 3; i++) {
    30. // ④每个线程打出3个序列值
    31. System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["
    32. + sn.getNextNum() + "]");
    33. }
    34. }
    35. }
    36. }

     代码运行结果

    thread[Thread-0] --> sn[1]
    thread[Thread-1] --> sn[1]
    thread[Thread-2] --> sn[1]
    thread[Thread-1] --> sn[2]
    thread[Thread-0] --> sn[2]
    thread[Thread-1] --> sn[3]
    thread[Thread-2] --> sn[2]
    thread[Thread-0] --> sn[3]
    thread[Thread-2] --> sn[3]

    可以看出,变量的值在各个线程之间是独立的,不会互相干扰。这是因为我们通过ThreadLocal为每一个线程提供了单独的副本。

    ThreadLocal内部是如何为每一个线程维持一个变量副本呢?

    其实在Thread类中维护了一个ThreadLocalMap对象threadLocals(ThreadLocalMap是ThreadLocal类中的一个静态内部类),该Map的Key是当前ThreadLocal对象,Value是对应线程的局部变量副本,每个线程可能存在多个ThreadLocal

    ThreadLocal的部分get/set代码如下

    1. /**
    2. * Returns the value in the current thread's copy of this
    3. * thread-local variable. If the variable has no value for the
    4. * current thread, it is first initialized to the value returned
    5. * by an invocation of the {@link #initialValue} method.
    6. *
    7. * @return the current thread's value of this thread-local
    8. */
    9. public T get() {
    10. Thread t = Thread.currentThread();
    11. ThreadLocalMap map = getMap(t);
    12. if (map != null) {
    13. ThreadLocalMap.Entry e = map.getEntry(this);
    14. if (e != null)
    15. return (T)e.value;
    16. }
    17. return setInitialValue();
    18. }
    19. /**
    20. * Get the map associated with a ThreadLocal. Overridden in
    21. * InheritableThreadLocal.
    22. *
    23. * @param t the current thread
    24. * @return the map
    25. */
    26. ThreadLocalMap getMap(Thread t) {
    27. return t.threadLocals;
    28. }
    29. /**
    30. * Sets the current thread's copy of this thread-local variable
    31. * to the specified value. Most subclasses will have no need to
    32. * override this method, relying solely on the {@link #initialValue}
    33. * method to set the values of thread-locals.
    34. *
    35. * @param value the value to be stored in the current thread's copy of
    36. * this thread-local.
    37. */
    38. public void set(T value) {
    39. Thread t = Thread.currentThread();
    40. ThreadLocalMap map = getMap(t);
    41. if (map != null)
    42. map.set(this, value);
    43. else
    44. createMap(t, value);
    45. }

    ThreadLocal 是否会存在内存泄漏呢? 

           ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
    ThreadLocal Ref -> Thread -> ThreaLocalMap -> Entry -> value
    永远无法回收,造成内存泄露。

           在JDK的ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施,下面是ThreadLocalMap的getEntry方法的源码: 

    1. private Entry getEntry(ThreadLocal key) {
    2. int i = key.threadLocalHashCode & (table.length - 1);
    3. Entry e = table[i];
    4. if (e != null && e.get() == key)
    5. return e;
    6. else
    7. return getEntryAfterMiss(key, i, e);
    8. }
    9. private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
    10. Entry[] tab = table;
    11. int len = tab.length;
    12. while (e != null) {
    13. ThreadLocal k = e.get();
    14. if (k == key)
    15. return e;
    16. if (k == null)
    17. expungeStaleEntry(i);
    18. else
    19. i = nextIndex(i, len);
    20. e = tab[i];
    21. }
    22. return null;
    23. }

    整理一下ThreadLocalMap的getEntry函数的流程:

    1. 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (len-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
    1. 如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry,否则,如果key值为null,则擦除该位置的Entry,否则继续向下一个位置查询

    在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。仔细研究代码可以发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。

    但是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的genEntry函数或者set函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。

    使用方法

    线程上下文对象,用作存放线程局部变量,为每个线程存放一些特定的数据

    1、编写ContextManager,实现线程上下文统一管理。

    a、声明ThreadLocal对象

    Context为具体的局部变量对象

    private static final ThreadLocal THREAD_LOCAL_CONTEXT = new ThreadLocal<>();

    b、编写设置上下文对象的方法bindContext(args...)

    在此方法中设置上下文局部变量的数据,并通过setContext()(自编)方法放入到ThreadLocal对象中。

    c、编写获取上下文对象(getContext())、设置上下文对象(setContext(Context context))、清理上下文对象(clearContext())的方法,在对应的方法中直接调用ThreadLocal对象自带的对应方法(get()、set()、remove())即可。

    之后,在合适的地方直接使用ContextManager对象及其中的方法即可。

  • 相关阅读:
    Promise
    『现学现忘』Docker基础 — 40、发布镜像到Docker Hub
    Java基础入门1-2
    MyBatis框架-缓存
    RNA核糖核酸修饰Alexa 568/RNA-Alexa@ 594/RNA-Alexa@ 610/RNA-Alexa 633荧光染料
    Java分别用BIO、NIO实现简单的客户端服务器通信
    1757. 可回收且低脂的产品
    MySQL案例详解 三:MMM高可用架构及其故障切换
    π160E60 Pai160E60 5.0kVrms 200Mbps 六通道数字隔离器芯片代替Si8660BD-B-IS
    python期末复习案例
  • 原文地址:https://blog.csdn.net/weixin_52457624/article/details/133795349