关于java:java基础之ThreadLocal

37次阅读

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

早在 JDK 1.2 的版本中就提供 java.lang.ThreadLocal,ThreadLocal 为解决多线程程序的并发问题提供了一种新的思路。应用这个工具类能够很简洁地编写出柔美的多线程程序。ThreadLocal 是指作用域为 Thread 的局部变量,兴许把它命名为 ThreadLocalVariable 更容易让人了解一些。此博客很多内容参考了 (这篇博客 https://www.cnblogs.com/fsmly…).

介绍

多线程拜访同一个共享变量的时候容易呈现并发问题,特地是多个线程对一个变量进行写入的时候,为了保障线程平安,个别使用者在访问共享变量的时候须要进行额定的同步措施能力保障线程安全性。ThreadLocal 是除了加锁这种同步形式之外的一种保障一种躲避多线程拜访呈现线程不平安的办法,当咱们在创立一个变量后,如果每个线程对其进行拜访的时候拜访的都是线程本人的变量这样就不会存在线程不平安问题。ThreadLocal 是 JDK 包提供的,它提供线程本地变量,如果创立一个 ThreadLocal 变量,那么拜访这个变量的每个线程都会有这个变量的一个正本,在理论多线程操作的时候,操作的是本人本地内存中的变量,从而躲避了线程平安问题,如下图所示:

ThreadLocal 应用示例

上面的例子中,开启两个线程,在每个线程外部设置了本地变量的值,而后调用 print 办法打印以后本地变量的值。如果在打印之后调用本地变量的 remove 办法会删除本地内存中的变量,代码如下所示:

package test;

public class ThreadLocalTest {static ThreadLocal<String> localVar = new ThreadLocal<>();

    static void print(String str) {
        // 打印以后线程中本地内存中本地变量的值
        System.out.println(str + ":" + localVar.get());
        // 革除本地内存中的本地变量
        localVar.remove();}

    public static void main(String[] args) {Thread t1  = new Thread(new Runnable() {
            @Override
            public void run() {
                // 设置线程 1 中本地变量的值
                localVar.set("localVar1");
                // 调用打印办法
                print("thread1");
                // 打印本地变量
                System.out.println("after remove :" + localVar.get());
            }
        });

        Thread t2  = new Thread(new Runnable() {
            @Override
            public void run() {
                // 设置线程 1 中本地变量的值
                localVar.set("localVar2");
                // 调用打印办法
                print("thread2");
                // 打印本地变量
                System.out.println("after remove :" + localVar.get());
            }
        });

        t1.start();
        t2.start();}
}

ThreadLocal 的实现原理

从上一节中咱们能够看出,ThreadLocal 次要有 set 和 get 办法,用于设置和获取线程中的变量,那么 ThreadLocal 是怎么实现这个性能的呢?和 ThreadLocal 实现相干的类次要有三个:ThreadLocal、Thread、ThreadLocalMap,三者之间的关系同样如下图所示:

  1. ThreadLocalMap:名字上看是 Map,实际上是一个数组,不过它的性能和 Map 相似,能够依照 key 查找数据。
  2. Thread:线程大家应该都晓得,那么在 ThreadLocal 中他起什么作用呢?一个 Thread 中会蕴含两个 ThreadLocalMap,别离用于存储本线程和父线程的 ThreadLocal 数据。每一个 ThreadLocal 变量会在线程中对应一条 ThreadLocalMap 的 key-value,其中 key 是 ThreadLocal 的惟一 Hash 值。
  3. ThreadLocal:每个 ThreadLocal 都会有一个惟一的 Hash 值,用于查找这个 ThreadLocal 在 ThreadLocalMap 中的值;ThreadLocal 提供了办法用于获取以后线程的 ThreadLocal 数据。

数据寄存的地位

ThreadLocal 只是一层拜访线程数据的壳,ThreadLocal get 和 set 的数据不会在 ThreadLocal 的实例中寄存,而是寄存在线程 Thread 中的 ThreadLocalMap,ThreadLocal 只是提供了一个拜访这些数据的路径。

ThreadLoca 的 set 办法将 value 增加到调用线程的 ThreadLocalMap 中,当调用线程调用 get 办法时候可能从它的 ThreadLocalMap 中取出变量。如果调用线程始终不终止,那么这个本地变量将会始终寄存在他的 ThreadLocalMap 中,所以不应用本地变量的时候须要调用 remove 办法将 ThreadLocalMap 中删除不必的本地变量。

set 办法存放数据

ThreadLocal 办法的 set 能够向以后线程的 ThreadLocalMap 中放入数据,存放数据的源码如下所示,Set 过程分为以下步骤:

  1. 获取以后线程。
  2. 从以后线程中获取 ThreadLocalMap 变量。
  3. 如果以后线程的 ThreadLocalMap 不为空,用以后的 ThreadLocal 为 Key,须要寄存的数据为 Value,存放数据。
  4. 如果以后线程的 ThreadLocalMap 为空,创立 ThreadLocalMap 并存放数据。
public void set(T value) {//(1) 获取以后线程(调用者线程)Thread t = Thread.currentThread();
    //(2) 以以后线程作为 key 值,去查找对应的线程变量,找到对应的 map
    ThreadLocalMap map = getMap(t);
    //(3) 如果 map 不为 null,就间接增加本地变量,key 为以后定义的 ThreadLocal 变量的 this 援用,值为增加的本地变量值
    if (map != null)
        map.set(this, value);
    //(4) 如果 map 为 null,阐明首次增加,须要首先创立出对应的 map
    else
        createMap(t, value);
}

get 办法获取数据

ThreadLocal 办法的 get 能够获取以后线程 ThreadLocalMap 中寄存的数据,获取存放数据的源码如下所示,get 过程分为以下步骤:

  1. 获取以后线程
  2. 从以后线程中获取 ThreadLocalMap 变量。
  3. 如果 ThreadLocalMap 变量不为 null,就能够在 map 中查找到本地变量的值。
  4. 如果 ThreadLocalMap 变量为 null,那么就初始化以后线程的 ThreadLocalMap。
public T get() {//(1) 获取以后线程
    Thread t = Thread.currentThread();
    //(2) 获取以后线程的 threadLocals 变量
    ThreadLocalMap map = getMap(t);
    //(3) 如果 threadLocals 变量不为 null,就能够在 map 中查找到本地变量的值
    if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {@SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //(4) 执行到此处,threadLocals 为 null,调用该更改初始化以后线程的 threadLocals 变量
    return setInitialValue();}

private T setInitialValue() {//protected T initialValue() {return null;}
    T value = initialValue();
    // 获取以后线程
    Thread t = Thread.currentThread();
    // 以以后线程作为 key 值,去查找对应的线程变量,找到对应的 map
    ThreadLocalMap map = getMap(t);
    // 如果 map 不为 null,就间接增加本地变量,key 为以后线程,值为增加的本地变量值
    if (map != null)
        map.set(this, value);
    // 如果 map 为 null,阐明首次增加,须要首先创立出对应的 map
    else
        createMap(t, value);
    return value;
}

ThreadLocal 不反对继承性

同一个 ThreadLocal 变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals 中为以后调用线程对应的本地变量,所以二者天然是不能共享的)。

package test;

public class ThreadLocalTest2 {//(1) 创立 ThreadLocal 变量
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        // 在 main 线程中增加 main 线程的本地变量
        threadLocal.set("mainVal");
        // 新创建一个子线程
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {System.out.println("子线程中的本地变量值:"+threadLocal.get());
            }
        });
        thread.start();
        // 输入 main 线程中的本地变量值
        System.out.println("mainx 线程中的本地变量值:"+threadLocal.get());
    }
}

InheritableThreadLocal 类

在下面说到的 ThreadLocal 类是不能提供子线程拜访父线程的本地变量的,而 InheritableThreadLocal 类则能够做到这个性能,上面是该类的源码,InheritableThreadLocal 类继承了 ThreadLocal 类,并重写了 childValue、getMap、createMap 三个办法。咱们接下来别离介绍一下三种办法的用途。

  1. createMap:当线程中不存在 ThreadLocalMap 变量,然而调用 set 或者 get 办法设置值的时候,须要初始化 ThreadLocalMap 变量时调用该办法。
  2. getMap:须要获取线程的 ThreadLocalMap 时调用该办法,这里返回的 ThreadLocalMap 始终为 InheritableThreadLocalMap。
  3. childValue:在创立新线程的时候,如果父线程有 ThreadLocalMap 变量并且容许 inherite ThreadLocalMap,那么程序会复制父线程的 InheritableThreadLocal 到子线程中,childValue 示意在复制过程中如何依据父线程中失去数据生成线程中的数据。


public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
     * Creates an inheritable thread local variable.
     */
    public InheritableThreadLocal() {}

    /**
     * Computes the child's initial value for this inheritable thread-local
     * variable as a function of the parent's value at the time the child
     * thread is created.  This method is called from within the parent
     * thread before the child is started.
     * <p>
     * This method merely returns its input argument, and should be overridden
     * if a different behavior is desired.
     */
    protected T childValue(T parentValue) {return parentValue;}

    /**
     * Get the map associated with a ThreadLocal.
     */
    ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}

    /**
     * Create the map associated with a ThreadLocal.
     */
    void createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

总结 :Thread 会在构造函数中将父线程的 inheritableThreadLocals 成员变量的值赋值到新的 ThreadLocalMap 对象中。返回之后赋值给子线程的 inheritableThreadLocals。InheritableThreadLocals 类通过重写 getMap 和 createMap 两个办法将本地变量保留到了具体线程的 inheritableThreadLocals 变量中,当线程通过 InheritableThreadLocals 实例的 set 或者 get 办法设置变量的时候,就会创立以后线程的 inheritableThreadLocals 变量。而父线程创立子线程的时候,ThreadLocalMap 中的构造函数会将父线程的 inheritableThreadLocals 中的变量复制一份到子线程的 inheritableThreadLocals 变量中。

ThreadLocal 内存透露

通过后面的剖析咱们晓得,ThreadLocal 的线程数据是寄存在 ThreadLocalMap 中的,所以如果 ThreadLocal 呈现内存透露,那么必定是 ThreadLocalMap 中存储的数据呈现了泄露,咱们须要看看 ThreadLocalMap 中的数据结构。ThreadLocalMap 的数据结构如下所示,ThreadLocalMap 中的数据存储在一个 Entry 数组中,Entry 中有对 ThreadLocal 的 WeakReference。

什么状况下会呈现内存泄露呢?

  1. 当一个线程调用 ThreadLocal 的 set 办法设置变量的时候,以后线程的 ThreadLocalMap 就会寄存一个记录,这个记录的 key 值为 ThreadLocal 的弱援用,value 就是通过 set 设置的值。
  2. 如果以后线程始终存在且没有调用该 ThreadLocal 的 remove 办法,如果这个时候别的中央还有对 ThreadLocal 的援用,那么以后线程中的 ThreadLocalMap 中会存在对 ThreadLocal 变量的援用和 value 对象的援用,是不会开释的,就会造成内存透露。
  3. 思考这个 ThreadLocal 变量没有其余强依赖,如果以后线程还存在,因为线程的 ThreadLocalMap 外面的 key 是弱援用,所以以后线程的 ThreadLocalMap 外面的 ThreadLocal 变量的弱援用在 gc 的时候就被回收,然而对应的 value 还是存在的这就可能造成内存透露 (因为这个时候 ThreadLocalMap 会存在 key 为 null 然而 value 不为 null 的 entry 项)。

总结 :THreadLocalMap 中的 Entry 的 key 应用的是 ThreadLocal 对象的弱援用,在没有其余中央对 ThreadLoca 依赖,ThreadLocalMap 中的 ThreadLocal 对象就会被回收掉,然而对应的不会被回收,这个时候 Map 中就可能存在 key 为 null 然而 value 不为 null 的项,这须要理论的时候应用结束及时调用 remove 办法防止内存透露。

Java 中的四种援用类型

上文中咱们说到了 WeakReference,大家可能对这个词有点生疏,Java 中有四种援用类型:

  1. 强援用:Java 中默认的援用类型,一个对象如果具备强援用那么只有这种援用还存在就不会被 GC。
  2. 软援用:简言之,如果一个对象具备弱援用,在 JVM 产生 OOM 之前(即内存短缺够应用),是不会 GC 这个对象的;只有到 JVM 内存不足的时候才会 GC 掉这个对象。软援用和一个援用队列联结应用,如果软援用所援用的对象被回收之后,该援用就会退出到与之关联的援用队列中。
  3. 弱援用(这里探讨 ThreadLocalMap 中的 Entry 类的重点):如果一个对象只具备弱援用,那么这个对象就会被垃圾回收器 GC 掉 (被弱援用所援用的对象只能生存到下一次 GC 之前,当产生 GC 时候,无论以后内存是否足够,弱援用所援用的对象都会被回收掉)。弱援用也是和一个援用队列联结应用,如果弱援用的对象被垃圾回收期回收掉,JVM 会将这个援用退出到与之关联的援用队列中。若援用的对象能够通过弱援用的 get 办法失去,当援用的对象呗回收掉之后,再调用 get 办法就会返回 null;
  4. 虚援用:虚援用是所有援用中最弱的一种援用,其存在就是为了将关联虚援用的对象在被 GC 掉之后收到一个告诉。(不能通过 get 办法取得其指向的对象)。

我是御狐神,欢送大家关注我的微信公众号:wzm2zsd

本文最先公布至微信公众号,版权所有,禁止转载!

正文完
 0