ThreadLocal 原理与实战

目录

一、ThreadLocal 的基本使用

1、ThreadLocal 的成员方法

2、小案例: 

二、ThreadLocal 使用场景

1、线程隔离

2、跨函数传递数据

三、ThreadLocal 原理和源码分析

1、set(T value)方法

2、get( )方法

3、remove()方法

4、initialValue( ) 方法

四、ThreadLocalMap 源码分析

1、set(ThreadLocal key, Object value) 源码解析: 

2、Entry 的 Key 需要使用弱引用

五、 什么是弱引用呢?

六、编程规范推荐使用 static final 修饰 ThreadLocal 对象 


一、ThreadLocal 的基本使用

ThreadLocal 是位于 JDK 的 java.lang 核心包中。
如果程序创建了一个 ThreadLocal 实例,那么在访问这个变量的值时,每个线程都会拥有一个独立的、自己的本地值。
“线程本地变量”可以看成专属于线程的变量,不受其他线程干扰,保存着线程的专属数据。当线程结束后,每个线程所拥有的那一个本地值也会被释放。
在多线程并发操作“线程本地变量”时候,线程各自操作的是自己的本地值,从而规避了线程安全问题。

1、ThreadLocal 的成员方法

方法说明
public void set(T var1)设置当前线程在“线程本地变量”实例中绑定的本地值
public T get()获得当前线程在“线程本地变量”实例中绑定的本地值
public void remove()移除当前线程在“线程本地变量”实例中绑定的本地值

2、小案例: 

public class ThreadLocalDemo {
    static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + ":" + threadLocal.get());
        //清除本地内存中的本地变量
        threadLocal.remove();
    }
    public static void main(String[] args) {
        Thread thread1 = new Thread(){
            @Override
            public void run() {
                //设置线程1中本地变量的值
                threadLocal.set("thread1");
                //调用打印方法
                print("thread1");
            }
        };
        Thread thread2 = new Thread(){
            @Override
            public void run() {
                threadLocal.set("thread2");
                print("thread2");
            }
        };
        thread1.start();
        thread2.start();
    }
}

输出:

thread1:thread1
thread2:thread2

二、ThreadLocal 使用场景

1、线程隔离

ThreadLocal 的主要价值在于线程隔离, ThreadLocal 中数据只属于当前线程,其本地值对别的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。另外,由于各个线程之间的数据相互隔离,避免同步加锁带来的性能损失,大大提升了并发性的性能。

2、跨函数传递数据

通常用于同一个线程内,跨类、跨方法传递数据时,如果不用 ThreadLocal ,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度。

三、ThreadLocal 原理和源码分析

1、set(T value)方法

set(T value) 方法用于设置“线程本地变量”在当前线程的 ThreadLocalMap 中对应的值,相当于设置线程本地值,其核心源码如下:
public void set(T var1) {
        //获取当前线程对象
        Thread var2 = Thread.currentThread();
        //获取当前线程的 ThreadLocalMap 实例属性
        ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
        //判断 map 是否存在
        if (var3 != null) {
            //value 被绑定到 threadLocal 实例
            var3.set(this, var1);
        } else {
            this.createMap(var2, var1);
        }
    }
ThreadLocal.ThreadLocalMap getMap(Thread var1) {
        return var1.threadLocals;
    }
//创建一个 ThreadLocalMap 成员 
//并为新的 Map 成员设置第一个 Key-Value 对,Key 为当前的 ThreadLocal 实例
void createMap(Thread var1, T var2) {
        var1.threadLocals = new ThreadLocal.ThreadLocalMap(this, var2);
    }

2、get( )方法

get( ) 方法用于获取“线程本地变量”在当前线程的 ThreadLocalMap 中对应的值,相当于获取线程本地值,其核心源码如下:
public T get() {
        // 获得当前线程对象
        Thread var1 = Thread.currentThread();
        // 获得线程对象的 ThreadLocalMap 内部成员
        ThreadLocal.ThreadLocalMap var2 = this.getMap(var1);
        // 如果当前线程的内部 map 成员存在
        if (var2 != null) {
            // 以当前 threadlocal 为 Key,尝试获得条目
            ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
            // 条目存在
            if (var3 != null) {
                Object var4 = var3.value;
                return var4;
            }
        }
        // 如果当前线程对应 map 不存在或者 map 存在,但是当前 threadlocal 实例没有对应的 Key-Value,返回初始值
        return this.setInitialValue();
    }
// 设置 threadlocal 关联的初始值并返回
    private T setInitialValue() {
        //调用初始化钩子函数,获取初始值
        Object var1 = this.initialValue();
        Thread var2 = Thread.currentThread();
        ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
        if (var3 != null) {
            var3.set(this, var1);
        } else {
            this.createMap(var2, var1);
        }

        return var1;
    }

3、remove()方法

remove() 方法用于在当前线程的 ThreadLocalMap 中,移除“线程本地变量”所对应的值,其核心源码如下:
 public void remove() {
        ThreadLocal.ThreadLocalMap var1 = this.getMap(Thread.currentThread());
        if (var1 != null) {
            var1.remove(this);
        }

    }

4、initialValue( ) 方法

当“线程本地变量”在当前线程的 ThreadLocalMap 中尚未绑定值时, initialValue( ) 方法用于获取初始值。其源码如下:
protected T initialValue() {
        return null;
    }
JDK 已经为大家定义 了一个 ThreadLocal 的内部 SuppliedThreadLocal 静态子类,并且提供了 ThreadLocal.withInitial(…) 静态工厂方法,方便大家在定义 ThreadLocal 实例时设置初始值回调函数。使用工厂方法构造ThreadLocal 实例的代码如下:
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "初始值");
SuppliedThreadLocal的源码如下:
 public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> var0) {
        return new ThreadLocal.SuppliedThreadLocal(var0);
    }
//内部静态子类 
//继承了 ThreadLocal,重写了 initialValue()方法,返回钩子函数的值作为初始值
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
        //保存钩子函数
        private final Supplier<? extends T> supplier;
        //传入钩子函数
        SuppliedThreadLocal(Supplier<? extends T> var1) {
            this.supplier = (Supplier)Objects.requireNonNull(var1);
        }

        protected T initialValue() {
            return this.supplier.get();
        }
    }

四、ThreadLocalMap 源码分析

static class ThreadLocalMap {
        // Map 的条目初始容量 16
        private static final int INITIAL_CAPACITY = 16;
        // Map 的条目数组,作为散列表使用
        private ThreadLocal.ThreadLocalMap.Entry[] table;
        // Map 的条目数量
        private int size;
        // 扩容因子
        private int threshold;
        ...省略
        // Map 的条目类,一个静态的内部类 
        // Entry 继承 WeakReference,Key 为 ThreadLocal 实例
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> var1, Object var2) {
                super(var1);
                this.value = var2;
            }
        }
    }
ThreadLocal 源码中 get ()、 set ( )、 remove ()方法都涉及到 ThreadLocalMap 的方法调用,主要调用了 ThreadLocalMap 的如下几个函数:
  • set(ThreadLocal<?> key, Object value) :向 Map 实例设置“Key-Value 对”。
  • getEntry(ThreadLocal):从 Map 实例获取 KeyThreadLocal 实例)所属的 Entry
  • remove(ThreadLocal):根据 KeyThreadLocal 实例)从 Map 实例移除所属的 Entry

1、set(ThreadLocal<?> key, Object value) 源码解析: 

private void set(ThreadLocal<?> key, Object value) {
        Entry[] tab = table;
        int len = tab.length;
        //根据 key 的 HashCode,找到 key 在数组上的槽点 i
        int i = key.threadLocalHashCode & (len-1);
        // 从槽点 i 开始向后循环搜索,找空余槽点(空余位置)或者找现有槽点
        // 如果没有现有槽点,则必定有空余槽点,因为没有空间时会扩容
        for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
            // 找到现有槽点:Key 值为 ThreadLocal 实例
            if (k == key) {
                e.value = value;
                return;
            }
            // 找到异常槽点:槽点被 GC 掉,重设 Key 值和 Value 值
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        // 没有找到现有的槽点,增加新的 Entry
        tab[i] = new Entry(key, value);
        // 设置 ThreadLocal 数量
        int sz = ++size;
        // 没有可清理的 Entry,并且现有条目数量大于扩容因子值,进行扩容
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

2、Entry Key 需要使用弱引用

Entry 用于保存 ThreadLocalMap 的“ Key-Value ”条目,但是 Entry 使用了对 Threadlocal 实例进行包装之后的弱引用(WeakReference )作为 Key ,其代码如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> var1, Object var2) {
                super(var1);
                this.value = var2;
            }
        }
问题:
为什么 Entry 需要使用弱引用对 Key 进行包装,而不是直接使用 Threadlocal 实例作为 Key呢?
通过分析以下示例来找到答案
public void fun(){
        // 创建一个线程本地变量
        ThreadLocal threadLocal= new ThreadLocal<Integer>();
        // 设置值
        threadLocal.set(999);
        // 获取值
        threadLocal.get();
        //末尾
    }
当线程thread 执行 fun 方法到其末尾时,线程 thread相关的 JVM 栈内存以及内 ThreadLocalMap 成员的结构为:

线程 thread调用 fun()方法,新建了一个 ThreadLocal 实例,并使用 threadLocal 局部变量指向这个实例,并且此 threadLocal是强引用;在调用 threadLocal.set(999) 之后,线程 thread的 ThreadLocalMap 成员内部 会新建一个 Entry 实例,其 Key 以弱引用包装的方式指向 ThreadLocal 实例。
当线程 thread执行完 fun方法后,fun的方法栈帧将被销毁,强引用 threadLocal 的值也就没有了,
但此时线程的 ThreadLocalMap 里的对应的 Entry Key 引用还指向了 ThreadLocal 实例。
Entry 的 Key 引用是强引用,就会导致 Key 引用指向的 ThreadLocal 实例、及其 Value 值都不能被 GC 回收,这将造成严重的内存泄露,如下图:

五、 什么是弱引用呢?

仅有弱引用(WeakReference)指向的对象,只能生存到下一次垃圾回收之前。

由于 ThreadLocalMap Entry Key 使用了弱引用,在下次 GC 发生时,就可以使那些没
有被其他强引用指向、仅被 Entry Key 所指向的 ThreadLocal 实例能被顺利回收。并且,在 Entry 的 Key 引用被回收之后,其 Entry Key 值变为 null 。后续当 ThreadLocal get set remove 被调用时,ThreadLocalMap 的内部代码会清除这些 Key null Entry ,从而完成相应的内存释放。总结一下,使用 ThreadLocal 会发生内存泄漏的前提条件:
  • 线程长时间运行而没有被销毁。线程池中的 Thread 实例很容易满足此条件。
  • ThreadLocal 引用被设置为 null,且后续在同一 Thread 实例的执行期间,没有发生对其ThreadLocal 实例的 getset remove 操作。

六、编程规范推荐使用 static final 修饰 ThreadLocal 对象 

ThreadLocal 实例作为 ThreadLocalMap Key ,针对一个线程内所有操作是共享的,所以建议设置 static 修饰符,以便被所有的对象共享。由于静态变量会在类第一次被使用时装载,只会分配一次存储空间,此类的所有实例都会共享这个存储空间,所以使用 static 修饰 ThreadLocal 就会节约内存空间。另外,为了确保 ThreadLocal 实例的唯一性,除了使用 static修饰之外,还会使用 final 进行加强修饰,以防止其在使用过程中发生动态变更。参考的实例如下:
   final static ThreadLocal<String> TD = new ThreadLocal<>();
凡事都有两面性,使用 static final 修饰 ThreadLocal 实例也会带来副作用:这使得 Thread 实例内部的 ThreadLocalMap Entry Key Thread 实例的生命期内将始终保持为非 null ,从而导致 Key 所在的 Entry 不会被自动清空,这就会导致 Entry 中的 Value 指向的对象一直存在强引用,Value 指向的对象在线程生命期内不会被释放,最终导致内存泄露。所以,使用 static final修饰 TheadLocal 实例,使用完后必须使用 remove ()进行手动释放。
如果使用线程池,可以定制线程池的 afterExecute 方法(任务执行完成之后的钩子方法),在任务执行完成之后,调用 TheadLocal 实例的 remove()方法对其手动释放,从而实现的其线程内部的 Entry 得到释放,参考的代码如下:

private static final ThreadLocal<Long> START_TIME= new ThreadLocal<>();
ExecutorService threadPoolExecutor = new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2)) {
    //异步任务执行完成之后的钩子方法
    @Override protected void afterExecute(Runnable target, Throwable t) {
        //清空 TheadLocal 实例的本地值
        START_TIME.remove();
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值