zoukankan      html  css  js  c++  java
  • ThreadLocal及InheritableThreadLocal的原理剖析

    我们知道,线程的不安全问题,主要是由于多线程并发读取一个变量而引起的,那么有没有一种办法可以让一个变量是线程独有的呢,这样不就可以解决线程安全问题了么。其实JDK已经为我们提供了ThreadLocal这个东西。


    ThreadLocal基本使用

    当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
    ThreadLocal 的主要方法有这么几个:

    1
    2
    3
    4
    initialValue 初始化
    set 赋值
    get 取值
    remove 清空

    下面来看一个简单的使用代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class ThreadLocalDemo {
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
    @Override
    public Integer initialValue() {
    return 0;
    }
    };
    static class ThreadDemo implements Runnable {
    @Override
    public void run() {
    for (int i = 0; i < 1000; i++) {
    threadLocal.set(threadLocal.get() + 1);
    }
    System.out.println("thread :" + Thread.currentThread().getId() + " is" + threadLocal.get());
    }
    }
    public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
    new Thread(new ThreadDemo()).start();
    }
    }
    }

    上方代码使用了10个线程循环对一个threadLocal的值进行一千次的加法,如果我们不知道ThreadLocal的原理的话我们可能会觉得最后打印的值一定是1000、2000、3000。。10000或者是线程不安全的值。
    但是如果你执行这段代码你会发现最后打印的都是1000。


    ThreadLocal原理剖析

    现在我们来看一下ThreadLocal是如何实现为每个线程单独维护一个变量的呢。
    先来看一下初始化方法。

    1
    2
    3
    protected T initialValue() {
    return null;
    }

    initialValue 默认是返回空的,所以为了避免空指针问题重写了这个方法设置了默认返回值为0,但是呢,虽然这个方法好像是设置默认值的,但是还没有生效,具体请接着往下看。

    1
    2
    3
    4
    5
    6
    7
    8
    public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
    map.set(this, value);
    else
    createMap(t, value);
    }

    我们可以看到set方法首先会获取当前线程,然后通过一个getMap方法获取了ThreadLocalMap,接着来看一下这个map是怎么来的呢。

    1
    2
    3
    ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
    }

    这个map是在Thread类维护的一个map,下方是Thread类维护的此变量。默认这个map是空的。

    1
    ThreadLocal.ThreadLocalMap threadLocals = null;

    接着往下看代码,如果获取的时候map不为空,则通过set方法把Thread类的threadLocals变量更新。如果是第一次创建的时候则初始化Thread的threadLocals变量。
    下方是createMap的代码:

    1
    2
    3
    void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    接下来看个get方法就比较容易理解了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
    @SuppressWarnings("unchecked")
    T result = (T)e.value;
    return result;
    }
    }
    return setInitialValue();
    }

    注意关注最后的一个return,看到调用的这个方法名我们就可以发现这个ThreadLocal的初始化原来是当第一调用get方法时如果还没有被set的时候才会去获取initialValue 方法的返回值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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;
    }


    使用ThreadLocal最应该注意的事项

    首先来看一下线程退出的办法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    private void exit() {
    if (group != null) {
    group.threadTerminated(this);
    group = null;
    }
    target = null;
    threadLocals = null;
    inheritableThreadLocals = null;
    inheritedAccessControlContext = null;
    blocker = null;
    uncaughtExceptionHandler = null;
    }

    我们看到当线程结束的时候上方第7行会把ThreadLocal的值制为空,这个东西本身是没问题的。但是,如果你是使用的线程池,这个问题可就大了!!!
    要知道线程池里的线程执行完一个任务之后紧接着下一个,这中间线程可不会结束,下一个任务获得Thread的值可是上一个任务的遗留数据。
    下面是这个问题的示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
    @Override
    public Integer initialValue() {
    return 0;
    }
    };
    static class ThreadDemo implements Runnable {
    @Override
    public void run() {
    for (int i = 0; i < 1000; i++) {
    threadLocal.set(threadLocal.get() + 1);
    }
    System.out.println("thread :" + Thread.currentThread().getId() + " is" + threadLocal.get());
    //threadLocal.remove();
    }
    }
    public static void main(String[] args) {
    ExecutorService executorService= Executors.newFixedThreadPool(5);
    for (int i = 0; i < 10; i++) {
    executorService.submit(new Thread(new ThreadDemo()));
    }
    }

    执行这段代码你就会发现同样的操作在线程池里已经得不到一样的结果了。想要解决这种问题也很简单,只需要把ThreadLocal的值在线程执行完清空就可以了。把第14行注释的代码放开再执行以下你就明白了。


    InheritableThreadLocal

    其实ThreadLocal还有一个比较强大的子类InheritableThreadLocal,它呢可以把父线程生成的变量传递给子线程。
    下面来看一下代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class InheritableThreadLocalDemo {
    private static InheritableThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>();
    static class ThreadDemo implements Runnable {
    @Override
    public void run() {
    for (int i = 0; i < 1000; i++) {
    inheritableThreadLocal.set(inheritableThreadLocal.get() + 1);
    }
    System.out.println("thread :" + Thread.currentThread().getId() + " is" + inheritableThreadLocal.get());
    }
    }
    public static void main(String[] args) {
    inheritableThreadLocal.set(24);
    for (int i = 0; i < 10; i++) {
    new Thread(new ThreadDemo()).start();
    }
    }
    }

    执行代码会发现程序输出全是1024,这就是因为InheritableThreadLocal吧在主线程设置的值24传递到了那10个子线程中。


    InheritableThreadLocal原理剖析

    接下来我们来看一下InheritableThreadLocal为什么可以实现这种功能呢。
    InheritableThreadLocal是ThreadLocal的子类,
    与ThreadLocal相同的set方法

    1
    2
    3
    4
    5
    6
    7
    8
    public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
    map.set(this, value);
    else
    createMap(t, value);
    }

    不同点是InheritableThreadLocal重写了createMap方法,将值赋值给了线程的inheritableThreadLocals变量。

    1
    2
    3
    void createMap(Thread t, T firstValue) {
    t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }

    再跟进去Thread类的源码看inheritableThreadLocals变量你会发现:我去,这不是跟Threadlocal一样么,同样初始值为null,线程退出的时候清空。没错,就是这样的。也就是说它其实也是一个线程私有的变量,ThreadLocal的功能它是都有的。

    那么它又是怎么把父线程的变量传递到子线程的呢?
    接着看Thread的构造方法

    1
    2
    3
    public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
    }

    一路追踪init方法你会看见这段代码:

    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
    private void init(ThreadGroup g, Runnable target, String name,
    long stackSize, AccessControlContext acc) {
    if (name == null) {
    throw new NullPointerException("name cannot be null");
    }
    this.name = name;
    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    if (g == null) {
    if (security != null) {
    g = security.getThreadGroup();
    }
    if (g == null) {
    g = parent.getThreadGroup();
    }
    }
    g.checkAccess();
    if (security != null) {
    if (isCCLOverridden(getClass())) {
    security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
    }
    }
    g.addUnstarted();
    this.group = g;
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    if (security == null || isCCLOverridden(parent.getClass()))
    this.contextClassLoader = parent.getContextClassLoader();
    else
    this.contextClassLoader = parent.contextClassLoader;
    this.inheritedAccessControlContext =
    acc != null ? acc : AccessController.getContext();
    this.target = target;
    setPriority(priority);
    if (parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    this.stackSize = stackSize;
    tid = nextThreadID();
    }

    仔细观察倒数第5行到倒数第二行你就明白了。

    本文所有源码https://github.com/shiyujun/syj-study-demo

  • 相关阅读:
    接口测试
    Excel
    day16 内置函数作业
    day16 内置函数和匿名函数
    day15 内置函数
    day14 生成器进阶
    day13迭代器
    day13生成器
    day11 作业
    day11 装饰器
  • 原文地址:https://www.cnblogs.com/zhixiang-org-cn/p/10612306.html
Copyright © 2011-2022 走看看