关于threadlocal:知道ThreadLocal吗一起聊聊到底有啥用

43次阅读

共计 7737 个字符,预计需要花费 20 分钟才能阅读完成。

摘要:ThreadLocal 是 java 提供的一个不便对象在本线程内不同办法中传递和获取的类。用它定义的变量,仅在本线程中可见和保护,不受其余线程的影响,与其余线程互相隔离。

本文分享自华为云社区《ThreadLocal:线程专属的变量》,作者:zuozewei。

ThreadLocal 简介

ThreadLocal 是 java 提供的一个不便对象在本线程内不同办法中传递和获取的类。 用它定义的变量,仅在本线程中可见和保护,不受其余线程的影响,与其余线程互相隔离。

那 ThreadLocal 到底解决了什么问题,又实用于什么样的场景?

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. 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).
Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

外围意思是

ThreadLocal 提供了线程本地的实例。它与一般变量的区别在于,每个应用该变量的线程都会初始化一个齐全独立的实例正本。ThreadLocal 变量通常被 private static 润饰。当一个线程完结时,它所应用的所有 ThreadLocal 绝对的实例正本都可被回收。

总的来说,ThreadLocal 实用于每个线程须要本人独立的实例且该实例须要在多个办法中被应用,也即变量在线程间隔离而在办法或类间共享的场景 。后文会通过实例具体论述该观点。另外,该场景下,并非必须应用 ThreadLocal,其它形式齐全能够实现同样的成果,只是 ThreadLocal 使得实现更简洁。

ThreadLocal 应用

ThreadLocal 通过 set 办法能够给变量赋值,通过 get 办法获取变量的值。当然,也能够在定义变量时通过 ThreadLocal.withInitial 办法给变量赋初始值,或者定义一个继承 ThreadLocal 的类,而后重写 initialValue 办法。

上面通过如下代码阐明 ThreadLocal 的应用形式:

public class TestThreadLocal
{private static ThreadLocal<StringBuilder> builder = ThreadLocal.withInitial(StringBuilder::new);

    public static void main(String[] args)
    {for (int i = 0; i < 5; i++)
        {new Thread(() -> {String threadName = Thread.currentThread().getName();
                for (int j = 0; j < 3; j++)
                {append(j);
                    System.out.printf("%s append %d, now builder value is %s, ThreadLocal instance hashcode is %d, ThreadLocal instance mapping value hashcode is %d\n", threadName, j, builder.get().toString(), builder.hashCode(), builder.get().hashCode());
                }

                change();
                System.out.printf("%s set new stringbuilder, now builder value is %s, ThreadLocal instance hashcode is %d, ThreadLocal instance mapping value hashcode is %d\n", threadName, builder.get().toString(), builder.hashCode(), builder.get().hashCode());
            }, "thread-" + i).start();}
    }

    private static void append(int num) {builder.get().append(num);
    }

    private static void change() {StringBuilder newStringBuilder = new StringBuilder("HelloWorld");
        builder.set(newStringBuilder);
    }
}

在例子中,定义了一个 builder 的 ThreadLocal 对象,而后启动 5 个线程,别离对 builder 对象进行拜访和批改操作,这两个操作放在两个不同的函数 append、change 中进行,两个函数拜访 builder 对象也是间接获取,而不是放入函数的入参中传递进来。

代码输入如下:

thread-0 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 566157654
thread-0 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 566157654
thread-4 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 654647086
thread-3 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1803363945
thread-2 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1535812498
thread-1 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 2075237830
thread-2 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1535812498
thread-3 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1803363945
thread-4 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 654647086
thread-0 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 566157654
thread-0 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1773033190
thread-4 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 654647086
thread-4 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 700642750
thread-3 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1803363945
thread-3 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1706743158
thread-2 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1535812498
thread-2 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1431127699
thread-1 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 2075237830
thread-1 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 2075237830
thread-1 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1970695360
  • 从输入中 1~6 行能够看出,不同线程拜访的是同一个 builder 对象(不同线程输入的 ThreadLocal instance hashcode 值雷同),然而每个线程取得的 builder 对象存储的实例 StringBuilder 不同(不同线程输入的 ThreadLocal instance mapping value hashcode 值不雷同)。
  • 从输入中 1~2、9~10 行能够看出,同一个线程中批改 builder 对象存储的实例的值时,并不会影响到其余线程的 builder 对象存储的实例(thread-4 线程扭转存储的 StringBuilder 的值并不会引起 thread-0 线程的 ThreadLocal instance mapping value hashcode 值产生扭转)
  • 从输入中 9~13 行能够看出,一个线程对 ThreadLocal 对象存储的值产生扭转时,并不会影响其余的线程(thread-0 线程调用 set 办法扭转本线程 ThreadLocal 存储的对象值,本线程的 ThreadLocal instance mapping value hashcode 产生扭转,然而 thread-4 的 ThreadLocal instance mapping value hashcode 并没有因而扭转)。

ThreadLocal 原理

ThreadLocal 能在每个线程间进行隔离, 其次要是靠在每个 Thread 对象中保护一个 ThreadLocalMap 来实现的。 因为是线程中的对象,所以对其余线程不可见,从而达到隔离的目标。那为什么是一个 Map 构造呢。次要是因为一个线程中可能有多个 ThreadLocal 对象,这就须要一个汇合来进行存储辨别,而用 Map 能够更快地查找到相干的对象。
ThreadLocalMap 是 ThreadLocal 对象的一个动态外部类,外部保护一个 Entry 数组,实现相似 Map 的 get 和 put 等操作,为简略起见,能够将其看做是一个 Map,其中 key 是 ThreadLocal 实例,value 是 ThreadLocal 实例对象存储的值。

ThreadLocal 实用场景

如上文所述,ThreadLocal 实用于如下场景:

  • 每个线程须要有本人独自的实例,如实现每个线程单例类或每个线程上下文信息(例如事务 ID)。
  • ThreadLocal 实用于变量在线程间隔离且在办法间共享的场景,提供了另一种扩大 Thread 的办法。如果要保留信息或将信息从一个办法调用传递到另一个办法,则能够应用 ThreadLocal 进行传递。
  • 因为不须要批改任何办法,因而能够提供极大的灵活性。

案例一

这里一个解决 flag 的类,通过 ThreadLocal 应用,能够保障每个申请都领有惟一的一个追踪标记。

public class TestFlagHolder {private final static ThreadLocal<String> TEST_FLAG = new ThreadLocal<>();

  public static void set(String value) {TEST_FLAG.set(value);
  }

  public static String get() {return TEST_FLAG.get();
  }

  public static String get4log() {if (TEST_FLAG.get() == null) {return "-";}
    return TEST_FLAG.get();}

  public static void remove() {TEST_FLAG.remove();
  }

}

案例二

在同一线程中 trace 信息的传递:

ThreadLocal<String> traceContext = new ThreadLocal<>();

String traceId = Tracer.startServer();
traceContext.set(traceId) // 生成 trace 信息 传入 threadlocal
...
Tracer.startClient(traceContext.get()); // 从 threadlocal 获取 trace 信息
Tracer.endClient();
...
Tracer.endServer();

案例三

给同一个申请的每一行日志减少一个雷同的标记。这样,只有拿到这个标记就能够查问到这个申请链路上所有步骤的耗时了,咱们把这个标记叫做 requestId,咱们能够在程序的入口处生成一个 requestId,而后把它放在线程的上下文中,这样就能够在须要时随时从线程上下文中获取到 requestId 了。

简略的代码实现就像上面这样:

String requestId = UUID.randomUUID().toString();
ThreadLocal<String> tl = new ThreadLocal<String>(){
    @Override
    protected String initialValue() {return requestId;}
}; //requestId 存储在线程上下文中
long start = System.currentTimeMillis();
processA();
Logs.info("rid :" + tl.get() + ", process A cost" + (System.currentTimeMillis() - start)); // 日志中减少 requestId
start = System.currentTimeMillis();
processB();
Logs.info("rid :" + tl.get() + ", process B cost" + (System.currentTimeMillis() - start));
start = System.currentTimeMillis();
processC();
Logs.info("rid :" + tl.get() + ", process C cost" + (System.currentTimeMillis() - start));

有了 requestId,你就能够清晰地理解一个调用链路上的耗时散布状况了。

小结

无论是单体零碎还是微服务化架构,无论是链路打标还是服务追踪,你都须要在零碎中减少 TraceId,这样能够将你的链路串起来,给你出现一个残缺的问题场景。如果 TraceId 能够在客户端上生成,在申请业务接口的时候传递给服务端,那么就能够把客户端的日志体系也整合进来,对于问题的排查帮忙更大。

同时,在全链路压测框架中,Trace 信息的传递性能是基于 ThreadLocal 的。但理论业务中可能会应用异步调用,这样就会失落 Trace 信息,毁坏了链路的完整性。 所以在理论的我的项目倡议大家不要轻意应用 ThreadLocal。

参考资料:

[1]:《高并发零碎设计 40 问》
[2]:https://juejin.cn/post/684490…

点击关注,第一工夫理解华为云陈腐技术~

正文完
 0