深入ThreadLocal原理剖析

Posted ATFWUS

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入ThreadLocal原理剖析相关的知识,希望对你有一定的参考价值。

分享一下最近看的ThreadLocal的源码的一些体会。



1.了解ThreadLocal

简介

  • ThreadLocal是JDK中java.lang包下提供的类。
  • ThreadLocal是线程安全的,并且没有使用到锁。
  • 常用来存放线程独有变量,解决参数传递问题。
  • 当我们创建一个ThreadLocal包装的变量后,每个访问这个变量的线程会在自己的线程空间创建这个变量的一个副本,在每次操作这个变量的时候,都是在自己的线程空间内操作,解决了线程安全问题。

使用(是线程安全的)

  • 在这个demo中,localStr是共享的,随后在每个线程中给localStr设置值为自己线程的名字,然后再将当前线程的日志输出。
  • sleep5毫秒是为了体现出是否存在线程安全问题。
  • 从运行结果可以看到,是不存在线程安全问题的:
/**
 * @author ATFWUS
 * @version 1.0
 * @date 2021/11/8 21:23
 * @description
 */
@Slf4j
public class ThreadLocalTest {

    static ThreadLocal<String> localStr = new ThreadLocal<>();

    public static void main(String[] args) {
        List<Thread> list = new LinkedList<>();
        for(int i = 0; i < 1000; i++){
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    localStr.set(Thread.currentThread().getName() + " localStr");
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.debug(localStr.get());
                }
            }, "t" + String.valueOf(i));
            list.add(t);
        }
        for (Thread t : list) {
            t.start();
        }

    }
}

  • 而对于普通变量来说,很明显是存在线程安全问题的:
/**
 * @author ATFWUS
 * @version 1.0
 * @date 2021/11/8 21:23
 * @description
 */
@Slf4j
public class ThreadLocalTest {

    static ThreadLocal<String> localStr = new ThreadLocal<>();
    static String shareStr;

    public static void main(String[] args) {
        List<Thread> list = new LinkedList<>();
        for(int i = 0; i < 1000; i++){
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    shareStr = Thread.currentThread().getName() + "  shareStr";
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.debug(shareStr);
                }
            }, "t" + String.valueOf(i));
            list.add(t);
        }
        for (Thread t : list) {
            t.start();
        }

    }
}


2.源码解析 – 探究实现思路

threadLocals变量与ThreadLocalMap

  • 每个线程的本地变量并不存放于ThreadLocal对象中,而是存在调用线程的threadLocals变量中。因为是线程对象的成员变量,所以生命周期等同于线程的生命周期。
  • 而threadLocals是ThreadLocalMap类的实例。
  • ThreadLocalMap实际上是一个类似HashMap的实现,是ThreadLocal的静态内部类。
  • 看下Doug Lea写的注释:
    • ThreadLocalMap是一个定制的hash map,仅适用于维护线程本地值。在ThreadLocal类之外没有暴露任何的操作。这个类是私有的,允许在类线程中声明字段。为了处理非常大并长期存在(对象)的用法,哈希表的entries使用weakReference作为键。但是,由于没有使用引用队列,因此只有当表开始耗尽空间时,才能保证删除过时的entries。
  • 暂不探究ThreadLocalMap的内部实现细节,暂时只需要知道实现了一个hash map,并且Entry的key是弱引用即可,具体的set() get() remove() 方法在下文中会有。

set(T value) 方法

  • 进入set(T value) 方法后,先尝试获取map,如果获取到了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() 方法

  • 进入get()方法后,首先获取当前线程,然后进入getMap(Thread t)中获取ThreadLocalMap对象,直接返回t.threadLocals。
  • 如果map不为空,直接返回map中当前ThreadLocal作为键对应的值。
  • 如果map为空,需要先进行初始化。调用setInitialValue()方法进行初始化。
    • setInitialValue()中先获取一个初始值,默认为null。
    • 如果map存在当前线程中,直接设置初始值。
    • 如果map不存在当前线程中,需要先创建一个map。
    • createMap(Thread t, T firstValue)中就是new了一个ThreadLocalMap对象,并且初始化了一个entry对。
    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();
    }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    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;
    }
    protected T initialValue() {
        return null;
    }
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

remove() 方法

  • remove() 方法中,先判断map是否存在,不存在直接将map中this作为键的entry删掉。
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

实现思路总结

  • ThreadLocal搭配线程的threadLocals变量实现,当调用set(T value) 和 get() 方法时,如果线程中的threadLocals仍然为null,会为其初始化。
  • ThreadLocal对象往threadLocals存储具体变量时,key是ThreadLocal对象的自身引用,value是真正的变量,且key是弱引用


3.InheritableThreadLocal与继承性

InheritableThreadLocal英语翻译一下就是可继承的ThreadLocal,让我们看下它和ThreadLocal的继承性体现在哪。

这里的继承性指的是:子线程是否能访问父线程的变量

ThreadLocal的不可继承性

threadLocals是当前线程的成员变量,在子线程中不可见

/**
 * @author ATFWUS
 * @version 1.0
 * @date 2021/11/9 14:29
 * @description
 */
@Slf4j
public class InheritableThreadLocalTest {

    static ThreadLocal<String> localStr = new ThreadLocal<>();

    public static void main(String[] args) {
        localStr.set("main线程为其设置的值");
        new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug("访问localStr : " + localStr.get());
            }
        }).start();
        System.out.println(localStr.get());
    }
}

InheritableThreadLocal实现继承性的源码剖析

看一下InheritableThreadLocal的源码:

源码非常简短,下面简单分析一下:

  • InheritableThreadLocal类继承自ThreadLocal类,重写了childValue(T parentValue)、getMap()、createMap(Thread t, T firstValue) 三个方法。
  • createMap(Thread t, T firstValue)会在初始化的时候调用,重写createMap(Thread t, T firstValue) 意味着,InheritableThreadLocal的实例使用的是线程对象中的inheritableThreadLocals,而不再是原来的threadLocals。
  • getMap() 方法也是确保使用的是inheritableThreadLocals。
  • childValue(T parentValue) 方法中,直接返回了parentValue,这个方法会在ThreadLocal的构造方法中被调用,为了弄清这个意图,我们有必要看看Thread类初始化方法的源码。

从Thread的构造方法看,发现所有的构造方法都会调用init()方法进行初始化,init()方法有两个重载形式。

我们进入参数较多的init方法查看一下:

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
				// 新线程还未创建出来,当前线程就是即将要创建线程的父线程
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        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);
        // 如果父线程的inheritThreadLocals 不为空
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        		 // 设置子线程中的inheritableThreadLocals设置为父线程的inheritableThreadLocals
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

我们重点看一下和inheritThreadLocals相关的地方(含注释的地方)

  • 在进入init方法后,先获取了父线程,然后再下面判断了父线程的inheritThreadLocals 是否为空,不为空就调用ThreadLocal.createInheritedMap方法,参数就是父线程的inheritThreadLocals 。

再看下ThreadLocal.createInheritedMap方法:

  • 调用了自身的构造方法,将parentMap传入。
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

看下这个构造方法:

  • 发现主要是用parentMap的所有entry初始化当前的map。
  • 在注释处,调用了inheritThreadLocals重写的childValue方法,而重写后,直接返回的是parentValue,也就是将父线程的inheritThreadLocal里面的entry完整的复制到了子线程中。
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        // 调用inheritThreadLocals重写的childValue方法
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

如何理解这个继承性

通过上面的源码分析,可以发现,InheritableThreadLocal的继承性主要体现在:创建子线程时,会将父线程的inheritThreadLocals里面所有entry拷贝一份给子进程。

那么当子进程被创建出来之后,父进程又修改了inheritThreadLocals里面的值,这个操作是否对子线程可见,通过上面的源码可知,这个操作明显是不可见的,下面有个demo可以证实。

  • sleep操作是为了控制两个线程的执行流程。
/**
 * @author ATFWUS
 * @version 1.0
 * @date 2021/11/9 14:29
 * @description
 */
@Slf4j
public class InheritableThreadLocalTest {

    static ThreadLocal<String> localStr = new ThreadLocal<>();
    static InheritableThreadLocal<String> inheritableLocalStr = new InheritableThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        inheritableLocalStr.set("main线程第一次为inheritableLocalStr设置的值");
        new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug("子线程第一次访问inheritableLocalStr : " + inheritableLocalStr.get());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("子线程第二次访问inheritableLocalStr : " + inheritableLocalStr.get());
            }
        }).start();
        Thread.sleep(500);
        inheritableLocalStr.set("main线程第二次为inheritableLocalStr设置的值");
        log.debug("main线程第二次为inheritableLocalStr赋值");
        Thread.sleep(1000);
    }
}

看下输出:


可以发现,子线程创建出来后,对父线程中inheritThreadLocals的修改操作,对子线程不可见

总结

  • ThreadLocal不可继承,threadLocals是当前线程的成员变量,在子线程中不可见。
  • InheritableThreadLocal可继承,原理是:在新建子线程的时候,将父线程中inheritThreadLocals所有的entry拷贝给了子线程。
  • 子线程创建出来后,对父线程中inheritThreadLocals的修改操作,对子线程不可见。

4.存在的内存泄露问题

要充分理解ThreadLocal中存在的内存泄露问题,需要有以下JVM对内存管理的前置知识(这里篇幅问题就不补充了):

  • 什么是内存泄露?
  • 什么是强引用?
  • 什么是弱引用?
  • 何时GC?
  • 强引用和弱引用GC时的区别?

在分析上述ThreadLocalMap源码的时候,注意到有一个小细节,ThreadLocalMap的Entry继承了WeakReference<ThreadLocal<?>>,也就是说Entry的key是一个对ThreadLocal<?>的弱引用。问题来了,为什么这里要使用弱引用呢?

使用强引用会如何?

现在假设Entry的key是一个对ThreadLocal的强引用,当ThreadLocal对象使用完后,外部的强引用不存在,但是因为当前线程对象中的threadLocals还持有ThreadLocal的强引用,而threadLocals的生命周期是和线程一致的,这个时候,如果没有手动删除,整个Entry就发生了内存泄露

使用弱引用会如何?

现在假设Entry的key是一个对ThreadLocal的弱引用,当ThreadLocal对象使用完后,

以上是关于深入ThreadLocal原理剖析的主要内容,如果未能解决你的问题,请参考以下文章

深入剖析ThreadLocal

深入剖析ThreadLocal

Java并发编程:深入剖析ThreadLocal

转:Java并发编程:深入剖析ThreadLocal

Java并发--深入剖析ThreadLocal

Java并发编程:深入剖析ThreadLocal