美文网首页java多线程
Java 多线程(四):ThreadLocal 详解

Java 多线程(四):ThreadLocal 详解

作者: 聪明的奇瑞 | 来源:发表于2018-03-12 00:53 被阅读47次

    ThreadLocal

    • 多线程在并发执行时,需要数据共享,因此才有了 volatile 变量解决多线程数据之间可见性的问题,也有了锁的同步机制,使变量或代码再某一时刻只能被一个线程访问,确保共享数据的正确性。但多线程并发执行时,并不是所有数据都是要共享的,这些不需要共享的数据让每个线程各自去维护就可以了,因此才有了 ThreadLocal
    • ThreadLocal(线程局部变量),其功能是为每一个使用该变量的线程提供一个变量值的副本,使它不会和其它线程的副本冲突,在线程结束后其副本会被垃圾回收。它为多线程并发访问提供了一种隔离机制
    • ThreadLocal 实例通常都是 private static 类型的,用于关联线程和线程上下文
    • ThreadLocal 类中有一个 Map,用于存储每一个线程的变量副本,从而做到为每一个线程维护变量副本,使得多个线程线程同时访问一份变量而不互相影响

    ThreaLocal 接口方法

    • set(T value):设置当前线程的线程局部变量的值
    • get():返回当前线程所对应的线程局部变量的值
    • remove():将当前线程局部变量的值删除(当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度)
    • initialValue():返回该线程局部变量的初始值,该方法是一个 protected 的方法,目的是为了让子类重写而设计的(这个方法是一个延迟调用方法,在线程第 1 次调用 get 或 set 时才执行,并且仅执行 1 次,缺省实现直接返回一个 null)

    ThreadLocal 用法

    public class TreadLocalTest {
    
        private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
    
        public static void main(String[] args) {
            for (int i = 0; i < 2; i++) {
                new Thread(new Runnable() {
                    public void run() {
                        int data = new Random().nextInt();
                        System.out.println(Thread.currentThread().getName() + " put data :" + data);
                        threadLocal.set(data);
    
                        new A().get();
                    }
                }).start();
            }
        }
    
        static class A {
            public void get() {
                int data = threadLocal.get();
                System.out.println(Thread.currentThread().getName() + " get data :" + data);
            }
        }
    }
    
    • 输出结果如下,可以看到 ThreadLocal 为线程之间提供了隔离机制,多线程虽然访问的是相同的 ThreadLocal 对象,但它们之间的线程局部变量都是独立的
    Thread-0 put data :-405727869
    Thread-1 put data :-2129414360
    Thread-0 get data :-405727869
    Thread-1 get data :-2129414360
    

    ThreadLocal 解决的问题

    • 解决并发问题:使用 ThreadLocal 代替 synchronized 来保证线程安全,同步机制采用了“以时间换空间”的方式,而 ThreadLocal 采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响
    • 解决数据存储问题:ThreadLocal 为变量在每个线程中都创建了一个副本,所以每个线程可以访问自己内部的副本变量,不同线程之间不会互相干扰

    Spring 与 ThreadLocal 解决线程安全问题

    • 在一般情况下,只有无状态的 Bean 才可以再多线程环境下共享
    • 但在 Spring 中绝大部分 Bean 都可以声明为 singleton(单例) 作用域,这是因为 Spring 对一些有"状态性对象"的 Bean 采用 ThreadLocal 进行处理,让它们成为线程安全的 "状态性对象",因此有状态的 Bean 才能以单例模式在多线程中正常工作
    • 它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题

    ThreadLocal 内存泄露问题

    • 当使用线程池来复用线程时,一个线程使用完后并不会销毁线程,那么分发的那个实例会一直绑定在这个线程上
    • 由于 Entry 继承了 ThreadLocal,而 ThreadLocal 使用 WeakReference 弱引用,并作为了 ThreadLocalMap 的 Entry 的 Key
    • 如果在某些时候 ThreadLocal 对象被赋 Null 的话,弱引用会被 GC 收集,这样就会导致 Entry 的 Value 对象无法获取到到
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
    
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    
    • 线程被复用后如果有调用 ThreadLocal.get/set 方法的话,方法里面会去做遍历清除那些以 [ThreadLocal=Null ] 为 Key 的 Entry
    • 但如果一直没调用 ThreadLocal.get/set 方法的话就会导致内存泄漏了
    • 所以一般使用完 ThreadLocal 后要调用 threadLocal.remove() 方法线程局部变量

    ThreadLocal 源码分析

    • 每个 Thread 维护一个 ThreadLocalMap 哈希表,这个哈希表的 key 是 ThreaLocal 实例本身(因为一个 Thread 可能有多个 ThreadLocal),value 才是真正要存储的值 Object

    get 方法

    • 获取当前线程的 Thread 对象,进而获取此线程对象中维护的 ThreadLocalMap 对象
    • 判断当前的 ThreadLocalMap 是否存在
      • 如果存在,则以当前的 ThreadLocal 为 key,调用 ThreadLocalMap 中的 getEntry 方法获取对应存储的键值对 e,再获取 e 中的 value 值并返回
      • 如果不存在,则证明此线程没有维护的 ThreadLocalMap 对象,调用 setInitialValue 方法进行初始化,返回 setInitialValue 初始化的值
        public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            return setInitialValue();
        }
        
        ThreadLocalMap getMap(Thread t) {
            // 获取当前线程Thread对应维护的ThreadLocalMap 
            return t.threadLocals;
        }
    

    setInitialValue 方法

    • 调用 initialValue 获取初始化的值
    • 获取当前线程 Thread 对象,进而获取此线程对象中维护的 ThreadLocalMap 对象
    • 判断当前的 ThreadLocalMap 是否存在
      • 如果存在,则调用 map.set 设置此实体 entry
      • 如果不存在,则调用 createMap 进行 ThreadLocalMap 对象的初始化,并将此实体 entry 作为第一个值存放至 ThreadLocalMap 中
        // 返回该线程局部变量的初始值,该方法是一个 protected 的方法,目的是为了让子类重写而设计的
        protected T initialValue() {
            return null;
        }
        
        private T setInitialValue() {
            T value = initialValue();
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
            return value;
        }
    

    set 方法

    • 获取当前线程 Thread 对象,进而获取此线程对象中维护的ThreadLocalMap对象
    • 判断当前的 ThreadLocalMap是否存在:
      • 如果存在,则调用 map.set 设置此实体 entry
      • 如果不存在,则调用 createMap 进行 ThreadLocalMap 对象的初始化,并将此实体 entry 作为第一个值存放至 ThreadLocalMap 中
        public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }
    
        // 为当前线程 Thread 创建对应维护的 ThreadLocalMap
        void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    
    

    remove 方法

    • 获取当前线程 Thread 对象,进而获取此线程对象中维护的ThreadLocalMap对象
    • 判断当前的 ThreadLocalMap是否存在:
      • 如果存在,则调用 map.remove 以当前 ThreadLocal 为 key 删除对应的实体 entry
        public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null)
                 m.remove(this);
         }
    

    相关文章

      网友评论

        本文标题:Java 多线程(四):ThreadLocal 详解

        本文链接:https://www.haomeiwen.com/subject/nbjbfftx.html