zoukankan      html  css  js  c++  java
  • ThreadLocal 线程本地存储

    ThreadLocal | 线程本地存储

    并发场景下,多个线程同时读写共享变量就有可能产生并发安全问题。反过来也可以说,不存在共享变量,就不会出现线程安全问题。Java中有两种常用的避免共享变量的方法,使用局部变量,以及使用 ThreadLocal。

    局部变量存在于每个线程内部的调用栈中,多个线程之间互相访问不到对方的局部变量,这就叫做线程封闭。如下图所示,局部变量存在于线程各自的调用栈中,线程之间互不打扰。

    局部变量,线程封闭

    采用局部变量的方案,的确避免了变量被多个线程共享,同时它也禁止同一个线程中不同方法共享这个变量。然而,单线程中不同的方法之间共享变量是不会导致线程安全问题的。


    如果想让同一个线程,不同的方法共享变量就可以使用 ThreadLocal,Java 提供的线程本地存储方案。ThreadLocal 可以保证同一个变量,该线程中的方法看到的值是一样,不同线程之间却是隔离。


    ThreadLocal 的使用方法

    常规使用 ThreadLocal 的方式很简单,创建一个 ThreadLocal 对象,然后调用它的 set(value)方法设置值,再调用 get() 方法获取这个 ThreadLocal 对象对应的value。

    // 创建一个 ThreadLocal
    ThreadLocal<String> tl = new ThreadLocal<>();
    // set方法
    tl.set("深页");
    // get方法
    tl.get();
    

    ThreadLocal 类的注释中还带有为每个线程分配自增 id 的示例代码。withInitial()方法会调用initialValue()方法,为 ThreadLocal 设置 get() 的初始值。执行下面的代码,可以看到每个类都有自己的id,并且id的自增的。

    public class ThreadId {
    
        // Integer类型的原子类,用来分配Id,保证其本身是线程安全的
        private static final AtomicInteger nextId = new AtomicInteger();
    
        // 创建一个 ThreadLocal 变零,并且为其赋值
        private static ThreadLocal <Integer> threadId = ThreadLocal.withInitial(
                () -> nextId.getAndIncrement()
        );
    
        // 获取id,即从ThreadLocal中获取对应的值
        public static int getId() {
            return threadId.get();
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 100; i++) {
                new Thread(() ->
                        System.out.println(Thread.currentThread().getName()
                                + ": " + ThreadId.getId()))
                        .start();
            }
        }
    }
    

    ThreadLocal还有一个经典的使用案例,就是将线程不安全的 SimpleDateFormat 类封装成线程安全的,原理其实和上面的例子是一样:

    static class SafeDateFormat {
        static final ThreadLocal<DateFormat> tl = ThreadLocal.withInitial(
                ()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
        );
        
        static DateFormat get() {
            return tl.get();
        }
    }
    

    ThreadLocal的底层原理

    先来看 set() 方法:

    1. 首先获取当前线程,然后通过当前线程获取线程持有的局部变量 threadLocals
    2. 如果返回的 map 不是空的就设置值
    3. 如果返回的 map 是空的,就调用构造方法初始化 map 并为其设置值
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    

    再看 get() 方法:

    public T get() {
        Thread t = Thread.currentThread();
        // getMap()返回当前线程的threadLocals
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // map存在返回当前ThreadLocal对应的value值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // map不存在就初始化
        return setInitialValue();
    }
    

    看过上面两个方法,可以看到它们除了涉及到 Thread 类,还涉及到了一个类 ThreadLocalMap。那么 Thread、ThreadLocal、ThreadLocalMap 之间是什么关系呢?

    ThreadLocal 的数据结构

    ThreadLocalMap 是 ThreadLocal 的静态内部类,ThreadLocalMap 的底层是一个 Entry[] table 数组,Entry 是 ThreadLocalMap 的静态内部类,以 ThreadLocal 作为 key,以设置的值作为 value,如下所示:

    static class Entry extends WeakReference <ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
    
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    

    Thread 持有一个 ThreadLocalMap 的引用 threadLocals

    ThreadLocal.ThreadLocalMap threadLocals = null;
    

    所以说,我们通过当前线程 Thread t 可以到 t 持有的 ThreadLocalMap,并且通过 ThreadLocal 对象返回其对应的 value。

    ThreadLocal 内存泄露问题

    使用 Thread.start() 方法是不会产生内存泄露的问题的,只有当我们在线程池中使用 ThreadLocal 才有可能产生内存泄露问题。

    内存泄露的本质是长生命周期的对象,持有短生命周期对象。当短生命周期的对象使用结束之后,理应被垃圾回收器回收,但是它却被一个更长生命周期的对象引用。通过可达性分析算法,该短生命周期的对象被一个GC Root引用,理应被回收的它就无法被回收。

    **那为什么在线程池中使用ThreadLocal就可能发生内存泄露的问题呢?**我们就从长生命周期的对象,持有短生命周期对象这个角度进行分析。

    图片来自于网络

    线程池作为一种池化资源技术,目的是避免线程的频繁创建和销毁。一般来说,线程池中的线程生命周期都很长,是和应用程序同生共死的。这就意味着,被 Thread 持有的 ThreadLocalMap 一直都不会被回收。

    ThreadLocalMap 底层是一个 Entry 数组,Entry是<ThreadLocal,value>对结构。 Entry 对 ThreadLocal 是弱引用(WeakReference),所以ThreadLocal 生命周期之后,是结束是可以被回收掉的。但是 Entry 对 value 强引用的,所以即便 Value 的生命周期结束了,Value 也是无法被回收的,从而导致内存泄露。

    InheritableThreadLocal 与继承性

    使用 ThreadLocal 还有这样一种需求,ThreadLocal 创建了线程变量 V,然后希望该线程创建的子线程也能访问到父线程的线程变量 V。

    为此 Java 提供了 InheritableThreadLocal 来支持这种特性,InheritableThreadLocal 继承自 ThreadLocal,用法其实和 ThreadLocal 一样。

    public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    

    最后做一个小结,多线程同时读写共享变量就有可能产生并发问题。一种解决并发问题的思路就是避免变量被共享。与之对应的技术有线程隔离(局部变量),以及线程本地存储ThreadLocal

    相比于使用局部变量,ThreadLocal 存储的变量可以供线程中的方法共享,单线程对共享变量的读写必定是线程安全的。

  • 相关阅读:
    RDay2-Problem 2 B
    杭电 1862 EXCEL排序(sort+结构体)
    杭电 2803 The MAX(sort)
    杭电 5053 the Sum of Cube(求区间内的立方和)打表法
    杭电 2089 不要62
    杭电 4548 美素数(素数打表)
    杭电2098 分拆素数和
    杭电1722 Cake (分蛋糕)
    素数判定 (素数打表)
    最小公倍数
  • 原文地址:https://www.cnblogs.com/shuiyj/p/13185060.html
Copyright © 2011-2022 走看看