目录
频道首页
ThreadLocal详解
收藏
1
Rocky-BCRJ 最近修改于 2023-04-23 14:56:22

@[toc]

ThreadLocal 简介

ThreadLocal 类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal 实例通常来说都是 private static 类型的,用于关联线程和线程上下文。

验证 ThreadLocal 隔离性

public class ThreadLocalDemo01 {

    ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    public static void main(String[] args) {
        ThreadLocalDemo01 threadLocalTest = new ThreadLocalDemo01();
        threadLocalTest.showTwoThread();
    }

    public void showTwoThread() {
        new Thread(new Runnable() {
            public void run() {
                testQuarantine();
            }
        },"t1").start();

        new Thread(new Runnable() {
            public void run() {
                testQuarantine();
            }
        },"t2").start();

        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void testQuarantine() {
        System.out.println(Thread.currentThread().getName() + " 线程未设置之前: " + threadLocal.get());
        threadLocal.set(Thread.currentThread().getName());
        try {
            System.out.println("线程: " + Thread.currentThread().getName() + " 已放入到 threadLocal");
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()  + " 线程设置之后: " + threadLocal.get());
    }
}

**控制台打印结果如下: **

t1 线程未设置之前: null
线程: t1 已放入到 threadLocal
t2 线程未设置之前: null
线程: t2 已放入到 threadLocal
t1 线程设置之后: t1
t2 线程设置之后: t2

由此可以看到,我们在线程中使用了 ThreadLocal 做到了线程之间彼此隔离的作用。

ThreadLocal 如何做到线程隔离

早期的 JDK 版本 ThreadLocal 设计如下图:

在这里插入图片描述

每个ThreadLocal 都创建一个Map,然后用线程作为 Mapkey,要存储的局部变量作为Mapvalue,从而达到使各个线程的局部变量隔离的效果。

JDK 8 优化设计方案 如下图:

在这里插入图片描述

每个Thread 维护一个ThreadLocalMap,这个Map的keyThreadLocal实例本身,value才是真正要存储的值Object

具体流程如下:

  1. 每个 Thread 线程内部都有一个 Map (ThreadLocalMap)

  2. Map 里面存储 ThreadLocal对象(key)和线程的变量副本(value)

  3. Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。

  4. 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

这样子的调整带来了什么好处

  1. Thread销毁之后,对应的 ThreadLocalMap 也会随之销毁,能减少内存的使用。

  2. 在实际运用当中, 往往 ThreadLocal的数量要少于 Thread 的数量。这样设计之后每个Map存储的Entry数量就会变少。

ThreadLocal 使用场景

由于 ThreadLocal 的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

  1. 共享变量

  2. 计算的中间结果共享给其他方法

    @TODO 代码演示

ThreadLocal 源码解析

ThreadLocal 相关代码

private final int threadLocalHashCode = nextHashCode();

private static AtomicInteger nextHashCode =
        new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

ThreadLocal#set

    public void set(T value) {
        Thread t = Thread.currentThread();
        // 获取当前线程下的 ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // 后续调用 ThreadLocalMap的set方法插入数据
            map.set(this, value);
        else
            // 第一次会创建 ThreadLocalMap
            createMap(t, value);
    }

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

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


 private void set(ThreadLocal<?> key, Object value) {
            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对象
                ThreadLocal<?> k = e.get();
                // 如果相同的ThreadLocal,直接更新即可
                if (k == key) {
                    e.value = value;
                    return;
                }
                // 当前的key=null, 则表示虚引用的ThreadLocal是被GC回收的状态
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            // 走到这里说明下标 i 的位置上,是没有元素的,所以可以直接将新建的 Entry元素插入
            tab[i] = new Entry(key, value);
            int sz = ++size;
             // cleanSomeSlots: 存在陈旧的 Entry 且已经被清除
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

ThreadLocalMap 相关代码

static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

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

        /**
         * 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

        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        /**
         * Increment i modulo len.
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * Decrement i modulo len.
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

// ThreadLocal#ThreadLocalMap 的构造方法
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 1. 初始化table, 默认数值为16 
            table = new Entry[INITIAL_CAPACITY];
            // 2. 获取下标
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 创建 Entry 对象并插入到 对应下标 i的 table 中
            table[i] = new Entry(firstKey, firstValue);
            // 调整容量大小
            size = 1;
            // 调整负载系数
            setThreshold(INITIAL_CAPACITY);
        }

        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++;
                    }
                }
            }
        }

ThreadLocal#get

     public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 得到当前 entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
     // 初始化(和Set流程一致,不懂老爷子为啥不再去调用一下set方法)
    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;
    }

        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            // 如果不为空且当前 key 相等
            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;
            // 循环遍历当前entry
            while (e != null) {
                ThreadLocal<?> k = e.get();
                // 找到即返回
                if (k == key)
                    return e;
                if (k == null)
                    // 清理陈旧的key
                    expungeStaleEntry(i);
                else
                    // 遍历下一个
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

ThreadLocal#remove

         public void remove() {\
         // 拿到当前线程的ThreadLocalMap
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
         }

        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            // 获取当前key的所属下标
            int i = key.threadLocalHashCode & (len-1);
            // 循环找出当前的key
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) 
            {
                // 若找到引用置为空并清理陈旧的entry
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

开放地址法

根据以上hash函数计算数组下标时,当遇到数据存放的冲突时就需要重新找到数组的其他位置。关于开放地址法通常有三种方法: 线性探测二次探测再哈希法

具体的区别这里先不展开, 只是先阐述一下开放地址链地址法的区别 :

1.开放地址法:容易产生堆积问题;不适于大规模的数据存储;散列函数的设计对冲突会有很大的影响;插入时可能会出现多次冲突的现象,删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂;结点规模很大时会浪费很多空间;

2.链地址法:处理冲突简单,且无堆积现象,平均查找长度短;链表中的结点是动态申请的,适合构造表不能确定长度的情况;相对而言,拉链法的指针域可以忽略不计,因此较开放地址法更加节省空间。插入结点应该在链首,删除结点比较方便,只需调整指针而不需要对其他冲突元素作调整。

ThreadLocalMap 采用开放地址法原因是什么?

ThreadLocal 往往存放的数据量不会特别大(而且key 是弱引用又会被垃圾回收,及时让数据量更小)。采用开放地址法简单的结构会更节省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也比较低。

探测式清理

ThreadLocal#expungeStaleEntry

        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len))
            {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

启发式清理

ThreadLocal#cleanSomeSlots

        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    // 探测式清理
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

内存泄漏分析

创建与 ThreadLocalMap 一样的结构测试代码演示

public class ThreadLocalDemo02 {


    public static void main(String[] args) {
        Student student = new Student(new Person("Rocky"));

        Person person = new Person("Rocky-BCRJ"); // 强引用
        Student s1 = new Student(person);
        System.gc();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("gc之后: " + student.get());
        System.out.println("gc之后是否为null: " + (student.get() == null));
        System.out.println("gc之后: " + student.get());
        System.out.println("===============测试强引用是否被gc========================");
        System.out.println("s1 gc之后: " + s1.get());
        System.out.println("s1 gc之后是否为null: " + (s1.get() == null));
        System.out.println("s1 gc之后: " + s1.get());
    }

}

class Person  {
    String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Person(String name) {
        this.name = name;
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize: " + this);
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
}

class Student  extends WeakReference<Person> {

    public Student(Person referent) {
        super(referent);
    }

}
finalize: Person{name='Rocky'}
gc之后: null
gc之后是否为null: true
gc之后: null
===============测试强引用是否被gc========================
s1 gc之后: Person{name='Rocky-BCRJ'}
s1 gc之后是否为null: false
s1 gc之后: Person{name='Rocky-BCRJ'}

内存泄漏代码演示

public class ThreadLocalForOOM {
    /**
     * -Xms50m -Xmx50m
     */
    static class OOMObject {
        private Long[] a = new Long[2 * 1024 * 1024];
    }

    final static ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 5, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>());

    final static ThreadLocal<OOMObject> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        for (int i = 0; i < 50; i++) {
            int finalI = i;
            pool.execute(() -> {
                threadLocal.set(new OOMObject());
                System.out.println("oom object--->" + finalI);
                OOMObject oomObject = threadLocal.get();
                System.out.println("oomObject---->" + oomObject);
                // threadLocal.remove(); // 记得remove 防止内存泄露,此时一定要在使用完remove
            });

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

我们运行上述代码,会出现 OOM 异常:Exception in thread "pool-1-thread-4" java.lang.OutOfMemoryError: Java heap space 。在使用 线程池 + threadLocal 之后,需要及时的进行 remove 删除操作避免内存泄漏问题。

如何给子线程传递父线程数据

前面也说了, ThreadLocal 具有线程隔离的特性, 再来温习一下叭!

public class ThreadLocalDemo03 {

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


    public static void main(String[] args) {
        threadLocal.set(Thread.currentThread().getName());
        System.out.println(Thread.currentThread().getName() + " 线程获取到: " + threadLocal.get());
        Thread t1 = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " 线程获取到: " + threadLocal.get());
        },"t1");
        t1.start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果输出

main 线程获取到: main
t1 线程获取到: null

方法1 InheritableThreadLocal

public class ThreadLocalDemo04 {

    static ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>();


    public static void main(String[] args) {
        threadLocal.set(Thread.currentThread().getName());
        System.out.println(Thread.currentThread().getName() + " 线程获取到: " + threadLocal.get());
        Thread t1 = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " 线程获取到: " + threadLocal.get());
        },"t1");
        t1.start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果输出

main 线程获取到: main
t1 线程获取到: main

需要注意

  1. 变量的传递是发生在线程创建的时候,如果不是新建线程,而是用了线程池里的线程,就不灵了

  2. 变量的赋值就是从主线程的map复制到子线程,它们的value是同一个对象,如果这个对象本身不是线程安全的,那么就会有线程安全问题

方法2 TransmittableThreadLocal

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>transmittable-thread-local</artifactId>
            <version>2.14.2</version>
        </dependency>
public class ThreadLocalDemo05 {

    static ThreadLocal<String> threadLocal = new TransmittableThreadLocal<>();
    static ExecutorService executorService = Executors.newFixedThreadPool(1);
    static ExecutorService executorService2 = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));

    public static void main(String[] args) {
        // 验证TTL 传递性
        threadLocal.set(Thread.currentThread().getName());
        System.out.println(Thread.currentThread().getName() + " 线程获取到: " + threadLocal.get());
        Thread t1 = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " 线程获取到: " + threadLocal.get());
        },"t1");
        t1.start();

        InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
        TransmittableThreadLocal<String> transmittableThreadLocal = new TransmittableThreadLocal<>();

        // 验证线程池传递
        for (int i = 0; i < 3; i++) {
            inheritableThreadLocal.set("inheritable     -value-> " + i);
            transmittableThreadLocal.set("transmittable -value-> " + i);
            executorService2.submit(() -> {
                System.out.println(transmittableThreadLocal.get());
            });
            executorService.submit(()->{
                System.out.println(inheritableThreadLocal.get());
            });
        }

        try {
            TimeUnit.SECONDS.sleep(1);
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
        finally {
            executorService.shutdown();
            executorService2.shutdown();
            inheritableThreadLocal.remove();
            transmittableThreadLocal.remove();
        }
    }
}

结果演示

main 线程获取到: main
t1 线程获取到: main
transmittable -value-> 0
transmittable -value-> 1
transmittable -value-> 2
inheritable     -value-> 0
inheritable     -value-> 0
inheritable     -value-> 0

参考博客

参考1: https://xiaohuang.blog.csdn.net/article/details/129979690

参考2: https://www.jianshu.com/p/3bb70ae81828

参考3: https://blog.csdn.net/u010445301/article/details/111322569

参考4: https://blog.csdn.net/wxy941011/article/details/80740716

参考5: https://blog.csdn.net/A_art_xiang/article/details/125889527

参考6: https://baijiahao.baidu.com/s?id=1736593241526122157&wfr=spider&for=pc

文末

相信大家在看完这篇文章之后 对于 ThreadLocal 有了比较清晰的地方, 如果还有不懂的地方可以评论留言, 我们可以一起探讨研究一下。同时文章中还有许多地方写得不好不够详细,还请多多包涵, 也希望你发现错误问题的同时能及时像博主反馈以免误人子弟, 有些不够详细的地方我也会在后续的文档中慢慢完善, 最后也想说的是, 我会努力提升自己,写出更优质的文章出来。谢谢大家。

内容大纲
批注笔记
ThreadLocal详解
ArticleBot
z
z
z
z
主页
Git管理
文章
云文档
留言墙
AI文档