史上最全ThreadLocal 详解

骑猪看日落 2024-05-23 22:16 166阅读 0赞

史上最全ThreadLocal 详解

ThreadLocal简介

  • ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
  • ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

    • 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
    • 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
  • ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
  • 总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景
  • 下图可以增强理解:
  • 在这里插入图片描述

ThreadLocal与Synchronized的区别

  • ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。
  • 但是ThreadLocal与synchronized有本质的区别:
  • Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
  • Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
  • 一句话理解ThreadLocal,threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。

ThreadLocal的简单使用

  • 直接上代码:
  • public class ThreadLocaDemo {

    1. private static ThreadLocal<String> localVar = new ThreadLocal<String>();
    2. static void print(String str) {
    3. //打印当前线程中本地内存中本地变量的值
    4. System.out.println(str + " :" + localVar.get());
    5. //清除本地内存中的本地变量
    6. localVar.remove();
    7. }
    8. public static void main(String[] args) throws InterruptedException {
    9. new Thread(new Runnable() {
    10. public void run() {
    11. ThreadLocaDemo.localVar.set("local_A");
    12. print("A");
    13. //打印本地变量
    14. System.out.println("after remove : " + localVar.get());
    15. }
    16. },"A").start();
    17. Thread.sleep(1000);
    18. new Thread(new Runnable() {
    19. public void run() {
    20. ThreadLocaDemo.localVar.set("local_B");
    21. print("B");
    22. System.out.println("after remove : " + localVar.get());
    23. }
    24. },"B").start();
    25. }

    }

    A :local_A
    after remove : null
    B :local_B
    after remove : null

  • 从这个示例中我们可以看到,两个线程分表获取了自己线程存放的变量,他们之间变量的获取并不会错乱。这个的理解也可以结合图1-1,相信会有一个更深刻的理解。

ThreadLocal的原理

要看原理那么就得从源码看起。

ThreadLocal的set()方法:

  • public void set(T value) {
    1. //1、获取当前线程
    2. Thread t = Thread.currentThread();
    3. //2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
    4. //则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
    5. ThreadLocalMap map = getMap(t);
    6. if (map != null)
    7. map.set(this, value);
    8. else
    9. // 初始化thradLocalMap 并赋值
    10. createMap(t, value);
    11. }
  • 从上面的代码可以看出,ThreadLocal set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。
  • 那么ThreadLocalMap又是什么呢,还有createMap又是怎么做的,我们继续往下看。大家最后自己再idea上跟下源码,会有更深的认识。
  • static class ThreadLocalMap {

    1. /**
    2. * The entries in this hash map extend WeakReference, using
    3. * its main ref field as the key (which is always a
    4. * ThreadLocal object). Note that null keys (i.e. entry.get()
    5. * == null) mean that the key is no longer referenced, so the
    6. * entry can be expunged from table. Such entries are referred to
    7. * as "stale entries" in the code that follows.
    8. */
    9. static class Entry extends WeakReference<ThreadLocal<?>> {
    10. /** The value associated with this ThreadLocal. */
    11. Object value;
    12. Entry(ThreadLocal<?> k, Object v) {
    13. super(k);
    14. value = v;
    15. }
    16. }
    17. }
  • 可看出ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。详细内容要大家自己去跟。
  • //这个是threadlocal 的内部方法
    void createMap(Thread t, T firstValue) {
    1. t.threadLocals = new ThreadLocalMap(this, firstValue);
    2. }
  1. //ThreadLocalMap 构造方法
  2. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  3. table = new Entry[INITIAL_CAPACITY];
  4. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  5. table[i] = new Entry(firstKey, firstValue);
  6. size = 1;
  7. setThreshold(INITIAL_CAPACITY);
  8. }

ThreadLocal的get方法

  • public T get() {
    1. //1、获取当前线程
    2. Thread t = Thread.currentThread();
    3. //2、获取当前线程的ThreadLocalMap
    4. ThreadLocalMap map = getMap(t);
    5. //3、如果map数据不为空,
    6. if (map != null) {
    7. //3.1、获取threalLocalMap中存储的值
    8. ThreadLocalMap.Entry e = map.getEntry(this);
    9. if (e != null) {
    10. @SuppressWarnings("unchecked")
    11. T result = (T)e.value;
    12. return result;
    13. }
    14. }
    15. //如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
    16. return setInitialValue();
    17. }
  1. private T setInitialValue() {
  2. T value = initialValue();
  3. Thread t = Thread.currentThread();
  4. ThreadLocalMap map = getMap(t);
  5. if (map != null)
  6. map.set(this, value);
  7. else
  8. createMap(t, value);
  9. return value;
  10. }

ThreadLocal的remove方法

  • public void remove() {
    1. ThreadLocalMap m = getMap(Thread.currentThread());
    2. if (m != null)
    3. m.remove(this);
    4. }
  • remove方法,直接将ThrealLocal 对应的值从当前相差Thread中的ThreadLocalMap中删除。为什么要删除,这涉及到内存泄露的问题。
  • 实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
  • 所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。
  • ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。举个例字,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。

ThreadLocal与Thread,ThreadLocalMap之间的关系

  • 在这里插入图片描述
  • 在这里插入图片描述
  • 从这个图中我们可以非常直观的看出,ThreadLocalMap其实是Thread线程的一个属性值,而ThreadLocal是维护ThreadLocalMap
  • 这个属性指的一个工具类。Thread线程可以拥有多个ThreadLocal维护的自己线程独享的共享变量(这个共享变量只是针对自己线程里面共享)

ThreadLocal 常见使用场景

  • 如上文所述,ThreadLocal 适用于如下两种场景
  • 每个线程需要有自己单独的实例
  • 实例需要在多个方法中共享,但不希望被多线程共享
场景
  • 存储用户Session
  • 一个简单的用ThreadLocal来存储Session的例子:
  • private static final ThreadLocal threadSession = new ThreadLocal();

    1. public static Session getSession() throws InfrastructureException {
    2. Session s = (Session) threadSession.get();
    3. try {
    4. if (s == null) {
    5. s = getSessionFactory().openSession();
    6. threadSession.set(s);
    7. }
    8. } catch (HibernateException ex) {
    9. throw new InfrastructureException(ex);
    10. }
    11. return s;
    12. }
  • 数据库连接,处理数据库事务
  • 数据跨层传递(controller,service, dao)
  • 每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。
  • 例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。
  • 在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。
  • 比如说我们是一个用户系统,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就会依次调用service-1()、service-2()、service-3()、service-4(),这4个方法可能是分布在不同的类中的。这个例子和存储session有些像。
  • package com.kong.threadlocal;
  1. public class ThreadLocalDemo05 {
  2. public static void main(String[] args) {
  3. User user = new User("jack");
  4. new Service1().service1(user);
  5. }
  6. }
  7. class Service1 {
  8. public void service1(User user){
  9. //给ThreadLocal赋值,后续的服务直接通过ThreadLocal获取就行了。
  10. UserContextHolder.holder.set(user);
  11. new Service2().service2();
  12. }
  13. }
  14. class Service2 {
  15. public void service2(){
  16. User user = UserContextHolder.holder.get();
  17. System.out.println("service2拿到的用户:"+user.name);
  18. new Service3().service3();
  19. }
  20. }
  21. class Service3 {
  22. public void service3(){
  23. User user = UserContextHolder.holder.get();
  24. System.out.println("service3拿到的用户:"+user.name);
  25. //在整个流程执行完毕后,一定要执行remove
  26. UserContextHolder.holder.remove();
  27. }
  28. }
  29. class UserContextHolder {
  30. //创建ThreadLocal保存User对象
  31. public static ThreadLocal<User> holder = new ThreadLocal<>();
  32. }
  33. class User {
  34. String name;
  35. public User(String name){
  36. this.name = name;
  37. }
  38. }
  39. 执行的结果:
  40. service2拿到的用户:jack
  41. service3拿到的用户:jack
  • Spring使用ThreadLocal解决线程安全问题
  • 我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。
  • 一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9-2所示。
  • img
  • 这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。
  • 下面的实例能够体现Spring对有状态Bean的改造思路:
  • TopicDao:非线程安全
  • public class TopicDao {
    //①一个非线程安全的变量
    private Connection conn;
    public void addTopic(){
    1. //②引用非线程安全变量
    2. Statement stat = conn.createStatement();
    }
  • 由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:
  • 线程安全
  • import java.sql.Connection;
    import java.sql.Statement;
    public class TopicDao {

    //①使用ThreadLocal保存Connection变量
    private static ThreadLocal connThreadLocal = new ThreadLocal();
    public static Connection getConnection(){

    1. //②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,
    2. //并将其保存到线程本地变量中。

    if (connThreadLocal.get() == null) {

    1. Connection conn = ConnectionManager.getConnection();
    2. connThreadLocal.set(conn);
    3. return conn;
    4. }else{
    5. //③直接返回线程本地变量
    6. return connThreadLocal.get();
    7. }
    8. }
    9. public void addTopic() {
    10. //④从ThreadLocal中获取线程对应的
    11. Statement stat = getConnection().createStatement();
    12. }
  • 不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否为null,如果为null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其他线程的Connection。因此,这个TopicDao就可以做到singleton共享了。
  • 当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在Dao只能做到本Dao的多个方法共享Connection时不发生线程安全问题,但无法和其他Dao共用同一个Connection,要做到同一事务多Dao共享同一个Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。但这个实例基本上说明了Spring对有状态类线程安全化的解决思路。在本章后面的内容中,我们将详细说明Spring如何通过ThreadLocal解决事务管理的问题。

发表评论

表情:
评论列表 (有 0 条评论,166人围观)

还没有评论,来说两句吧...

相关阅读

    相关 ByteBuddy(

    ByteBuddy(史上最全) 文章很长,建议收藏起来慢慢读! [总目录 博客园版][Link 1] 为大家准备了更多的好文章!!!! 推荐:尼恩Java面试宝典(持

    相关 正则

    [一个正则表达式测试(只可输入中文、字母和数字)][Link 1] 在项目中碰到了正则表达式的运用,正则还是非常强大的,不管什么编程语言,基本上都可以用到。之前在用jav