透析ThreadLocal(以源码角度讲解原理)

序言

本文主要以案例+源码的形式介绍ThreadLocal相关原理,对于概念术语,网上一大堆,而且不容易理解,本篇旨在通过源码的形式让读者深入理解并掌握ThreadLocal,面试时遇到相关题目更有底气地show给面试官。下面进入正题。

初探ThreadLocal

先看看ThreadLocal的简单使用,

public class Main {

    public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

    public static void main(String[] args) {

        THREAD_LOCAL.set("卷");

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " " + THREAD_LOCAL.get());
            }
        }, "thread");
        thread.start();

        System.out.println(Thread.currentThread().getName() + " " + THREAD_LOCAL.get());
    }
}

上面代码逻辑为主线程调用ThreadLocal变量设置值,然后新建一个线程thread去读取该ThreadLocal变量设置的值,主线程也同样去读取ThreadLocal变量里设置的值(读者可以先想想程序运行结果)。程序的运行结果为:

main 卷
thread null

结果是否跟你想的一样呢,为什么是这个结果,后面会从源码角度剖析让你彻底弄懂,这里我们可以得出一个结论:
线程A使用ThreadLocal设置某个值,那么这个值只有线程A能访问的到,其他线程是访问不到的!

对Java中的线程不熟悉的建议学习这篇文章Java中的线程
注意main、thread这个两个线程名字,后面的分析基于这个案例的,下面开始分析源码。

深入ThreadLocal

在Java中的Thread类里有两个ThreadLocalMap成员变量,

public class Thread implements Runnable {
			// ......
			ThreadLocal.ThreadLocalMap threadLocals;
			ThreadLocal.ThreadLocalMap inheritableThreadLocals;
			// ......
}

ThreadLocal长啥样呢?看下面,
在这里插入图片描述
结构也不复杂,代码量也不多很好阅读。ThreadLocalMap是ThreadLocal里的静态内部类,map这种数据结果我们应该很熟悉了,比如我们常用的HashMap
来看看ThreadLocalMap长啥样,
在这里插入图片描述
读过HashMap源码的同学是否有种似曾相识的感觉?
🆗,然后呢?
上面是铺垫,主要介绍涉及的类及其结构,有个初步映像就好,下面重头戏来了,

当某个线程执行到这行代码会发生什么?我们一步步追进去看,

THREAD_LOCAL.set("卷");

set方法的源码如下,

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

场景还原:片头我们是用的主线程执行的这行代码,也就是说此时的 t 就是main线程,getMap方法源码如下,

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

见名知意,返回main线程的threadLocals成员变量。

如果map不为null,直接map.set(this, value),这里的this就是ThreadLocal变量即THREAD_LOCAL,由此可知,ThreadLocalMap存的键key是ThreadLocal
因为main线程第一次执行ThreadLocal里的方法,显然main线程的threadLocals成员变量(lazy思想)是为空的,因此会走这一步逻辑,

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

这时main线程的threadLocals终于创建出来了。

我们再来看看当某个线程执行这行代码时,内部是怎样运行的,

THREAD_LOCAL.get()

get的源码如下

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();
    }

场景复原:main线程执行这行代码的逻辑为:因为main线程先执行的set方法,所以此时map(也就是main线程的threadLocals成员变量)已经初始化过了,不为null,所以最终可以得到值“”;
而thread线程它的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;
    }

此时thread线程的threadLocals成员变量也终于初始化了,不过是一个空的map集合,所以最后得到的值为null。

现在,我们再来读读ThreadLocal的八股文:ThreadLocal是线程本地变量,每个线程保存一份,相互隔离…阿巴阿巴…是采用空间换时间…balabala…
面试官听了直摇头…

总结:调用ThreadLocal的set、get成员变量方法,操作的是当前执行这行代码的线程的ThreadLocal.ThreadLocalMap threadLocals成员变量因为每个线程都有这样一份成员变量,因此是互不干扰的,也即可以有多个线程都执行THREAD_LOCAL.set(“卷”);这行代码,影响的是线程各自的threadLocals成员变量,我们各自“卷”,你“卷“你的,他”卷“他的,嘿嘿。

拓展ThreadLocal

设置初始值

在初始化ThreadLocal时,想要给它设置个默认的初始值怎么实现?
还是以开头的代码为例,稍微改造下就好,

public class Main {

    public static final ThreadLocal<String> THREAD_LOCAL = ThreadLocal.withInitial(() -> "卷");

    public static void main(String[] args) {

//        THREAD_LOCAL.set("卷");

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " " + THREAD_LOCAL.get());
            }
        }, "thread");
        thread.start();

        System.out.println(Thread.currentThread().getName() + " " + THREAD_LOCAL.get());
    }
}

想想此时的运行结果是什么?
结果如下,

main 卷
thread 卷

现在main线程跟thread线程都没有调用ThreadLocal的方法设置”卷“值,确都能获取到”卷“值,让我们来看看究竟…
改动的代码为

public static final ThreadLocal<String> THREAD_LOCAL = ThreadLocal.withInitial(() -> "卷");

我们使用的ThreadLocal静态方法withInitial实例化ThreadLocal对象,

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

该方法需要一个函数式接口的参数,

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

返回的是一个SuppliedThreadLocal对象,
在这里插入图片描述
也是ThreadLocal里的静态内部类,不过它继承了ThreadLocal,并重写了initialValue方法,之前我们在分析ThreadLocal的get方法时,知道initialValue方法返回的是null值,现在被子类SuppliedThreadLocal重写返回的是supplier.get(),即我们传入的”卷“。

结论:使用ThreadLocal的静态方法withInitial实例化对象,可以传入默认值,返回的是ThreadLocal的子类SuppliedThreadLocal,默认值存储在SuppliedThreadLocal的成员变量supplier中。

继承父线程的ThreadLocal

我们经常听到父子线程的概念,比如上文中的main线程是父线程,thread线程便是main线程创建出来的子线程,main跟thread都是父子如此亲密的关系了,main存的ThreadLocal能否轻易的给子thread一份?

public class Main {

    public static final ThreadLocal<String> THREAD_LOCAL = new InheritableThreadLocal();

    public static void main(String[] args) {

        THREAD_LOCAL.set("卷");

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " " + THREAD_LOCAL.get());
            }
        }, "thread");
        thread.start();

        System.out.println(Thread.currentThread().getName() + " " + THREAD_LOCAL.get());
    }
}

想想程序的运行结果?
结果如下,

main 卷
thread 卷

同样main线程跟thread线程都能获取到”卷“值。
改动的代码为

public static final ThreadLocal<String> THREAD_LOCAL = new InheritableThreadLocal();

InheritableThreadLocal类长这样,
在这里插入图片描述
还记得前面说过Thread类有2个ThreadLocal成员变量,另外一个即inheritableThreadLocals在这里就派上用场了。
所以如果用InheritableThreadLocal实例化ThreadLocal变量,那么之后一系列的set、get操作的便是Thread的inheritableThreadLocals成员变量了。
再来看看线程初始化的关键源码,
在这里插入图片描述
针对以上程序,main线程由JVM帮我们创建,它是没有父线程的,所以不会走这段逻辑,main线程执行set方法之后,它的inheritableThreadLocals便会初始化,并且存储了”卷“,当初始化thread时,发现parentThread = main != null并且main.inheritableThreadLocals!=null,看源码可知boolean类型的变量inheritThreadLocals传递过来的是true值,因此会走这个逻辑,

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }
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) {
                        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++;
                    }
                }
            }
        }

即把父线程(main)的ThreadLocal.ThreadLocalMap inheritableThreadLocals变量copy了一份给子线程(thread),所以可以这么说,子线程继承了父线程的ThreadLocal。

进阶ThreadLocal

ThreadLocalMap与HashMap的异同

下面我们来研究ThreadLocalMap的源码,
ThreadLocalMap内部维护了一个Entry类型的数组,private Entry[] table;
Entry类如下,

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

Entry继承了WeakReference,所以它是弱引用类型,而它所关联的类型是ThreadLocal,背过八股文的都知道弱引用类型的特点:如果某个类型(在这里是ThreadLocal)被弱引用类型(即Entry)关联,GC后,不管内存足不足够,都会被回收。 这个特点是有可能导致内存泄漏的关键点,后面还有一小节着重分析原因。

ThreadLocalMap也就包含四个成员变量,如下,

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0

初始容量是16并且要求power of two,这点跟HashMap一样
它有两个构造方法,其中一个在上文讲解子线程继承父线程的ThreadLocal时分析过了,来看另外一个,

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

HashMap的阈值设置的是数组长度的3 / 4,即len * 0.75

首先来看看ThreadLocal是如何添加值的,

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.
            
			// 片段1
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

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

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

			// 片段3
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

片段1跟HashMap一样,通过key的hashCode与(len-1)做与运算得到桶下标 i ;在调用map.set(key, value)时,分两个场景,

  1. tab[i] == null,即key在数组中还不存在,这好办,跳过片段2,进入片段3,new一个Entry放入桶里就好
  2. tab[i] != null,此时会进入片段2的for循环中,如果k == key,说明这个key之前存储过,把value更新下结束;如果k为null(此时称这个k为过期的)
    就会调用replaceStaleEntry方法替换;(这里普及一下,stale的意思是过期的,老掉牙的意思)

在场景2里,如果k != key,即发生了hash冲突了,也叫hash碰撞,怎么办?解决呗,

private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

ThreadLocalMap采用线性探测的方法解决hash冲突,而HashMap用的拉链法
进入片段3满足一定条件会执行一个rehash方法,

private void rehash() {
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
        }

满足条件时(size >= threshold*0.75)进行扩容,HashMap的扩容条件是size > threshold,扩容后的大小都是原来的2倍

private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }

下面再来看看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);
        }

步骤差不多,也是先获得对应得数组下标,满足条件直接返回value,否则试探附近的下标,

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;
                if (k == null)
                    expungeStaleEntry(i);// 清除过期的entry
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

为啥这么处理呢?这个后文的内存泄漏有关,这里再普及一下,expungeStaleEntry方法的意思:消除过期的Entry,expunge消除,清除的意思。

总结:

  • ThreadLocalMap底层数据结构就单纯的数组,而HashMap数组+链表+红黑树
  • ThreadLocalMap与HashMap默认初始容量都为16且满足power of two,桶下标的计算方式也一样,即hash & (n - 1)
  • hash冲突处理方式:ThreadLocalMap使用线性探测法,HashMap使用拉链法
  • 扩容条件:ThreadLocalMap(threshold = len * 2 / 3 ,size >= threshold * 0.75),HashMap(threshold = len * 3 / 4,size > threshold),扩容后的大小都为原来的2倍

内存泄漏的原因及应对策略

引入百度百科对内存泄漏的定义:内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
通俗的讲就是不再使用的对象一直占据着内存,又清理不掉,久而久之,内存不够爆OOM了。
分析内存泄漏,首先要分析对象的生命周期,如何确定哪些对象是可以回收的,八股文开始了…引用计数…可达性…

每个Thread类都拥有ThreadLocalMap成员变量,因此这个map的生命周期同线程的生命周期,ThreadLocalMap里的Entry是弱引用类型,关联的类型是ThreadLocal

public class WeakReference<T> extends java.lang.ref.Reference<T> {

/**
 * Constructs a new instance of this class.
 *
 * @param		r	referent to track.
 * @param		q	queue to register to the reference object with.
 */
public WeakReference(T r, ReferenceQueue<? super T> q) {
	initReference(r, q);
}

/**
 * Constructs a new instance of this class.
 *
 * @param	r	referent to track.
 */
public WeakReference(T r) {
	initReference(r);
}
}

在这里插入图片描述

在我们初始化Entry时,会调用super(k);即这个方法,

void initReference (T r) {
	state = STATE_INITIAL;
	referent = r;
}

因此我们的ThreadLocal保存在Reference的成员变量referent中。调用entry.get()方法时其实调用的是Reference的get方法返回referent,从而得到key(ThreadLocal)。现在再读读八股文四大引用类型的特点容易理解很多。

因为Entry弱引用的特性,就有可能导致Entry[] 存在大量key为null(被GC清理掉了),而value不为null的情况,这样的entry在源码里被叫做stale eentry,此时的value就是没用的对象又清理不掉,从而引发内存泄漏。

我们普通开发人员都能想到的问题,编写源码的人会想不到?,其实源码里也做了一些相应的处理,
我们上面分析get、set方法时,会看到多出几个陌生的方法,如expungeStaleEntry、replaceStaleEntry、cleanSomeSlots等,这些方法的作用就是帮助我们清理stale entry,但这是随机的,并不可靠,比较保险的方法还是我们开发人员自己手动调用remove方法清理不需要的entry

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
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)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

总结:
使用ThreadLocal导致内存泄漏的原因是我们的ThreadLocal被弱引用关联,而Threadlocal
又被做为ThreadLocalMap的key,解决方法:手动调用ThreadLocal对象的remove方法。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
ThreadLocal源码Java中一个关键的类,它提供了一种在多线程环境下实现线程本地变量的机制。在JDK 8之前和之后,ThreadLocal的内部结构有所变化。ThreadLocal源码分为两部分:ThreadLocal类和ThreadLocalMap类。 ThreadLocal类是一个泛型类,它包含了两个核心方法:set()和get()。set()方法用于将一个值与当前线程关联起来,get()方法用于获取当前线程关联的值。 ThreadLocalMap类是ThreadLocal的内部类,它用于存储每个线程的本地变量。在JDK 8之前,ThreadLocalMap是通过线性探测法解决哈希冲突的,每个ThreadLocal对象都对应一个Entry对象,Entry对象包含了ThreadLocal对象和与之关联的值[2]。 在JDK 8之后,ThreadLocalMap的实现方式发生了改变。使用了类似于HashMap的方式,采用了分段锁的机制来提高并发性能。每个线程维护一个ThreadLocalMap对象,其中的Entry对象也是采用链表的形式来解决哈希冲突。 总结起来,ThreadLocal源码主要由ThreadLocal类和ThreadLocalMap类组成。ThreadLocal类提供了set()和get()方法来管理线程本地变量,而ThreadLocalMap类则负责存储每个线程的本地变量,并解决哈希冲突的问题。 史上最全ThreadLocal 详解 ThreadLocal源码分析_02 内核(ThreadLocalMap) 【JDK源码】线程系列之ThreadLocal 深挖ThreadLocal ThreadLocal原理及内存泄露预防 ThreadLocal原理详解——终于弄明白了ThreadLocal ThreadLocal使用与原理 史上最全ThreadLocal 详解。 ThreadLocal源码分析,主要有ThreadLocal源码以及ThreadLocal的内部结构在jdk8前后的变化。 使用方式非常简单,核心就两个方法set/get public class TestThreadLocal { private static final ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { try { threadLocal.set("aaa"); Thread.sleep(500); System.out.println("threadA:" threadLocal.get()); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { threadLocal.set("bbb"); System.out.println("threadB:" threadLocal.get()); } }).start(); } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

b17a

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值