深入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对象只存在Entry中key对它的弱引用,在下次GC的时候,这个ThreadLocal对象就会被回收,导致key为null,此时value的强引用还存在,但是value已经不会被使用了,如果没有手动删除,那么这个Entry中的key就会发生内存泄露

使用弱引用还有一些好处,那就是,当key为null时, ThreadLocalMap中最多存在一个key为null,并且当调用set(),get(),remove()这些方法的时候,是会清除掉key为null的entry的。

set()、get()、remove() 方法中相关实现

  • 从下可以发现,set方法首先会进入一个循环。
  • 在这个循环中,会遍历整个Entry数组。直到遇到一个空的entry,退出循环。
  • 当遇到已存在的key’时,会直接替换value,然后返回。
  • 当遇到key为空的entry的时候,会直接将当前的entry存在这个过时的entry中,然后返回。

通过这个方法的源码可以看出,key为null的那个entry实际上迟早会被替换成新的entry。

        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }
							 // 发现key为空
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
        
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

同理,可以看到在get方法中也存在:

        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
         private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                // 替换过时的entry
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

remove() 方法中也是一样:

        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                // 清除过时的key
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

总结

  • ThreadLocal如果对ThreadLocalMap的key使用强引用,那么会存在整个entry发生内存泄露的问题如果不手动清除,那么这个不被使用的entry会一直存在。
  • ThreadLocal如果对ThreadLocalMap的key使用弱引用,那么可能会存在一个entry的value发生内存泄露,但是在调用set(),get(),remove() 方法时,key为null的entry会被清除掉。
  • 发生内存泄露最根本的原因是:threadLocals的生命周期是和线程一致的
  • 每次使用完ThreadLocal对象后,必须调用它的remove()方法清除数据。

5.ThreadLocal应用

ThreadLocal把数据存放到线程本地,解决了线程安全问题,没有使用锁,直接访问线程本地变量,效率较高(空间换时间。)
同时threadLocals的生命周期是和线程一致的,可以解决很多参数传递问题。

  • Session管理(Mabaties使用ThreadLocal存储session),数据库连接。
  • 如果需要跟踪请求的整个流程,可以使用ThreadLocal来传递参数。

ATFWUS 2021-11-09

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码

)">
< <上一篇
下一篇>>