ThreadLocal-线程安全利器

news2024/12/24 2:56:43

原文链接:https://www.jylt.cc/#/detail?activityIndex=2&id=9df3fd62d6ee13ff555c30157798b092

ThreadLocal是什么

ThreadLocal用来提供线程内部的局部变量,是各个线程独有的,该变量无法被其他线程访问。主要有以下作用:

  • 解决共享变量线程安全问题
  • 线程生命周期内全局参数传递

其中最典型的应用是对数据库连接池的处理。可以参考这篇文章阅读:ThreadLocal在数据库连接中的应用

ThreadLocal怎么用

ThreadLocal主要有以下几个方法:

public void set(T value); // 存值
public T get(); // 取值
public void remove(); // 移除线程局部变量的引用

ThreadLocal底层原理

多线程的时候 ThreadLocal 为什么能够做到线程数据的隔离呢?原因是由于每个线程Thread都维护了一个 ThreadLocal.ThreadLocalMap 的引用,而 ThreadLocalMap 就是存放 ThreadLocal 值的地方。引用关系如下图:https://oss.jylt.cc/img/content/32ef85ebfc3b3eb8ef83903ee3757cc0/a09b1f73-34ce-4394-b78c-4697d1a1683f.png

下面根据源码来解释以下上面的引用关系。

Thread类

public class Thread implements Runnable {
    // Thread拥有ThreadLocal.ThreadLocalMap的引用
    ThreadLocal.ThreadLocalMap threadLocals = null
} 

ThreadLocal 类结构

注意上图的红线部分和 Entry extends WeakReference。Entry的key被包装成了弱引用是什么原因呢?

首先要知道弱引用的作用,我们都知道平时我们创建对象 Object o = new Object() ,这种方式是强引用,在对象 o使用完之前,该对象是不会被垃圾回收的,因为该对象是可达状态;该对象使用完之后,是可以被垃圾回收的,因为该对象是不可达的。如果 o是弱引用对象,并且没有其他强引用对象对其引用时,不管任何收执行GC,对象 o都会被垃圾回收掉。

可以看出红线部分的设计是为了防止key长时间无法被GC,导致内存溢出。

public class ThreadLocal<T> {
	// ThreadLocal的内部类,这个也就是上面Thread里面持有的threadLocals对象
	static class ThreadLocalMap {
	// Entry是具体存放ThreadLocal数据的容器,可以发现Entry的数据结构跟Hash Map的是比较像的,都是<key,value>形式。此处的Entry的key是ThreadLocal对象,下面会说到
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
  
	// table是Entry数组的原因是,每个线程可能会有多个ThreadLocal对象,这个时候,需要将不同ThreadLocal对象对应的值放到不同下标的Entry数组中,具体如何存放的下面会说到
	private Entry[] table;
    }
}

方法解析:

set(T value)

该方法是向ThreadLocal中存放值的,如下:

ThreadLocal<Integer> tl = new ThreadLocal<>();
tl.set(1);

具体设置值的逻辑:

public void set(T value) {
	// 获取当前线程对象
	Thread t = Thread.currentThread();
	// 获取当前线程引用的 threadLocals 对象
	ThreadLocalMap map = getMap(t);
	if (map != null)
		// 设置值
		map.set(this, value);
	else
		// 创建新的引用
		createMap(t, value);
}


ThreadLocalMap getMap(Thread t) {
	// 获取当前线程持有的threadLocals
	return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
	// 初始化一个 ThreadLocalMap 赋值给当前线程的 threadLocals
	t.threadLocals = new ThreadLocalMap(this, firstValue);
}

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
  
  	// 计算数据应该存放的位置
    int i = key.threadLocalHashCode & (len-1);

  	// 在进行设置值的时候,为了解决hash冲突,使用了 线性探测法
  	// 如果第 i 个位置已经有值了,则判断下一个位置有没有值,没有值则将数据放入该位置
	// 整个循环的意思是,从上面获取的hash下标开始向后遍历,在遍历过程中如果当前下标的Entry没有值,如果有值,判断Entry的key是不是当前threadLocal对象,如果是,则给当前ThreadLocal设置新的value;如果Entry的key为空,说明该Entry已经没有引用的ThreadLoca了,无法再被访问到,将该无效Entry移除,然后赋值新的key和value
    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) {
        // 说明当前的 threadLocal 对象已经被GC清理,移除失效的 Entry,下面会说到
        replaceStaleEntry(key, value, i);
        return;
      }
    }
    // 说明当前下标的Entry还没有值,初始化一个新Entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
      rehash();
 }

T get()

public T get() {
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null) {
    // map.getEntry 通过循环遍历的方式查找当前 ThreadLocal
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
      @SuppressWarnings("unchecked")
      T result = (T)e.value;
      return result;
    }
  }
  return setInitialValue();
}

private Entry getEntry(ThreadLocal<?> key) {
     int i = key.threadLocalHashCode & (table.length - 1);
     Entry e = table[i];
     if (e != null && e.get() == key)
	// Entry的key==当前threadLocal,说明是要查询的Entry
         return e;
      else
	// 通过线性探测法,循环获取下一个下标的Entry,并判断是不是目标Entry
          return getEntryAfterMiss(key, i, e);
}

void remove()

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

private void remove(ThreadLocal<?> key) {
  Entry[] tab = table;
  int len = tab.length;
  int i = key.threadLocalHashCode & (len-1);
  for (Entry e = tab[i];
       e != null;
       e = tab[i = nextIndex(i, len)]) {
    if (e.get() == key) {
      // 移除 key 的引用
      e.clear();
      // 移除 value 的引用
      expungeStaleEntry(i);
      return;
    }
  }
}
  • 注意事项

使用完 ThreadLocal 之后, 一定要手动调用 remove 方法 ,不然可能会导致内溢出。前面说了 Entry 里的 key 是弱引用对象,可以避免了内存溢出。但是 value 是强引用对象,如果 value 的对象还被其他对象引用,value 会一直不被 GC 回收,如果这样的 value 比较多的时候,会导致内存溢出。

value可能被长时间引用的原因是Thread的生命周期要比对象的生命周期长的多,在整个生命周期内,可能会创建了许许多多的ThradLocal,这时value对象就会特别多,而且不会被垃圾回收。

移除失效 Entry 的逻辑

private int expungeStaleEntry(int staleSlot) {
  Entry[] tab = table;
  int len = tab.length;

  // 将当前 Entry 的 value 置为 null
  tab[staleSlot].value = null;
  // 将当前 Entry 置为 null
  tab[staleSlot] = null;
  // Entry 数量 -1
  size--;
  
  Entry e;
  int i;
  // 通过线性探测法将 table 中所有失效的 Entry 都做清理
  for (i = nextIndex(staleSlot, len);
       (e = tab[i]) != null;
       i = nextIndex(i, len)) {
    ThreadLocal<?> k = e.get();
    if (k == null) {
      e.value = null;
      tab[i] = null;
      size--;
    } else {
      int h = k.threadLocalHashCode & (len - 1);
      if (h != i) {
        tab[i] = null;
        while (tab[h] != null)
          h = nextIndex(h, len);
        tab[h] = e;
      }
    }
  }
  return i;
}

ThreadLocal 子线程无法继承父线程数据问题

如果在线程中创建了子线程,那么子线程与父线程的 ThreadLocal 数据是不能共享的,比如下面的代码:

public static void main(String[] args) {
  ThreadLocal<Integer> local = new ThreadLocal<>();
  local.set(1);
  System.out.println("父线程get=" + local.get());

  new Thread(() -> {
    System.out.println("子线程get=" + local.get());
  }).start();
}

// 输出结果:
// 父线程get=1
// 子线程get=null

如何在子线程中使用父线程 ThreadLocal 数据呢?可以使用 InheritableThreadLocal,如下代码:

InheritableThreadLocal<Integer> local1 = new InheritableThreadLocal<>();
local1.set(1);
System.out.println("父线程get1=" + local1.get());

new Thread(() -> {
  System.out.println("子线程get1=" + local1.get());
}).start();

// 打印结果
// 父线程get1=1
// 子线程get1=1

其原理是因为在调用 Thread 的构造方法的时候,会将父线程的局部变量赋值给子线程,实现了在子线程能够使用到父线程数据。

但这种方法不能在线程池中使用,线程池中的线程不一定是当前线程创建的。

public class Thread implements Runnable {
  
  ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  
  public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
  }
  
  private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
    init(g, target, name, stackSize, null, true);
  }
  
   private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
     Thread parent = currentThread();
     //  parent.inheritableThreadLocals 的值是在调用 set 方法时设置的
     if (inheritThreadLocals && parent.inheritableThreadLocals != null)
       		// 将父线程的局部变量赋值给子线程
            this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
   }
}

线程池中如何使用父线程的ThreadLocal局部变量呢

可以参考阿里的开源项目:Gitee 极速下载/transmittable-thread-local

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/708708.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

pyqt5实现线程与弹窗功能

pyqt5实现线程与弹窗功能 效果图&#xff1a; 示例下载 点我下载 https://download.csdn.net/download/lm_is_dc/87982279 简介 Pyqt5线程使用 QThread, pyqtSignal, QMutex, QWaitCondition来实现&#xff0c;涉及到线程&#xff0c;锁&#xff0c;信号量&#xff0c;线程…

深入理解计算机系统(2)_计算机基本组成

深入理解计算机系统系列文章目录 第一章 计算机的基本组成 1. 内容概述 2. 计算机基本组成 第二章 计算机的指令和运算 第三章 处理器设计 第四章 存储器和IO系统 文章目录 深入理解计算机系统系列文章目录前言参考资料一、组成架构&#xff08;冯/图&#xff09;1. 组成架构2…

42. 接雨水

题目链接&#xff1a;力扣 解题思路&#xff1a;从左往右按列进行计算&#xff0c;依次计算每一列能够接到的雨水数量。对于当前列能够接到的雨水数量是由左右两边最高的两根柱子决定的&#xff0c;类似于木桶定律&#xff08;一只水桶能装多少水取决于它最短的那块木板&#x…

AI实践-定制你的专属简历-软件测试

简历对于找到一份理想的工作至关重要。但是&#xff0c;准备简历这一过程却让人感到头疼和繁琐&#xff0c;而且如何突出自己的优势&#xff0c;也是许多求职者遇到困惑。 chatgpt不会受到情感和个人喜好的影响&#xff0c;能够通过算法和数据分析为您编写最合适的简历。并且可…

目标检测算法-YOLOV5解析(附论文与源码)

目标检测算法-YOLOV5解析&#xff08;附论文与源码&#xff09;

数据结构 | 顺序二叉树

一、数据结构定义 1、顺序二叉树 /* 顺序二叉树 */ typedef char TreeType; typedef struct seqTree{int MAXNUM; // 最大元素个数int curNum; // 元素的个数TreeType nodelist[]; // 顺序二叉树节点存储 } *SeqTree; 本次代码中二叉树的结构如下图&#xff0c;用层次序列可…

【Python】字典

文章目录 一. 字典的创建二. 字典的操作1. 查找 key2. 新增键值对3. 删除键值对4. 遍历字典4.1 使用 for 循环遍历字典4.2 通过方法遍历字典keys() 获取到字典中所有 keyvalues() 获取到字典中的所以 valueitems 获取到字典中的所有键值对 三. 理解字典操作的效率 一. 字典的创…

Linux--打印文件内容:cat

cat是cater的简写 语法&#xff1a; cat [选项] [文件] 常用选项&#xff1a; -b 对非空输出行编号 -n 对输出的所有行编号 -s 不输出多行空行 示例&#xff1a; ①打印文件hello.c的内容 ②带行号打印文件hello.c的内容 ③输入什么&#xff0c;打印什么 ④输入重定向&…

讲座笔记:如何撰写高质量科技论文

1 论文总体思路 2 摘要 3 Intro 常见Introduction逻辑&#xff1a; 说明问题是什么&#xff1b;简单罗列前人工作&#xff1b;描述我们的工作。 说明问题是什么&#xff1b;目前最好的工作面临什么挑战&#xff1b;我们的方法能缓解上述挑战 3.1 段落写法 首先列出几句话 …

【 Linux】文件删除原理

文章目录 Linux文件删除原理文件的索引节点和链接文件删除的过程文件删除后是否能恢复 Linux文件删除原理 Linux是一个强大的操作系统&#xff0c;它提供了许多命令和工具来管理文件和目录。其中&#xff0c;最常用的命令之一就是rm&#xff0c;它可以用来删除不需要的文件或目…

Less简明教程

一.概述 Less是一种动态样式语言&#xff0c;它在CSS的基础上扩展了混合、嵌套、变量等实用功能。Less也是一种CSS预处理语言&#xff0c;less文件在经过less.js处理后&#xff0c;最终会生成.css文件&#xff0c;如下图所示&#xff1a; 1.动态样式语言的比较 动态样式语言主…

git push报错rejected:no-fast-forward

报错&#xff1a; 报错关键词&#xff1a; non-fast-forwardyour current branch is behindthe remote changes 即&#xff1a;不能快速前进、当前分支滞后、远端改变 原因&#xff1a; 这个分支下&#xff0c;别人提交了一些代码到远程仓库。对于这个改变&#xff0c;你没有…

SpringBoot+微信小程序在线订餐小程序系统 附带详细运行指导视频

文章目录 一、项目演示二、项目介绍三、运行截图四、主要代码 一、项目演示 项目演示地址&#xff1a; 视频地址 二、项目介绍 项目描述&#xff1a;这是一个基于SpringBoot微信小程序框架开发的在线订餐小程序系统。首先&#xff0c;这是一个前后端分离的项目&#xff0c;代…

什么是 Kubernetes 服务器端应用 (SSA)?

自 2021 年 8 月 v1.22 版本发布以来,服务器端应用 (SSA) 已在 Kubernetes 中普遍可用。这是一种声明式资源管理策略,可通过将命令逻辑移至kubectl apply服务器来改进 diff 计算并警告合并冲突。 本文将解释 SSA 的工作原理以及为什么它比以前的客户端应用 (CSA) 方法更受青…

Python3 面向对象 | 菜鸟教程(十六)

目录 一、面向对象技术简介 &#xff08;一&#xff09;类(Class) &#xff08;二&#xff09;方法 &#xff08;三&#xff09;类变量 &#xff08;四&#xff09;数据成员 &#xff08;五&#xff09;方法重写 &#xff08;六&#xff09;局部变量 &#xff08;七&am…

并发-抽象队列同步器AQS应用Lock详解

锁的膨胀是指synchronized原本是无锁态&#xff0c;当有一个线程调用时变为偏向锁&#xff0c;当有多个线程排队自旋等待锁时会升级为轻量锁&#xff0c;当线程等待时间太长时会升级为重量级锁&#xff0c;这就是锁的膨胀过程&#xff0c;且是不可逆的。 锁的粗化是指如果在一个…

【总结】1727- 前端开发中如何高效地模拟数据?

&#x1f449; 「文章推荐」 详细聊一聊 Vue3 动态组件深入浅出 Vue3 自定义指令6 个你必须明白 Vue3 的 ref 和 reactive 问题初中级前端必须掌握的 10 个 Vue 优化技巧分享 15 个 Vue3 全家桶开发的避坑经验 在开发和测试工作中&#xff0c;mock 数据非常实用。mock 数据是指…

基于值的深度强化学习算法

目录 DQN2013 —— Playing Atari with Deep Reinforcement LearningDQN2015 —— Human-level control through deep reinforcement learning引用文献 DQN2013 —— Playing Atari with Deep Reinforcement Learning 论文下载地址 论文介绍 该论文提出了一个基于卷积神经网络…

数字IC验证环境的创建

本文介绍了从一组可重用的验证组件中构建测试平台所需的步骤。UVM促进了重用&#xff0c;加速了测试平台构建的过程。 首先对测试平台集成者&#xff08;testbench integrator&#xff09;和测试编写者&#xff08;test writer &#xff09;进行区分&#xff0c;前者负责测试平…

【Java EE】-博客系统二(前后端分离)

作者&#xff1a;学Java的冬瓜 博客主页&#xff1a;☀冬瓜的主页&#x1f319; 专栏&#xff1a;【JavaEE】 分享: 徘徊着的 在路上的 你要走吗 易碎的 骄傲着 那也曾是我的模样 ——《平凡之路》 主要内容&#xff1a;显示用户信息、上传头像、新增博客、删除博客、修改博客…