关于ThreadLocal原理分析及其使用

目录

一、ThreadLocal的作用

二、ThreadLocal使用

三、ThreadLocal原理

四、ThreadLocal内存泄露问题

五、ThreadLocal的应用场景

六、总结


一、ThreadLocal的作用

背景:多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步

问题:同步的措施一般是加锁,但加锁会在一定程度上增加系统的复杂度以及影响系统的性能。

解决:多线程间共享变量的线程安全,ThreadLocal应运而生。当创建一个ThreadLocal变量时,访问这个变量的每个线程都有这个变量的一个本地副本,当多个线程操作这个变量时,实际上就是操作自己本地内存里面的变量,从而避免了线程安全问题

二、ThreadLocal使用

讲完了理论的东西,我们来通过下面的例子体会下ThreadLocal的神奇之处吧

public class Test {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
 
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            threadLocal.set("本地变量1");
            print("thread1");
        });
 
        Thread thread2 = new Thread(() -> {
            threadLocal.set("本地变量2");
            print("thread2");
        });
 
        thread1.start();
        thread2.start();
    }
 
    public static void print(String thread){
        System.out.println(thread+":"+threadLocal.get());
    }
}

执行结果如下:

现象:不同线程之间的ThreadLocal的值是独立开

本质:ThreadLocal的set 与get 方法,可以理解为其实就是操作当前线程的一个里面一个属性变量

三、ThreadLocal原理

1、对Thread对象的里面关键属性变量的理解->ThreadLocal.ThreadLocalMap

一个实例对象中实例成员字段的内容肯定是这个对象独有的ThreadLocal线程本地变量作为一个Thread类的成员字段

ThreadLocal.ThreadLocalMap threadLocals = null;

​ 是一个在ThreadLocal中定义的Map对象,保存了该线程中的所有本地变量。ThreadLocalMap中的Entry的定义如下:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    // key为一个ThreadLocal对象,v就是我们要在线程之间隔离的对象
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

2、ThreadLocal常用方法

  • set(T value):设置线程本地变量的内容。
  • get():获取线程本地变量的内容。
  • remove():移除线程本地变量。注意在线程池的线程复用场景中在线程执行完毕时一定要调用remove避免在线程被重新放入线程池中时被本地变量的旧状态仍然被保存。

3、ThreadLocal.set方法原理

set方法源码如下:

public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的threadLocals字段
    ThreadLocalMap map = getMap(t);
    // 判断线程的threadLocals是否初始化了
    if (map != null) {
        map.set(this, value);
    } else {
        // 没有则创建一个ThreadLocalMap对象进行初始化
        createMap(t, value);
    }
}

createMap方法的源码如下:

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

map.set方法的源码如下:

/**
* 往map中设置ThreadLocal的关联关系
* set中没有使用像get方法中的快速选择的方法,因为在set中创建新条目和替换旧条目的内容一样常见,
* 在替换的情况下快速路径通常会失败(对官方注释的翻译)
*/
private void set(ThreadLocal<?> key, Object value) {
    // map中就是使用Entry[]数据保留所有的entry实例
    Entry[] tab = table;
    int len = tab.length;
    // 返回下一个哈希码,哈希码的产生过程与神奇的0x61c88647的数字有关
    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;
        }
        if (k == null) {
            // 在设置期间清理哈希表为空的内容,保持哈希表的性质
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 扩容逻辑
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

4、ThreadLocal.get方法原理

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 获取ThreadLocal对应保留在Map中的Entry对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 获取ThreadLocal对象对应的值
            T result = (T)e.value;
            return result;
        }
    }
    // map还没有初始化时创建map对象,并设置null,同时返回null
    return setInitialValue();
}

5、ThreadLocal.remove方法原理

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    // 键在直接移除
    if (m != null) {
        m.remove(this);
    }
}

6、类结构图

image-20201227212035680

 7、ThreadLocal的设计

  • 每个Thread线程内部都有一个Map(ThreadLocalMap::threadlocals);
  • Map里面存储ThreadLocal对象(key)和线程的变量副本(value);
  • Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置变量值;
  • 对于不同的线程,每次获取副本值时,别的线程不能获取当前线程的副本值,就形成了数据之间的隔离

 8、使用ThreadLocal好处

  • 保存每个线程绑定的数据,在需要的地方可以直接获取,避免直接传递参数带来的代码耦合问题;
  • 各个线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。

四、ThreadLocal内存泄露问题

​内存泄露问题:指程序中动态分配的堆内存由于某种原因没有被释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢或者系统奔溃等严重后果。内存泄露堆积将会导致内存溢出

image-20220101200417534

① TreadLocal使用弱引用原因:如果强引用的话会导致 ThreadLocal无法回收

② 使用完ThreadLocal必须remove原因:由于ThreadLocalMap中任然存在Current Thread Ref这个强引用,因此Entry中value的值任然无法清除。还是存在内存泄露的问题。(线程重复利用情况或用到线程池)

五、ThreadLocal的应用场景

场景一:在重入方法中替代参数的显式传递

例如:在Spring的@Transaction事务声明的注解中就使用ThreadLocal保存了当前的Connection对象,避免在本次调用的不同方法中使用不同的Connection对象。

场景二:全局存储用户信息

例如:可以尝试使用ThreadLocal替代Session的使用,当用户要访问需要授权的接口的时候,可以现在拦截器中将用户的Token存入ThreadLocal中;之后在本次访问中任何需要用户用户信息的都可以直接冲ThreadLocal中拿取数据

六、总结

① ThreadLocal更像是对其他类型变量的一层包装,通过ThreadLocal的包装使得该变量可以在线程之间隔离和当前线程全局共享。

② 线程的隔离性和变量的线程全局共享性得益于在每个Thread类中的threadlocals字段。(从类实例对象的角度抽象的去看Java中的线程!!!)

③ ThreadLocalMap中Entry的Key不管是否使用弱引用都有内存泄露的可能。引起内存泄露主要在于ThreadLocal对象和Entry中的Value对象,因此要确保每次使用完之后都remove掉Entry!
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值