【JUC】二十四、线程局部变量ThreadLocal

本文详细介绍了ThreadLocal的作用,如何实现线程隔离,以及在多线程环境下的使用和注意事项,包括线程池中的ThreadLocal管理。重点讨论了Thread、ThreadLocal和ThreadLocalMap之间的关系以及内存泄漏问题的处理。
摘要由CSDN通过智能技术生成

System.out.println("分割线======")

近期有些新的理解,编辑补充下:

0、回头补充

0.1 使用

成员变量,本来有线程安全问题,用ThreadLocal包装一下,则可实现每个线程都有自己的独立副本。 如用JDBC操作数据库时,会将各自的Connection对象用ThreadLocal包装,从而保证每个线程都在自己的Connection上操作数据库(ThreadLocal关键词:线程间隔离)

在这里插入图片描述

实例代码:开两个线程,分别操作成员变量threadLocal,ThreadLocal里包了一个String对象,发现两个线程各自有各自的String值

public class ThreadLocalTest {
	//成员变量,ThreadLocal包个String
    static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(() -> {
            String name = Thread.currentThread().getName();
            threadLocal.set("111");
            print(name);
            System.out.println(name + "-after remove : " + threadLocal.get());
        }, "t1").start();
        new Thread(() -> {
            String name = Thread.currentThread().getName();
            threadLocal.set("222");
            print(name);
            System.out.println(name + "-after remove : " + threadLocal.get());
        }, "t2").start();
    }

    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + threadLocal.get());
        //清除本地内存中的本地变量
        threadLocal.remove();
    }

}

运行:

t1 :111
t2 :222
t1-after remove : null
t2-after remove : null

可以看出,ThreadLocal包装的对象,在每个线程中都有自己的值,而多个线程之间又是互相隔离的,t1线程存储的,t1线程可以取,t2取不了。t2线程存储的,t2线程可以取,t1取不了。

0.2 源码

ThreadLocal让每个线程只操作自己内部的值,从而实现线程数据隔离。ThreadLocal的结构如下,其有个内部类ThreadLocalMap,而ThreadLocalMap中有个table属性,是一个数组,数组里存着一个个的Entry对象。

在这里插入图片描述

而每个线程对象,又有ThreadLocal.ThreadLocalMap类型的属性,即每个线程对象,都有一个ThreadLocalMap对象。调用ThreadLocal属性对象的set、get、remove方法时,都是操作的当前线程对象的ThreadLocalMap的Entry类型的数组,这也是ThreadLocal对象实现线程隔离的关键,普通对象的set、get改的是普通对象自己,而ThreadLocal对象set、get改的当前线程对象的属性。

在这里插入图片描述

ThreadLcoal对象的set方法

先获取当前线程对象Thread,然后获取线程对象Thread的ThreadLocal.ThreadLocalMap类型的属性threadLocals,线程对象一开始,其threadLocals为默认的null

在这里插入图片描述

于是走到createMap方法,传入当前线程对象Thread t 和要set的值value。

createMap方法里在创建ThreadLocalMap对象,传入ThreadLocal对象以及要set的值value,并把返回结果赋值给Thread t 的ThreadLocalMap对象。

创建ThreadLocalMap对象的方法里,先初始化了一个容量为16的Entry数组,再根据ThreadLocal的哈希值计算出放入数组的哪个位置,然后创建以ThreadLocal对象为key,以要set的值value的Entry对象,放入这个位置。

到此,线程对象Thread t 的ThreadLocalMap对象有值了,且ThreadLocalMap对象的Entry数组属性里,放了我要set的value。

在这里插入图片描述
当然了,一个业务里可以有多个ThreadLocal对象,因此,Entry是个数组。下次再set,Thread t 的ThreadLcoalMap属性就不是null了,走map.set(this, value)分支,传入ThreadLocal对象和要set的值:

在这里插入图片描述

核心逻辑差不多,还是根据ThreadLcoal对象,确定下这个value存线程对象Thread t 的ThreadLocalMap对象的Entry数组的哪个位置,如果是同一个ThreaLocal对象,那这次set就是更新值,如果是另一个ThreadLcoal类型的对象,那也就又存了一个value

ThreadLcoal对象的get方法

还是先获取当前线程对象Thread t ,以及Thread t 的ThreadLcoalMap属性,如果ThreadLcoalMap不为空,传入当前ThreadLocal对象,调用getEntry方法。

getEntry方法中,根据ThreadLcoal对象的哈希值,定位到是Entry数组的哪一个位置,返回这个Entry对象e,e.value即是我当时set存进去的值

在这里插入图片描述

有十个ThreadLocal对象,调用其set、get、remove方法,实际上操作的是当前线程对象Thread的ThreadLocalMap属性的Entry类型的数组,Entry对象以ThreadLcoal对象为key,以set的值为value,把这十个ThreadLocal对象分别哈希定位安排到Entry数组。

Thread、ThreadLocal、ThreadLocalMap的Entry数组,分别就像人、人的各种卡片(如身份证、学生证)、存各种卡片的卡包。每个人都有一个自己的卡包,卡包里装的卡片外形都一样(类比ThreadLcoal类型的成员变量),但卡片上面记录的信息是私有的,每个人的都不同(类比每个线程给ThreadLcoal类型的成员变量赋的值都不同)。

在这里插入图片描述

0.3 核心理解

每个线程对象,又有ThreadLocal.ThreadLocalMap类型的属性,即每个线程对象,都有一个ThreadLocalMap对象。调用ThreadLocal属性对象的set、get、remove方法时,都是操作的当前线程对象的ThreadLocalMap的Entry类型的数组,这也是ThreadLocal对象实现线程隔离的关键,普通对象的set、get改的是普通对象自己,而ThreadLocal对象set、get改的当前线程对象的属性。

System.out.println("分割线======")

0.4 之前的一点疑惑

之前想,HashMap线程不安全,是因为其允许多个线程同时对其读和写,因此,可能导致多个线程put时同时触发resize扩容(进而出现数据重新哈希散列时丢数据等)、put方法非原子性(执行期间可被其他线程打断)、迭代器失效导致遍历抛异常等问题。

那用ThreadLocal包装一下HashMap岂不是没安全问题了,但这样一个线程对 HashMap 的更改对其他线程不可见,那你还不如用HashMap类型的局部变量。因此,并发下,想处理这个,还是用ConcurrentHashMap。

此外,和局部变量来对比:

在这里插入图片描述

局部变量在栈,方法执行结束就销毁,不能实现跨方法或者跨类。而ThreadLocal的出现,解决了成员变量的共享竞争问题,同时实现了同一个线程内的资源共享,可以跨类、跨方法取值。

1、ThreadLocal

ThreadLocal,即线程局部变量,ThreadLocal类型的变量与正常的变量不同,因为每一个线程在访问ThreadLocal类型的变量的时候(通过其get或set方法),都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将某些状态(例如用户ID或事务ID)与一个个线程关联起来

实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他线程共享,人人有份,人各一份),主要解决了让每个线程绑定自己的值,通过使用get和set方法,获取、设置当前线程所存的副本的值,从而避免了线程安全问题,比如之前synchronized的8种case,资源类是使用同一部手机,多个线程抢夺同一部手机使用,假如可以每个线程人手一份就天下太平了。

在这里插入图片描述

对于线程T,有些信息,没必要写回主内存,也不需要和别的线程共享。比如下面这个游戏里,三个玩家,对应三个线程,玩家的生命值、攻击值等值就属于这类数据。

在这里插入图片描述

一句话,记录只属于线程t自身的一些东西。

2、常用方法

  • 构造方法
ThreadLocal()
  • 返回当前线程的ThreadLocal中的值
T get()
  • 返回此ThreadLocal的当前线程的初始值
protected T initialValue();
  • 删除此ThreadLocal的当前线程的初始值
void remove();
  • 设置ThreadLocal的当前线程副本值
void set(T value)
  • 创建一个ThreadLocal
static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)

创建ThreadLocal对象的方式:

使用构造方法创建时,建议重写写initialValue方法,给一个ThreadLocal的初始值,因为源码中return的是null,用起来可能导致空指针。

在这里插入图片描述

ThreadLocal<Integer> saleValue = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 0;   //指定ThreadLocal的初始值
        }
};

也可使用上面的静态方法,传入一个供给式函数接口,指定初始值:

ThreadLocal<Integer> saleValue = ThreadLocal.withInitial(() -> 0);

以上两种方法一个意思,后者更清爽。

3、案例

需求:5个销售卖房子,集团高层只关心销售总量的准确统计数,按照总销售额统计,方便集团公司给部门发送奖金。

//资源类
public class House {

    int saleCount = 0;
    public synchronized void saleHouse(){
        ++saleCount;
    }

}
@Slf4j
public class ThreadLocalDemo {
    public static void main(String[] args) throws InterruptedException {
        House house = new House();
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                //随机数模拟每个销售卖出房子的数量
                int number = new Random().nextInt(6) + 1;
                log.info("销售{}卖出{}套", Thread.currentThread().getName(), number);
                for (int j = 1; j <= number; j++) {
                    house.saleHouse();
                }
                countDownLatch.countDown();
            }, String.valueOf(i)).start();

        }
        countDownLatch.await();
        log.info("该部门总共卖出房子数量:{}", house.saleCount);
    }
}

在这里插入图片描述

现在需求变更,希望部门里的所有销售各自分灶吃饭,即各凭销售本事提成,按照出单数各自统计每个人的销售额:

//资源类
public class House {

    int saleCount = 0;
    public synchronized void saleHouse(){
        ++saleCount;
    }
    ThreadLocal<Integer> saleValue = ThreadLocal.withInitial(() -> 0);
    //操作的是ThreadLocal变量,每个线程一份,无安全问题一说,自然无关synchronized
    public void saveValueByThreadLocal(){
        saleValue.set(saleValue.get() + 1);
    }
    

}

上面操作的是共享资源类的ThreadLocal变量,每个线程一份,因此无安全问题一说,自然无关synchronized。

在这里插入图片描述

运行:

在这里插入图片描述

可以看到ThreadLocal计算线程自身的属性值,不加锁,结果依然正确。操作ThreadLocal类型的对象或者属性,并发下不加锁,结果也正确,因为它是每个线程有自己的一份值。

4、线程池下必须remove掉线程的LocalThread值

必须回收自定义的 ThreadLocal变量的值,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑结果,以及造成内存泄露等问题。 可使用try-finally 块进行回收。

PS:内存泄漏,即已经申请的内存空间无法释放,就会造成内存泄漏,例如线程死循环、资源不关闭等,大量的内存泄漏堆积就会造成内存不够,从而发生内存溢出。

//正例
myThreadLocal.set(xxx);
try{

	//...
	
}finally{

	myThreadLocal.remove();
	
}

比如,上面案例可改为:

在这里插入图片描述

以上Demo里的线程每次都是新建的,所以这个无影响。如果是线程池,则remove是必须的。先看不加remove:

//错误写法,不remove
@Slf4j
public class ThreadLocalDemo2 {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(3);
        MyData myData = new MyData();
        for (int i = 1; i <= 10; i++) {
            System.out.println(pool.submit(() -> {
                log.warn("线程==> {}", Thread.currentThread().getName());
                log.info("初始数据:{}", myData.threadLocal.get());
                myData.add();
                log.info("改完后数据:{}", myData.threadLocal.get());
            }, String.valueOf(i)));

        }
    }
}

结果累加:

在这里插入图片描述

加了remove:

在这里插入图片描述

说白了,每个线程保存的ThreadLocal类型的值,用完后不remove,下一个http请求进来,又从线程池把刚才用过的线程拿来用,就会用到上一个请求的旧的ThreadLocal类型的值

5、Thread、ThreadLocal、ThreadLocalMap

大致看下三者的关系:

在这里插入图片描述

在这里插入图片描述

即:Thread类中包含ThreadLocal.ThreadLocalMap类型的属性,而ThreadLocalMap是ThreadLocal的静态内部类。也就是说,每New一个线程对象,就有一个ThreadLocal.ThreadLocalMap类型的属性的值

在这里插入图片描述

即:

ThreadLocalMap实际就是一个以ThreadLocal为key,传入的值为value的Entry对象。当给ThreadLocal类型变量赋值,就是以当前ThreadLocal实例对象为key,值为value的Entry放入这个ThreadLocalMap中。

资源类中有一个ThreadLocal类型的属性,即每个线程Thread过来,都会有
在这里插入图片描述
Thread、ThreadLocal、ThreadLocalMap可类比成:人、人的身份证、身份证上的信息,更确切的说,ThreadLocalMap是身份证为key:名字为value的键值对(ThreadLocal为key,赋的值为value)

ThreadLocalMap从字面上就可以看出这是一个保存ThreadLoca对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象:

在这里插入图片描述

而JVM内部,则是维护了一个线程版的Map<ThreadLocal,Value>(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key放进了ThreadLoalMap中),每个线程要用到这个T的时候,用当前的线程去Map里面获取,如此,每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,没有竞争一说,在并发模式下是绝对安全的变量。

6、Thread、ThreadLocal、ThreadLocalMap的关系

在这里插入图片描述

Thread 中持有一个ThreadLocalMap ,这里你可以简单理解为就是持有一个数组,这个数组是Entry 类型的。 Entry 的key 是ThreadLocal 类型的,value 是Object 类型。 也就是一个ThreadLocalMap 可以持有多个ThreadLocal。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

-代号9527

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

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

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

打赏作者

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

抵扣说明:

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

余额充值