Java 多线程 —— ThreadLocal

一、引言

ThreadLocal是Java帮助实现线程封闭性的典型手段。

作用:提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量的传递复杂度。同时也用来维护线程中的变量不被其他线程干扰。

这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get 与set方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值

二、ThreadLocal的简单应用

 ThreadLocal是使用空间换时间,synchronized是使用时间换空间,比如在hibernate中session就存在于ThreadLocal中,避免synchronized的使用。

下面程序的输出结果为null,因为从ThreadLocal中取出的对象一定是本线程中set的对象,别的线程无法取出,因为线程自己放入的对象只能自己取得,因此无需进行加锁处理,执行效率上ThreadLocal比synchronized要高。

public class ThreadLocal_02 {static ThreadLocal<Person> tl = new ThreadLocal<>();public static void main(String[] args) {new Thread(() -> {try {TimeUnit.SECONDS.sleep(2);} catch (Exception e) {e.printStackTrace();}System.out.println(tl.get()); // output : null}).start();new Thread(() -> {try {TimeUnit.SECONDS.sleep(1);} catch (Exception e) {e.printStackTrace();}tl.set(new Person("张三"));}).start();}static class Person {String name;public Person(String name) {this.name = name;}}
}

三、对ThreadLocal的理解

ThreadLocal对象通常用于防止对可变的单例变量或全局变量进行共享。

例如,在单线程应用程序中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个Connection对象。由于JDBC的连接对象不一定是线程安全的,因此,当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的。通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接:

    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){public Connection initialValue() {return DriverManager.getConnection(DB_URL);}};public static Connection getConnection() {return connectionHolder.get();}

在比如,当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用ThreadLocal。

四、ThreadLocal的实现原理

ThreadLocal内部提供了四个对外开放的接口方法,这也是用户操作ThreadLocal对象的基本方法:

1、public T get() :取得线程局部变量

2、public void set(T value) :设置线程局部变量

3、public void remove() :删除线程局部变量

4、protected T initialValue() :返回该线程局部变量初始值

思考:ThreadLocal的实例是如何为每一个线程维护变量副本的呢?

上图来自http://www.importnew.com/22039.html

其实,每一个线程Thread其内部都维护一个ThreadLocal.ThreadLocalMap的实例对象(变量名为:threadLocals)。

你可以将这个ThreadLocalMap对象理解为一个Map,但实际上它是一个数组,一个以封装了ThreadLocal为键,Object为值的元素的数组。也就是说ThreadLocal本身不存储值,它只是作为一个key来让当前线程从ThreadLocalMap中获取value。值得注意的是,ThreadLocalMap是使用 ThreadLocal的弱引用作为 Key 的,弱引用的对象在GC时会被回收。

static class ThreadLocalMap {//map中的每个节点Entry,其键key是ThreadLocal并且还是弱引用static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}// 初始化容量为16,以为对其扩充也必须是2的指数private static final int INITIAL_CAPACITY = 16;// 真正用于存储线程的每个ThreadLocal的数组,将ThreadLocal和其对应的值包装为一个Entryprivate Entry[] table;///....其他方法和操作都和map类似
}

由此,我们可以大概了解到了其线程局部变量的维护机制:为不同的线程创建不同的ThreadLocalMap,以线程本身作为区分,每个线程之间没有任何联系。

下面感兴趣可以看一下get()、set()的源码:

public T get() {Thread t = Thread.currentThread();//当前线程ThreadLocalMap map = getMap(t);//获取当前线程对应的ThreadLocalMapif (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);//获取对应ThreadLocal的变量值if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//若当前线程还未创建ThreadLocalMap,则返回调用此方法并在其中调用createMap方法进行创建并返回初始值。return setInitialValue();
}
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
}

五、ThreadLocal内存泄漏问题

5.1 ThreadLocal为什么会内存泄漏?

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束(如线程池的线程回收)的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永远无法回收,造成内存泄漏。 

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

但这些被动的预防措施并不能保证不会内存泄漏。

5.2 为什么使用弱引用?

从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?

我们先来看看官方文档的说法:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了应对非常大和长时间的用途,哈希表使用弱引用的 key。

下面我们分两种情况讨论:

  • key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  • key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应Key就会导致内存泄漏,而不是因为弱引用。

5.3 有效避免内存泄漏的最佳实践

每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

六、鸣谢

《深入剖析ThreadLocal实现原理以及内存泄漏问题》

《深入分析 ThreadLocal 内存泄漏问题》

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

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

相关文章

设计模式---适配器设计模式

设计模式---适配器设计模式 什么事适配器&#xff1a; 1. 在使用监听的时候&#xff0c;需要定义一个类事件监听器接口 2. 通常接口中有多个方法&#xff0c;而程序中不一定所有的方法都用到&#xff0c;但又必须重写&#xff0c;很繁琐 3. 适配器简化了这些操作&#xff0c…

Java并发编程实战————售票问题

引言 现有一个需求如下&#xff1a; 有10000张火车票&#xff0c;每张票都有一个编号&#xff0c;同时有10个窗口对外售票&#xff0c;如何确保车票的正常售卖&#xff1f; 程序一&#xff1a;使用List 问题的解决办法都是从我们最最熟悉的角度思考。程序一&#xff0c;我们…

多线程相关知识

多线程相关知识 两个线程进行通信&#xff1a;通过等待&#xff08;wait&#xff09;唤醒&#xff08;notify&#xff09;机制 三个或三个以上线程进行通信&#xff1a;通过notifyAll&#xff08;&#xff09;方法 /* * 1. 在同步代码块中&#xff0c;用哪个对象锁&#xff0c…

Eclipse集成PyDev5.2.0开发插件

引言 在进行Python学习的时候&#xff0c;希望不使用IDLE进行开发&#xff0c;但是其他的IDE如PyCharm可能需要一段短暂时间的上手&#xff0c;因为开发过Java&#xff0c;所以使用能够集成到Eclipse上的PyDev插件进行开发应该会好一些。 但是在安装PyDev的时候发生了一些问题…

PostMan 四种常见的 POST 提交数据方式

HTTP/1.1 协议规定的 HTTP 请求方法有 OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE、CONNECT 这几种。其中 POST 一般用来向服务端提交数据&#xff0c;本文主要讨论 POST 提交数据的几种方式。 协议规定 POST 提交的数据必须放在消息主体&#xff08;entity-body&#xff0…

史上最真实行业鄙视链

本文转载自菜鸟教程的微信公众号&#xff0c;原文链接&#xff1a;https://mp.weixin.qq.com/s/d9cdtq8y4Msq-_ZNof-iuw 引言 作为程序员的一份子&#xff0c;掌握好各个生态系统中的鄙视链&#xff0c;可以写出更加符合改变世界要求的代码。掌握了鄙视链&#xff0c;就掌握了…

权限验证框架Shiro

权限验证框架Shiro&#xff1a; Shiro简介 什么是Shiro&#xff1a; shiro是一个强大易用的Java安全框架&#xff0c;提供了认证&#xff0c;授权&#xff0c;加密&#xff0c;回话管理等功能&#xff1b; 认证&#xff08;Authentication&#xff09;&#xff1a;用户身份识别…

Mybatis从入门到精通上篇

Mybatis从入门到精通上篇&#xff1a; 学习过的持久层框架&#xff1a;DBUtils , Hibernate Mybatis就是类似于hibernate的orm持久层框架。 Mybatis介绍&#xff1a; Mybatis是面向sql的持久层框架&#xff0c;他封装了jdbc访问数据库的过程&#xff0c;我们开发&#xff0c;只…

Eclipse使用————Working Set工作集

引言 经常看到在设置项目的时候&#xff0c;如导入项目&#xff0c;新建项目等看到对话框的下方有一个“add to working set”复选框&#xff0c;为了弄清这个working set&#xff0c;我们就来好好了解一下Eclipse 的working set功能。 Working Set&#xff1f; Eclipse中通…

Mybatis从入门到精通下篇

Mybatis从入门到精通下篇&#xff1a; 输入类型&#xff1a; 输出类型&#xff1a; ResultMap&#xff1a; 动态sql&#xff1a; if标签&#xff1a; where标签&#xff1a; sql片段&#xff1a; foreach标签&#xff1a; 关联查询&#xff1a; 以订单作为主体&#xff1a; 一…

爱上进制转换练习

引言 对于可能接触到通讯行业或是物联网的开发工作者&#xff0c;一般会面对十进制、二进制、十六进制的转换工作&#xff0c;不仅仅是体现在代码上&#xff0c;有时候也需要用肉眼来进行快速的转化&#xff0c;以获取协议指令中的信息。 今天通过简单的整理&#xff0c;特此…

Sprint Boot————@Qualifier、@Primary

引言 使用Autowired自动注入时&#xff1a; 如果注入的接口有多个实现类&#xff0c;如下所示&#xff1a; 那么如果不指定具体是哪个实现类的Bean&#xff0c;在Spring Boot启动时就会发生异常&#xff08;下图请点击查看&#xff09;&#xff1a; 异常的描述信息非常简单&am…

SpringMVC教程上篇

SpringMVC教程上篇 SpringMVC优势&#xff1a; SpringMVC代码执行流程&#xff1a; 框架结构&#xff1a; 架构流程&#xff1a; 组件说明&#xff1a; SpringMVC与Mybatis整合 ! 效果: 开发流程&#xff1a;

Eclipse使用————生成Get/Set、toString快捷键(不使用鼠标)

引言 除了鼠标右键空白处—>source选择我们需要的操作之外是否还有更快捷的不需要鼠标的操作呢&#xff1f; 如何快速的通过键盘来生成get、set方法&#xff1f;如何快速的通过键盘生成toString方法&#xff1f;如何快速的通过键盘生成需要实现的父类方法呢&#xff1f; …

SpringMVC教程下篇

SpringMVC教程下篇 内容包括&#xff1a; 绑定数组&#xff1a; 将表单数据绑定到list&#xff1a; RequestMapping注解的三种用法&#xff1a; Controller方法返回值&#xff1a; 乱码问题总结 异常处理&#xff1a; 照片上传&#xff1a; RESTFUL支持&#xff…

Spring Boot面试杀手锏————自动配置原理

引言 不论在工作中&#xff0c;亦或是求职面试&#xff0c;Spring Boot已经成为我们必知必会的技能项。除了某些老旧的政府项目或金融项目持有观望态度外&#xff0c;如今的各行各业都在飞速的拥抱这个已经不是很新的Spring启动框架。 当然&#xff0c;作为Spring Boot的精髓…

为什么要坚持写博客

引言 断断续续地写博客已经有一段时间了&#xff0c;作为一个Java中级开发工程师&#xff0c;工作了三年多也算渐渐入了门。不得不说&#xff0c;博客给我的改变是非常大的&#xff0c;那么作为一个技术人员&#xff0c;为什么我觉得必须要坚持写博客&#xff1f;下面&#xf…

Spring Boot——@ConfigurationProperties与@Value的区别

引言 Spring Boot从配置文件中取值的方式有两种&#xff0c;一种是批量注入ConfigurationProperties&#xff0c;另一种是单独注入Value。 它们之间除了批量与单独取值的区别之外&#xff0c;还存在着其他一些使用方式&#xff0c;本篇博客将详细讲解这两种注解之间的区别和使…

Spring Boot —— YAML配置文件

引言 首先&#xff0c;YAML并不是仅仅可以使用在Java项目中&#xff0c;它是一种类似于json结构的标记语言&#xff0c;可以为所有的编程语言服务。它强调更直观的层级表示&#xff0c;比较适合描述配置文件中的层级关系。 Spring Boot可以识别后缀名为".properties&quo…

centos7下docker启动失败解决

centos7下docker启动失败解决 docker安装成功却启动失败&#xff0c;查看docker服务&#xff0c;systemctl status docker.service, 服务日志提示Failed to start Docker Application Container Engine.如下图所示&#xff1a; 解决方法&#xff0c;修改docker文件&#xff0…