十分钟带你了解清楚什么是ThreadLocal

目录

1、概述

2、ThreadLocal与sync的区别

3、使用方法

4、原理分析

1、set方法

2、ThreadLocalMap

3、get方法

4、remove方法

5、总结

5、使用场景

1、Spring实现事务隔离级别

2、解决日期的线程安全

3、多个方法调用

4、JDBC的数据库连接

6、常见问题

1、ThreadLocal对象存放在哪里

2、如何共享ThreadLocal数据

3、为什么把ThreadLocalMap的键弄成弱引用

4、ThreadLocal的内存泄漏问题

5、ThreadLocal与Thread、ThreadLocalMap


1、概述

ThreadLocal叫做“线程变量”,其中填充的变量属于当前线程,对其他线程是隔离的,不会被别的线程读取或修改。这种思路叫线程封闭。

原理是,每个线程内部都有一个ThreadLocalMap,它用ThreadLocal作为键,能够存储值。

ThreadLocal适合,每个线程需要有自己的实例,且该实例需要在多个方法间传递,但不希望线程间共享。

  • 实例需要在多个方法间传递,就可以保存在当前线程的ThreadLocal中,就不需要通过参数传递了。需要时直接get()取出。

2、ThreadLocal与sync的区别

它们都用于解决多线程的并发访问,区别是:

  • synchronized用于线程间的数据共享,而ThreadLocal用于线程内的数据共享,线程间的数据隔离

  • synchronized利用锁机制,让变量或代码块在同一时刻只能被一个线程访问。

    而ThreadLocal在每个线程内都提供了变量的副本,每个线程只能操作自己内部的副本,避免了共享问题

3、使用方法

ThreadLocal的变量通常用private static修饰

public class ThreadLocalDemo {
    private static ThreadLocal<String> localVar = new ThreadLocal<>();

    //打印出本线程内的localVar值
    public static void printLocalVar(String str){
        System.out.println(str + " " + localVar.get());
        //清除本地内存中的本地变量
        localVar.remove();
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            ThreadLocalDemo.localVar.set("t1的localVar");
            printLocalVar("t1");
        }, "t1");
        t1.start();

        Thread.sleep(1000);

        Thread t2 = new Thread(() -> {
            ThreadLocalDemo.localVar.set("t2的localVar");
            printLocalVar("t2");
        }, "t2");
        t2.start();
    }
}

注意:

  • ThreadLocal的泛型是Object的,可以定义成map、set、list等等
  • 一个类中可以定义多个ThreadLocal属性

4、原理分析

1、set方法

 

赋值过程:

  • 首先获取当前线程对象,并获取当前线程的ThreadLocalMap
  • 如果这个map为null,就调用createMap()方法初始化map,初始化需要传入泛型和要存储的值
  • 如果map不为null,就调用set的重载方法,它会将当前线程对象作为键,存储值。

2、ThreadLocalMap

createMap()

 其实线程对象的threadLocals属性,就是用来存储ThreadLocalMap的。

 ThreadLocalMap是ThreadLocal的静态内部类。它用Entry保存数据,而且继承了弱引用。

 Entry内部使用ThreadLocal类型的变量作为键,保存传入的值。

ThreadLocalMap如何工作

 

这个Map使用哈希确定下标,将值保存在数组中,类似于HashMap。但没有实现Map接口,也没有链表结构。

一个线程只有一个ThreadLocalMap,但是可以创建多个ThreadLocal字段,所以需要使用数组存储每个Entry。

不使用链表,它解决哈希冲突的方式是,找空隙:

  • 要插入一组数据,根据ThreadLocal对象的哈希值,计算出一个下标
  • 如果该下标对应的位置是空的,就初始化一个Entry,存入数据
  • 如果不为空,就检查它的key,如果正好和要存入的key一样,此次是覆盖操作,直接替换Value
  • 如果不为空且key不符,说明出现了哈希冲突,就找下一个空的位置,继续判断,直到成功插入。
  • 在get的时候也是,如果下标中的key不符,说明插入时有哈希冲突,就找下一个位置,直到找到key

3、get方法

 

获取流程:

  • 获取当前线程对象
  • 如果map不为null,就通过ThreadLocal对象,取出对应的Entry
  • 如果entry不为空,就获取Entry中的Value,返回。
  • 如果前一步中map为空,就调用setInitialValue()方法

setInitialValue()

这个方法是给ThreadLocal设置初始值

4、remove方法

将ThreadLocal的值,从当前线程的ThreadLocalMap中删除。

5、总结

ThreadLocal的值,存储在当前线程对象的threadLocals属性中,这个属性对应一个ThreadLocalMap对象,在第一次调用ThreadLocal的set方法时被初始化。

ThreadLocalMap保存对象的策略是,以ThreadLocal为键,映射存储值。

这个ThreadLocal是多线程共享的,而ThreadLocalMap是线程私有的,所以每个线程都可以根据ThreadLocal存储不同的值,别的线程也无法获取到。

5、使用场景

1、Spring实现事务隔离级别

Spring使用ThreadLocal的方式,保证一个线程中的数据库操作都是使用的同一个连接对象

private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);

private static final ThreadLocal<Map<Object, Object>> resources =
    new NamedThreadLocal<>("Transactional resources");

private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
    new NamedThreadLocal<>("Transaction synchronizations");

private static final ThreadLocal<String> currentTransactionName =
    new NamedThreadLocal<>("Current transaction name");

……

2、解决日期的线程安全

项目中有部分用户的时间出错,发现是多个线程共享一个SimpleDataFormat的问题。

使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add()。

如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。

但是每个线程内部都new一个SimpleDataFormat对象也不太好,所以使用ThreadLocal包装SimpleDataFormat,解决了线程安全的问题。

3、多个方法调用

一个线程经常需要横跨多个方法调用,那么它的参数就必须层层传递,给每个方法都加上相同的参数不太优雅。

而且,如果中间遇到第三方类库,参数就无法传递了。可以使用ThreadLocal,开始时把参数存进去,需要时直接get取出即可。

4、JDBC的数据库连接

从数据库连接池里获取的连接 Connection,在 JDBC 规范里并没有要求这个 Connection 必须是线程安全的。

数据库连接池通过线程封闭技术,保证一个 Connection 一旦被一个线程获取之后,在这个线程关闭 Connection 之前的这段时间里,不会再分配给其他线程,从而保证了 Connection 不会有并发问题

6、常见问题

1、ThreadLocal对象存放在哪里

Java中,栈内存是线程私有的,堆内存是线程共享的。

ThreadLocal对象存放在堆上。

2、如何共享ThreadLocal数据

使用InheritableThreadLocal,可以实现主线程和子线程共享ThreadLocal数据。

在主线程new一个InheritableThreadLocal的实例,子线程就可以获取到它的值。它也是ThreadLocal类型。

final ThreadLocal threadLocal = new InheritableThreadLocal(); 

3、为什么把ThreadLocalMap的键弄成弱引用

减少了内存泄漏。

存储了一个对象到ThreadLocalMap中,如果后来把该ThreadLocal类型的对象设置为了null,就只有ThreadLocalMap的key引用了这个对象。

  • 如果定义成弱引用,当ThreadLocal失去所有外部强引用,下次GC它就能被垃圾回收,对应的Entry的Key将置为null。
  • 如果使用强引用,那么在已经失去该对象的所有外部引用的情况下,它仍将一直存在,无法被垃圾回收。
  • ThreadLocalMap的生命周期和线程实例一致,只要线程一直运行,ThreadLocalMap就不会销毁,所以Key也不会销毁,可能导致内存泄露。

但是,把Key做成弱引用也无法完全避免内存泄漏,因为Value是强引用的,所以需要及时清理才行。

4、ThreadLocal的内存泄漏问题

在线程池中使用ThreadLocal,需要考虑内存泄露问题。

ThreadLocalMap中,Entry的Key是ThreadLocal对象的弱引用。如果一个对象只存在弱引用,那它在下一次GC中一定会被清理。

所以,如果ThreadLocal没有外部的强引用,在垃圾回收时,key会被清理掉,但是Entry的Value是强引用,只有线程结束后才能被回收。

  • 因为线程对象通过强引用指向ThreadLocalMap,而ThreadLocalMap也是通过强引用指向Entry,所以Entry的value是强引用。

    key不再使用,被清理掉了,就没有任何途径能访问到这个value,所以value属于垃圾。

ThreadLocalMap在实现时考虑了这种情况,因此调用set()、get()时,会清理掉键为null的Entry对象。

保险起见,使用完ThreadLocal的值后,就手动调用remove()方法,把值全部清理掉,这样value就能被垃圾回收了。

调用set、get清理对象具体的流程是:

  • 调用ThreadLocal的get(),它会先获取当前线程对象的ThreadLocalMap
  • 调用ThreadLocalMap的getEntry(),它会调用哈希函数,计算出一个数组下标。
  • 如果发生了哈希冲突(下标的key不等于所需的key),就调用getEntryAfterMiss()
  • ThreadLocalMap使用开放定址法解决哈希冲突,即向后寻找所需的key值。
  • 期间如果遇到key为null的Entry,就会调用expungeStaleEntry(),将key为null的Entry的value也设置为null

5、ThreadLocal与Thread、ThreadLocalMap

Thread有一个threadLocals属性,存放一个线程私有的ThreadLocalMap类型变量。

ThreadLocalMap是ThreadLocal的静态内部类。它类似Map,使用Entry存放数据,key为ThreadLocal对象。

ThreadLocal相当于ThreadLocalMap的工具类。调用ThreadLocal对象的get、set方法,底层是在调用ThreadLocalMap的get、set方法

ThreadLocal帮助ThreadLocalMap初始化。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值