Java多线程之ThreadLocal原理总结

2023-05-15 08:05:45 java 原理 多线程

1、什么是 ThreadLocal:

ThreadLocal,即线程本地变量,如果你创建了一个变量,那么访问这个变量的每个线程都会有这个变量的本地拷贝,多个线程操作这个变量的时候,实际操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题

ThreadLocal 适用于无状态,副本变量独立后不影响业务逻辑的高并发场景,如果业务逻辑强依赖于变量副本,则不适合用 ThreadLocal 解决,需要另寻解决方案

应用场景:

数据库连接池会话管理中使用

2、ThreadLocal 的数据结构:

jdk8 中,每个线程 Thread 内部都维护了一个 ThreadLocalMap 的数据结构,ThreadLocalMap 中有一个由内部类 Entry 组成的 table 数组,Entry 的 key 就是线程的本地化对象 ThreadLocal,而 value 则存放了当前线程所操作的变量副本。每个 ThreadLocal 只能保存一个副本 value,并且各个线程的数据互不干扰,如果想要一个线程保存多个副本变量,就需要创建多个ThreadLocal。

一个 ThreadLocal 的值,会根据线程的不同,分散在 N 个线程中,所以获取 ThreadLocal 的 value,有两个步骤:

第一步,根据线程获取 ThreadLocalMap

第二步,根据自身从 ThreadLocalMap 中获取值,所以它的 this 就是 Map 的 Key

当执行 set() 方法时,其值是保存在当前线程的 ThreadLocal 变量副本中
当执行get() 方法中,是从当前线程的 ThreadLocal 的变量副本获取。

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

3、ThreadLocal 的核心方法:

ThreadLocal 对外暴露的方法有4个:

1.initialValue()方法:返回为当前线程初始副本变量值。
2.get()方法:获取当前线程的副本变量值。
3.set()方法:保存当前线程的副本变量值。
4.remove()方法:移除当前前程的副本变量值

 1、set()方法:

// 设置当前线程对应的ThreadLocal值
public void set(T value) {
    Thread t = Thread.currentThread(); // 获取当前线程对象
    ThreadLocalMap map = getMap(t);
    if (map != null) // 判断map是否存在
        map.set(this, value); 
        // 调用map.set 将当前value赋值给当前threadLocal。
    else
        createMap(t, value);
        // 如果当前对象没有ThreadLocalMap 对象。
        // 创建一个对象 赋值给当前线程
}
 
// 获取当前线程对象维护的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
// 给传入的线程 配置一个threadlocals
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

执行流程:

1.获得当前线程,根据当前线程获得 map。
2.如果 map 不为空,则将参数设置到 map 中,当前的 Threadlocal 作为 key。
3.如果 map 为空,则给该线程创建 map,设置初始值。

 2、get()方法:

public T get() {
    Thread t = Thread.currentThread();//获得当前线程对象
    ThreadLocalMap map = getMap(t);//线程对象对应的map
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);// 以当前threadlocal为key,尝试获得实体
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 如果当前线程对应map不存在
    // 如果map存在但是当前threadlocal没有关连的entry。
    return setInitialValue();
}
 
// 初始化
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;
}

执行流程:

(1)先尝试获得当前线程,再根据当前线程获取对应的 map
(2)如果获得的 map 不为空,以当前 threadlocal 为 key 尝试获得 entry
(3)如果 entry 不为空,返回值。
(4)如果 2 跟 3 出现无法获得,则通过 initialValue 函数获得初始值,然后给当前线程创建新 map

 3、remove()方法:

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

执行流程:

首先尝试获取当前线程,然后根据当前线程获得map,从map中尝试删除enrty。

4、initialValue() 方法:

protected T initialValue() {
    return null;
}

执行流程:

(1)如果没有调用 set() 直接 get(),则会调用此方法,该方法只会被调用一次,
(2)默认返回一个缺省值null,如果不想返回null,可以Override 进行覆盖。

4、ThreadLocal 的哈希冲突的解决方法:线性探测

HashMap 不同,ThreadLocalMap 结构中没有 next 引用
ThreadLocalMap 中解决哈希冲突的方式并非链表的方式,而是采用线性探测的方式,当发生哈希冲突时就将步长加1或减1,寻找下一个相邻的位置

流程说明:

1.根据 ThreadLocal 对象的 hash 值,定位到 table 中的位置 i;

2.如果当前位置是 null,就初始化一个 Entry 对象放在位置 i 上;

3.如果位置 i 已经有 Entry 对象了,如果这个 Entry 对象的 key 与即将设置的 key 相同,那么重新设置 Entry 的 value;

4.如果位置 i 的 Entry 对象和 即将设置的 key 不同,那么寻找下一个空位置;

5、ThreadLocal 的内存泄露:

在使用 ThreadLocal 时,当使用完变量后,必须手动调用 remove() 方法删除 entry 对象,否则会造成 value 的内存泄露,严格来说,ThreadLocal 是没有内存泄漏问题,有的话,那也是忘记执行 remove() 引起的

内存泄露的根本原因在于 ThreadLocalMap 的生命周期与当前线程 CurrentThread 的生命周期相同,且 ThreadLocal 使用完没有进行手动删除导致的

ThreadLocal 的内存泄露与强弱引用无关,那么为什么还要用弱引用呢?

(1)Entry 中的 key(Threadlocal)是弱引用,目的是将 ThreadLocal 对象的生命周期跟线程周期解绑,用 WeakReference 弱引用关联的对象,只能生存到下一次垃圾回收之前,GC发生时,不管内存够不够,都会被回收。

(2)当我们使用完 ThreadLocal,而 Thread 仍然运行时,即使忘记调用 remove() 方法, 弱引用也会比强引用多一层保障:当 GC 发生时,弱引用的 ThreadLocal 被收回,那么 key 就为 null 了。而 ThreadLocalMap 中的 set()、get() 方法,会针对 key == null (也就是 ThreadLocal 为 null) 的情况进行处理,如果 key == null,则系统认为 value 也应该是无效了应该设置为 null,也就是说对应的 value 会在下次调用 ThreadLocal 的 set()、get() 方法时,执行底层 ThreadLocalMap 中的 expungeStaleEntry() 方法进行清除无用的 value,从而避免内存泄露。

6、ThreadLocal 的应用场景:

(1)Hibernate 的 session 获取:每个线程访问数据库都应当是一个独立的 session 会话,如果多个线程共享同一个 session 会话,有可能其他线程关闭连接了,当前线程再执行提交时就会出现会话已关闭的异常,导致系统异常。

使用 ThreadLocal 的方式能避免线程争抢session,提高并发安全性。

(2)spring事务管理:事务需要保证一组操作同时成功或失败,意味着一个事务的所有操作需要在同一个数据库连接上,Spring 采用 Threadlocal 的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时采用这种方式可以使业务层使用事务时不需要感知并管理 connection 对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复

7、如果想共享线程的 ThreadLocal 数据怎么办 ?

使用 InheritableThreadLocal 可以实现多个线程访问 ThreadLocal 的值

我们在主线程中创建一个 InheritableThreadLocal 的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。

private void test() {    
final ThreadLocal threadLocal = new InheritableThreadLocal();       
threadLocal.set("主线程的ThreadLocal的值");    
Thread t = new Thread() {        
    @Override        
    public void run() {            
      super.run();            
      Log.i( "我是子线程,我要获取其他线程的ThreadLocal的值 ==> " + threadLocal.get());        
    }    
  };          
  t.start(); 
} 

8、为什么一般用 ThreadLocal 都要用 static?

ThreadLocal 能实现线程的数据隔离,不在于它自己本身,而在于 Thread 的 ThreadLocalMap,所以,ThreadLocal 可以只实例化一次,只分配一块存储空间就可以了,没有必要作为成员变量多次被初始化。

到此这篇关于Java多线程之ThreadLocal原理总结的文章就介绍到这了,更多相关Java多线程ThreadLocal内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

相关文章