ThreadLocal使用与原理

一、ThreadLocal简介


ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景

下图可以增强理解:

二、ThreadLocal原理

ThreadLocal变量只在单个线程内可见,那它是如何做到的呢?我们先从最基本的get()方法说起:

public T get() {//获得当前线程Thread t = Thread.currentThread();//每个线程 都有一个自己的ThreadLocalMap,//ThreadLocalMap里就保存着所有的ThreadLocal变量ThreadLocalMap map = getMap(t);if (map != null) {//ThreadLocalMap的key就是当前ThreadLocal对象实例,//多个ThreadLocal变量都是放在这个map中的ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")//从map里取出来的值就是我们需要的这个ThreadLocal变量T result = (T)e.value;return result;}}// 如果map没有初始化,那么在这里初始化一下return setInitialValue();
}

可以看到,所谓的ThreadLocal变量就是保存在每个线程的map中的。这个map就是Thread对象中的threadLocals字段。如下:

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal.ThreadLocalMap是一个比较特殊的Map,它的每个Entry的key都是一个弱引用:

static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;//key就是一个弱引用Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}

这样设计的好处是,如果这个变量不再被其他对象使用时,可以自动回收这个ThreadLocal对象,避免可能的内存泄露。

三、ThreadLocal中的内存泄漏问题

虽然ThreadLocalMap中的key是弱引用,当不存在外部强引用的时候,就会自动被回收,但是Entry中的value依然是强引用。这个value的引用链条如下:

ThreadLocalMap在内存中的关系如下图:

 可以看到,只有当Thread被回收时,这个value才有被回收的机会,否则,只要线程不退出,value总是会存在一个强引用。但是,要求每个Thread都会退出,是一个极其苛刻的要求,对于线程池来说,大部分线程会一直存在在系统的整个生命周期内,那样的话,就会造成value对象出现泄漏的可能。处理的方法是,在ThreadLocalMap进行set(),get(),remove()的时候,都会进行清理:

以getEntry()为例:

private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)//如果找到key,直接返回return e;else//如果找不到,就会尝试清理,如果你总是访问存在的key,那么这个清理永远不会进来return getEntryAfterMiss(key, i, e);
}

 下面是getEntryAfterMiss()的实现:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;while (e != null) {// 整个e是entry ,也就是一个弱引用ThreadLocal<?> k = e.get();//如果找到了,就返回if (k == key)return e;if (k == null)//如果key为null,说明弱引用已经被回收了//那么就要在这里回收里面的value了expungeStaleEntry(i);else//如果key不是要找的那个,那说明有hash冲突,这里是处理冲突,找下一个entryi = nextIndex(i, len);e = tab[i];}return null;
}

真正用来回收value的是expungeStaleEntry()方法,在remove()和set()方法中,都会直接或者间接调用到这个方法进行value的清理:

从这里可以看到,ThreadLocal为了避免内存泄露,也算是花了一番大心思。不仅使用了弱引用维护key,还会在每个操作上检查key是否被回收,进而再回收value。

但是从中也可以看到,ThreadLocal并不能100%保证不发生内存泄漏。

比如,很不幸的,你的get()方法总是访问固定几个一直存在的ThreadLocal,那么清理动作就不会执行,如果你没有机会调用set()和remove(),那么这个内存泄漏依然会发生。

因此,一个良好的习惯依然是:当你不需要这个ThreadLocal变量时,主动调用remove(),这样对整个系统是有好处的

四、ThreadLocal的使用

(一)简单使用

假设循环创建1000个线程,每个线程都用当前次循环的i值去调用say方法,方法中把对应参数值赋给一个静态变量num,然后还让线程sleep1毫秒模拟执行复杂业务逻辑,然后再输出num,同时往threadlocal中放入一开始的num。

public class School {static ThreadLocal<String> threadLocal = new ThreadLocal<>();static int num ;static void say(int time) throws InterruptedException {num = time;threadLocal.set(num+"");Thread.sleep(1);System.out.println("从ThreadLocal拿出来的:" + threadLocal.get());System.out.println("从当前方法的静态变量中的:"+num);}public static void main(String[] args) {for (int i = 0; i < 1000; i++) {// 线程创建int finalI = i;Thread t = new Thread(new Runnable() {@Overridepublic void run() {try {say(finalI);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();}}
}

查看结果:

在这里插入图片描述

不难看出从当前方法的静态变量中拿出来打印的很多重复值,发生了典型的值覆盖的线程安全问题,而从ThreadLocal中取出来的就是保证了没有重复值,这就是其中一个应用场景,典型的这个线程不安全的场景就有大名鼎鼎的日期格式化工具simpledateformat,多线程传进去的time会被覆盖。

(二)复杂使用

我们先看JdbcTemplate类数据访问类

public Object execute(ConnectionCallback action)throws DataAccessException{Assert.notNull(action, "Callback object must not be null");Connection con = DataSourceUtils.getConnection(getDataSource());try {Connection conToUse = con;if (this.nativeJdbcExtractor != null){conToUse = this.nativeJdbcExtractor.getNativeConnection(con);}else{conToUse = createConnectionProxy(con);}localObject1 = action.doInConnection(conToUse);}catch (SQLException ex){Object localObject1;DataSourceUtils.releaseConnection(con, getDataSource());con = null;throw getExceptionTranslator().translate("ConnectionCallback", getSql(action), ex);}finally {DataSourceUtils.releaseConnection(con, getDataSource());}}

  由上述源码中Connection con = DataSourceUtils.getConnection(getDataSource());这个可以看出,DataSourceUtils类保证当前线程获得的是同一个Connection对象。下面我们主要分析DataSourceUtils类:

public static Connection getConnection(DataSource dataSource)throws CannotGetJdbcConnectionException{try{return doGetConnection(dataSource);} catch (SQLException ex) {}throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);}public static Connection doGetConnection(DataSource dataSource)throws SQLException{Assert.notNull(dataSource, "No DataSource specified");ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);if ((conHolder != null) && ((conHolder.hasConnection()) || (conHolder.isSynchronizedWithTransaction()))) {conHolder.requested();if (!conHolder.hasConnection()) {logger.debug("Fetching resumed JDBC Connection from DataSource");conHolder.setConnection(dataSource.getConnection());}return conHolder.getConnection();}logger.debug("Fetching JDBC Connection from DataSource");Connection con = dataSource.getConnection();if (TransactionSynchronizationManager.isSynchronizationActive()) {logger.debug("Registering transaction synchronization for JDBC Connection");ConnectionHolder holderToUse = conHolder;if (holderToUse == null) {holderToUse = new ConnectionHolder(con);}else {holderToUse.setConnection(con);}holderToUse.requested();TransactionSynchronizationManager.registerSynchronization(new ConnectionSynchronization(holderToUse, dataSource));holderToUse.setSynchronizedWithTransaction(true);if (holderToUse != conHolder) {TransactionSynchronizationManager.bindResource(dataSource, holderToUse);}}return con;}

  由以上源码可以知道,数据库连接从TransactionSynchronizationManager中获得,如果已经存在则获得,否则重新从DataSource创建一个连接,并把这个连接封装为ConnectionHolder,然后注册绑定到TransactionSynchronizationManager中,并返回Connection对象。同时,可以看出DataSource和ConnectionHolder的存储管理在TransactionSynchronizationManager中,继续分析TransactionSynchronizationManager中的关键代码:

private static final ThreadLocal resources = new NamedThreadLocal("Transactional resources");public static Object getResource(Object key){Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);Object value = doGetResource(actualKey);if ((value != null) && (logger.isTraceEnabled())) {logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");}return value;}private static Object doGetResource(Object actualKey){Map map = (Map)resources.get();if (map == null) {return null;}Object value = map.get(actualKey);if (((value instanceof ResourceHolder)) && (((ResourceHolder)value).isVoid())) {map.remove(actualKey);value = null;}return value;}public static void bindResource(Object key, Object value)throws IllegalStateException{Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);Assert.notNull(value, "Value must not be null");Map map = (Map)resources.get();if (map == null) {map = new HashMap();resources.set(map);}if (map.put(actualKey, value) != null) {throw new IllegalStateException("Already value [" + map.get(actualKey) + "] for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");}if (logger.isTraceEnabled())logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to thread [" + Thread.currentThread().getName() + "]");}

分析源码可以得出,
(1)TransactionSynchronizationManager内部用ThreadLocal对象存储资源,ThreadLocal存储的为DataSource生成的actualKey为key值和ConnectionHolder作为value值封装成的Map。
(2)结合DataSourceUtils的doGetConnection函数和TransactionSynchronizationManager的bindResource函数可知:在某个线程第一次调用时候,封装Map资源为:key值为DataSource生成actualKey【Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);】value值为DataSource获得的Connection对象封装后的ConnectionHolder。等这个线程下一次再次访问中就能保证使用的是第一次创建的ConnectionHolder中的Connection对象。

  当事务结束后,调用【DataSourceUtils.releaseConnection(con, getDataSource());】将ConnectionHolder从TransactionSynchronizationManager中解除。当谁都不用,这个连接被close。 
 

public static void releaseConnection(Connection con, DataSource dataSource){try{doReleaseConnection(con, dataSource);}catch (SQLException ex) {logger.debug("Could not close JDBC Connection", ex);}catch (Throwable ex) {logger.debug("Unexpected exception on closing JDBC Connection", ex);}}public static void doReleaseConnection(Connection con, DataSource dataSource)throws SQLException{if (con == null) {return;}if (dataSource != null) {ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);if ((conHolder != null) && (connectionEquals(conHolder, con))){conHolder.released();return;}}if ((!(dataSource instanceof SmartDataSource)) || (((SmartDataSource)dataSource).shouldClose(con))) {logger.debug("Returning JDBC Connection to DataSource");con.close();}}

参考文章:

ThreadLocal使用与原理 - 知乎

史上最全ThreadLocal 详解(一)_倔强的不服的博客-CSDN博客

ThreadLocal从简单使用及源码_threadlocal代码使用_凉拌海蜇丝的博客-CSDN博客

Spring事务之如何保证同一个Connection对象_HaiwiSong的博客-CSDN博客

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

css基础知识十五:如果要做优化,CSS提高性能的方法有哪些?

一、前言 每一个网页都离不开css&#xff0c;但是很多人又认为&#xff0c;css主要是用来完成页面布局的&#xff0c;像一些细节或者优化&#xff0c;就不需要怎么考虑&#xff0c;实际上这种想法是不正确的 作为页面渲染和内容展现的重要环节&#xff0c;css影响着用户对整个…

flutter手写一个底部导航栏

使用了一下flutter提供的导航栏&#xff0c;NavigationBar&#xff0c;不过感觉使用起来不是很方便。 譬如说&#xff1a; 不能直接使用图片资源&#xff0c;需要中间加几层转换把图片转化成icon文本大小及颜色不太好控制状态栏的上边来一个横线也没有相应的样式&#xff0c;等…

【云原生 | 53】Docker三剑客之Docker Compose应用案例一:Web负载均衡

&#x1f341;博主简介&#xff1a; &#x1f3c5;云计算领域优质创作者 &#x1f3c5;2022年CSDN新星计划python赛道第一名 &#x1f3c5;2022年CSDN原力计划优质作者 &#x1f3c5;阿里云ACE认证高级工程师 &#x1f3c5;阿里云开发者社区专…

Django纪录操作之增删改查

一、单表 1、 添加记录 准备表 from django.db import modelsclass Book(models.Model):title models.CharField(max_length20)price models.DecimalField(max_digits65,decimal_places5)publish models.CharField(max_length30)pub_date models.DateTimeField(auto_now…

Python财经股票数据获取, 保存表格文件

目录标题 前言环境使用:模块使用]:代码展示尾语 前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! 环境使用: Python 3.8 解释器 Pycharm 编辑器 模块使用]: import requests —> 数据请求模块 pip install requests import csv 第三方模块安装: win R 输入cmd 输…

大数据Doris(五十):Export导出原理

文章目录 Export导出原理 一、原理 二、查询计划拆分 三、查询计划执行 Export导出原理 Doris Export、Select Into Outfile、MySQL dump三种方式数据导出。用户可以根据自己的需求导出数据。此外数据还可以以文件形式通过Borker备份到远端存储系统中&#xff0c;之后可以…

【Linux】Linux项目自动化构建工具-make/makefile

Linux项目自动化构建工具-make/makefile 什么是make/makefile&#xff1f;make/makefile的使用依赖关系依赖方法makefile是如何工作的&#xff1f;为什么要使用makefile呢&#xff1f;makefile是怎么做到的呢&#xff1f;make和make clean.PHONY&#xff1a;伪目标 特殊符号&am…

1 Prometheus-监控简介

目录 1 什么是监控 1.1 技术作为客户 1.2 业务作为客户 2. 监控基础知识 2.1 事后监控 2.2 机械式/模板式/无脑式监控 2.3 不够准确的监控 2.4 静态监控 2.5 不频繁的监控 2.6 缺少自动化或操作繁琐/不便 2.7 监控模式总结 3.监控机制 3.1 探针和内省 3.2 拉取和推…

LangChain大型语言模型(LLM)应用开发(二):Conversation Memory

LangChain是一个基于大语言模型&#xff08;如ChatGPT&#xff09;用于构建端到端语言模型应用的 Python 框架。它提供了一套工具、组件和接口&#xff0c;可简化创建由大型语言模型 (LLM) 和聊天模型提供支持的应用程序的过程。LangChain 可以轻松管理与语言模型的交互&#x…

Unity协程

unity提供了一种类似“多段代码并行执行”的功能&#xff0c;即协程。 我们在定义一个协程的时候&#xff0c;需要遵循类似这样的语法 IEnumerator&#xff08;枚举器接口&#xff09; namespace System.Collections {public interface IEnumerator{object Current { get; }/…

MySql基础知识及数据查询

目录 第一章 数据库概述 1.为什么要学习数据库&#xff1f; 2.数据库的相关概念 3.ORM(Object Relational Mapping)思想 4.表与表的记录之间存在哪些关联关系 第二章 基本的SELECT语句 1.SQL的分类 2. SQL基本规则 3.导入现有的数据表、表的数据 4.最基本的…

位图的详解

目录 位图 位图的概念 位图的实现 位图常见三道面试题 1.给定100亿个整数&#xff0c;设计算法找到只出现一次的整数&#xff1f; 2. 给两个文件&#xff0c;分别有100亿个整数&#xff0c;我们只有1G内存&#xff0c;如何找到两个文件交集&#xff1f; 3. 位图应用变形…