• ThreadLocal笔记


    并发的场景中,如果有多个线程同时修改公共变量,可能会出现线程安全问题,即该变量最终结果可能出现异常。
    如果使用锁来保证资源隔离,会存在大量锁等待,会让响应时间延长很多。
    ThreadLocal的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。

    ThreadLoacl原理

    public class ThreadLocal<T> {
         ...
         public T get() {
            //获取当前线程
            Thread t = Thread.currentThread();
            //获取成员变量ThreadLocalMap对象
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                //根据threadLocal对象从map中获取Entry对象
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    //获取保存的数据
                    T result = (T)e.value;
                    return result;
                }
            }
            //初始化数据
            return setInitialValue();
        }
        
        private T setInitialValue() {
            //获取要初始化的数据
            T value = initialValue();
            //获取当前线程
            Thread t = Thread.currentThread();
            //获取成员变量ThreadLocalMap对象
            ThreadLocalMap map = getMap(t);
            //如果map不为空
            if (map != null)
                //将初始值设置到map中,key是this,即threadLocal对象,value是初始值
                map.set(this, value);
            else
               //如果map为空,则需要创建新的map对象
                createMap(t, value);
            return value;
        }
        
        public void set(T value) {
            //获取当前线程
            Thread t = Thread.currentThread();
            //获取成员变量ThreadLocalMap对象
            ThreadLocalMap map = getMap(t);
            //如果map不为空
            if (map != null)
                //将值设置到map中,key是this,即threadLocal对象,value是传入的value值
                map.set(this, value);
            else
               //如果map为空,则需要创建新的map对象
                createMap(t, value);
        }
        
         static class ThreadLocalMap {
            ...
         }
         ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    ThreadLocal的get方法、set方法和setInitialValue方法,其实最终操作的都是ThreadLocalMap类中的数据。

    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
    
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
       }
       ...
       private Entry[] table;
       ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    ThreadLocalMap里面包含一个静态的内部类Entry,该类继承于WeakReference类,说明Entry是一个弱引用。

    ThreadLocalMap内部还包含了一个Entry数组,其中:Entry = ThreadLocal + value。

    而ThreadLocalMap被定义成了Thread类的成员变量。

    public class Thread implements Runnable {
        ...
        ThreadLocal.ThreadLocalMap threadLocals = null;
    }
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述

    Entry是由threadLocal和value组成,其中threadLocal对象是弱引用,在GC的时候,会被自动回收。

    ThreadLocalMap的Entry为什么用ThreadLocal做key?

    如果一个线程只使用一个ThreadLocal对象,使用Thread做key是可以的。但是一个线程中不可能只使用一个ThreadLocal对象,再使用Thread做key就有问题

    Entry的key为什么设计成弱引用?

    弱引用的对象,在GC做垃圾清理的时候,就会被自动回收了。

    如果key是弱引用,当ThreadLocal变量指向null之后,在GC做垃圾清理的时候,key会被自动回收,其值也被设置成null。

    线程很多情况下是复用的,一个线程执行多个任务。一个任务执行完如果不把key设置为nul会一直存在,从而造成内存泄漏。

    Entry的value为什么不设计成弱引用?

    Entry的value假如只是被Entry引用,有可能没被其他地方引用。如果将value改成了弱引用,被GC贸然回收了(数据突然没了),可能会导致出现异常。

    ThreadLocal真的会导致内存泄露?

    强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> value -> Object

    若Thread为工作线程,长期存在,会导致内存泄漏

    如何解决内存泄露问题?

    在finally代码块中,调用remove方法清理没用的数据,remove方法中会把Entry中的key和value都设置成null,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。

    父子线程如何共享数据?

    ThreadLocal都是在一个线程中保存和获取数据的。

     @Test
        public void test1() {
            ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
            threadLocal.set(6);
            System.out.println("父线程获取数据:" + threadLocal.get());
    
            new Thread(() -> {
                System.out.println("子线程获取数据:" + threadLocal.get());
            }).start();
        }
        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述
    使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类。

      @Test
        public void test2(){
            InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
            threadLocal.set(6);
            System.out.println("父线程获取数据:" + threadLocal.get());
    
            new Thread(() -> {
                System.out.println("子线程获取数据:" + threadLocal.get());
            }).start();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述
    InheritableThreadLocal的init方法中会将父线程中往ThreadLocal设置的值,拷贝一份到子线程中。

    线程池中如何共享数据?

       public void test3(){
            InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
            threadLocal.set(6);
            System.out.println("父线程获取数据:" + threadLocal.get());
    
            ExecutorService executorService = Executors.newSingleThreadExecutor();
    
            threadLocal.set(6);
            executorService.submit(() -> {
                System.out.println("第一次从线程池中获取数据:" + threadLocal.get());
            });
    
            threadLocal.set(7);
            executorService.submit(() -> {
                System.out.println("第二次从线程池中获取数据:" + threadLocal.get());
            });
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述

    由于这个例子中使用了单例线程池,固定线程数是1。

    第一次submit任务的时候,该线程池会自动创建一个线程。因为使用了InheritableThreadLocal,所以创建线程时,会调用它的init方法,将父线程中的inheritableThreadLocals数据复制到子线程中。在主线程中将数据设置成6,第一次从线程池中获取了正确的数据6。

    之后,在主线程中又将数据改成7,但在第二次从线程池中获取数据却依然是6。

    因为第二次submit任务的时候,线程池中已经有一个线程了,就直接拿过来复用,不会再重新创建线程了。所以不会再调用线程的init方法,所以第二次其实没有获取到最新的数据7,还是获取的老数据6。

    使用阿里巴巴的一个开源jar包

       <dependency>
                <groupId>com.alibabagroupId>
                <artifactId>transmittable-thread-localartifactId>
                <version>2.14.2version>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    @Test
        public void test4(){
            TransmittableThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();
            threadLocal.set(6);
            System.out.println("父线程获取数据:" + threadLocal.get());
    
            ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
    
            threadLocal.set(6);
            ttlExecutorService.submit(() -> {
                System.out.println("第一次从线程池中获取数据:" + threadLocal.get());
            });
    
            threadLocal.set(7);
            ttlExecutorService.submit(() -> {
                System.out.println("第二次从线程池中获取数据:" + threadLocal.get());
            });
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述

    ThreadLocal为什么建议用static修饰?

    static修饰的变量是在类在加载时就分配地址了,在类卸载才会被回收

    如果变量ThreadLocal是非static的就会造成每次生成实例都要生成不同的ThreadLocal对象,虽然这样程序不会有什么异常,但是会浪费内存资源

  • 相关阅读:
    Oracle + MyBatis 批量更新 update
    性能测试场景的设计方法
    设计模式——模板方法模式、策略模式(行为型模式)
    SBF浅谈FTX扩张计划,否认收购Coinbase传闻
    物联网AI MicroPython学习之语法 ucollections集合和容器类型
    HTTP状态码301和302的区别
    P4315 月下“毛景树”(树链剖分)
    国科大体系结构习题 | 第二章 计算机系统结构基础
    HTML5拖放(Drag and Drop)全面指南:让网页互动起来
    uniapp的扩展组件uni-popup 弹出层自动打开
  • 原文地址:https://blog.csdn.net/qq_23934475/article/details/128160908