Java并发(二十):线程本地变量ThreadLocal
ThreadLocal是一个本地线程副本变量工具类。
主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不同的变量值完成操作的场景。
读写锁ReentrantReadWriteLock 记录线程持有的读锁数量时使用了ThreadLocal。Java并发(十):读写锁ReentrantReadWriteLock
一、ThreadLocal的核心机制
每个Thread线程内部都有一个Map,Tread类的ThreadLocal.ThreadLocalMap属性
Map里面存储线程本地对象(key也就是当前的ThreadLoacal对象)和线程的变量副本(value)
Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值
数据结构:

二、ThreadLocal源码分析
ThreadLocal核心方法:
- get():返回此线程局部变量的当前线程副本中的值。
- initialValue():返回此线程局部变量的当前线程的“初始值”。
- remove():移除此线程局部变量当前线程的值。
- set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。
内部类 ThreadLocalMap:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用类型的,Value并非弱引用。
ThreadLocalMap的set()方法:
private void set(ThreadLocal<?> key, Object value) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
// 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置
int i = key.threadLocalHashCode & (len-1);
// 采用“线性探测法”,寻找合适位置
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// key 存在,直接覆盖
if (k == key) {
e.value = value;
return;
}
// key == null,但是存在值(因为此处的e != null),说明之前的ThreadLocal对象已经被回收了
if (k == null) {
// 用新元素替换陈旧的元素
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal对应的key实例不存在也没有陈旧元素,new 一个
tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
int sz = ++size;
// cleanSomeSlots 清楚陈旧的Entry(key == null)
// 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。
所以这里引出的建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。
get()方法:
(1)获取当前线程的ThreadLocalMap对象threadLocals
(2)从map中获取线程存储的K-V Entry节点。
(3)从Entry节点获取存储的Value副本值返回。
(4)map为空的话返回初始值null,即线程变量副本为null,在使用时需要注意判断NullPointerException。
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的成员变量 threadLocal
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从当前线程的ThreadLocalMap获取相对应的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 获取目标值
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
set()方法:
步骤:
(1)获取当前线程的成员变量map
(2)map非空,则重新将ThreadLocal和新的value副本放入到map中。
(3)map空,则对线程的成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal和value副本放入map中。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
initialValue()方法:
该方法定义为protected级别且返回为null,很明显是要子类实现它的,所以我们在使用ThreadLocal的时候一般都应该覆盖该方法。该方法不能显示调用,只有在第一次调用get()或者set()方法时才会被执行,并且仅执行1次。
protected T initialValue() {
return null;
}
三、使用场景
简单使用场景一:
public class SeqCount {
private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){
// 实现initialValue()
public Integer initialValue() {
return 0;
}
};
public int nextSeq(){
seqCount.set(seqCount.get() + 1);
return seqCount.get();
}
public static void main(String[] args){
SeqCount seqCount = new SeqCount();
SeqThread thread1 = new SeqThread(seqCount);
SeqThread thread2 = new SeqThread(seqCount);
SeqThread thread3 = new SeqThread(seqCount);
SeqThread thread4 = new SeqThread(seqCount);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
private static class SeqThread extends Thread{
private SeqCount seqCount;
SeqThread(SeqCount seqCount){
this.seqCount = seqCount;
}
public void run() {
for(int i = 0 ; i < 3 ; i++){
System.out.println(Thread.currentThread().getName() + " seqCount :" + seqCount.nextSeq());
}
}
}
}
运行结果:
Thread-1 seqCount :1
Thread-3 seqCount :1
Thread-3 seqCount :2
Thread-3 seqCount :3
Thread-0 seqCount :1
Thread-0 seqCount :2
Thread-0 seqCount :3
Thread-2 seqCount :1
Thread-1 seqCount :2
Thread-1 seqCount :3
Thread-2 seqCount :2
Thread-2 seqCount :3
类似的ReentrantReadWriteLock中的java.util.concurrent.locks.ReentrantReadWriteLock.Sync.readHolds属性,也使用的了TreadLocal来记录占有该读锁的线程重入次数。可参考:Java并发(十):读写锁ReentrantReadWriteLock
注意:initialValue()方法返回一个对象时,get()和set()方法操作的其实是同一个对象的属性,不能实现线程隔离。


使用场景二:session获取场景
每个线程访问数据库都应当是一个独立的Session会话,如果多个线程共享同一个Session会话,有可能其他线程关闭连接了,当前线程再执行提交时就会出现会话已关闭的异常,导致系统异常。此方式能避免线程争抢Session,提高并发下的安全性。
//获取Session
public static Session getCurrentSession(){
Session session = threadLocal.get();
//判断Session是否为空,如果为空,将创建一个session,并设置到本地线程变量中
try {
if(session ==null&&!session.isOpen()){
if(sessionFactory==null){
rbuildSessionFactory();// 创建Hibernate的SessionFactory
}else{
session = sessionFactory.openSession();
}
}
threadLocal.set(session);
} catch (Exception e) {
// TODO: handle exception
} return session;
}
四、内存泄漏问题
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
关于GC以及引用状态:JVM垃圾回收机制
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
但是这些被动的预防措施并不能保证不会内存泄漏:
使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。
内存泄漏实例分析:ThreadLocal 内存泄露的实例分析
解决:
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
参考资料 / 相关推荐
Java并发(二十):线程本地变量ThreadLocal的更多相关文章
- 深入理解java:2.4. 线程本地变量 java.lang.ThreadLocal类
ThreadLocal,很多人都叫它做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多. 可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那样每个线程可以访问自己内 ...
- 线程本地变量ThreadLocal
一.本地线程变量使用场景 并发应用的一个关键地方就是共享数据.如果你创建一个类对象,实现Runnable接口,然后多个Thread对象使用同样的Runnable对象,全部的线程都共享同样的属性.这意味 ...
- 线程本地变量ThreadLocal源码解读
一.ThreadLocal基础知识 原始线程现状: 按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步.但是Spring中的各种模板 ...
- 线程本地变量ThreadLocal (耗时工具)
线程本地变量类 package king; import java.util.ArrayList; import java.util.List; import java.util.Map; impor ...
- 线程本地变量ThreadLocal (耗时工具)【原】
线程本地变量类 package king; import java.util.ArrayList; import java.util.List; import java.util.Map; impor ...
- JAVA线程本地变量ThreadLocal和私有变量的区别
ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些. 所以,在Java中编写线程局部变量的代码相对来说要笨 ...
- 深入理解线程本地变量ThreadLocal
ThreadLocal理解: 假设在多线程并发环境中.一个可变对象涉及到共享与竞争,那么该可变对象就一定会涉及到线程间同步操作,这是多线程并发问题. 否则该可变对象将作为线程私有对象,可通过Threa ...
- Java并发编程:线程封闭和ThreadLocal详解
转载请标明出处: http://blog.csdn.net/forezp/article/details/77620769 本文出自方志朋的博客 什么是线程封闭 当访问共享变量时,往往需要加锁来保证数 ...
- Java笔记(二十)……线程间通信
概述 当需要多线程配合完成一项任务时,往往需要用到线程间通信,以确保任务的稳步快速运行 相关语句 wait():挂起线程,释放锁,相当于自动放弃了执行权限 notify():唤醒wait等待队列里的第 ...
随机推荐
- required_new spring事务传播行为无效碰到的坑!
在测试事务传播行为的时候,因为用了同一个service中的方法测试,所以不管怎么设置都无效了: 原因是aop动态代理只会拦截一次执行方法,第二个方法是照搬的,只要调用其他service中的事务方法,传 ...
- 手动实现图片预览-放大缩小全屏支持IE9以上
#{extends '/Index/index.html' /} #{set title:'意见反馈' /} <script src="/public/mgr/javascripts/ ...
- Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.
在设置请求头的时候报这个Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPE ...
- TF-图像的深度和通道的概念(转)
图像的深度和通道概念 图像的深度: 图片是由一个个像素点构成的,所有不同颜色的像素点构成了一副完整的图像,计算机存储图片是以二进制来进行的. 1 bit : 用一位来存储,那么这个像素点的取值范围就是 ...
- day03作业
---恢复内容开始--- 1.+.-.*./.%.++.-- ①正号:②加法运算:③连接符 除法是两个数相除所得的结果,取余是除法操作的余数. ++前置表示现将一个数自加然后进行赋值,++后置表示先赋 ...
- java基础48 IO流技术(序列流)
本文知识点目录: 1.SequenceInputStream序列流的步骤 2.实例 3.附录(音乐的切割与合并) 1.SequenceInputStream序列流的步骤 1.找到目标文件 ...
- java基础39 增强for循环(也叫foreach循环)
增强for循环是jdk1.5出现的新功能 1.增强for循环的作用 简化了迭代器的书写格式(注意:增强for循环底层还是使用了迭代器遍历) 2.增强for循环的格式 for(数据类型 变量名:遍历的目 ...
- php 的swoole 和websocket 连接wss
1. 下载证书 $serv = new swoole_server('0.0.0.0', 9501, SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL); $s ...
- 插入标识列identity_insert
插入标识列identity_insert 在进行数据插入时,如果插入列名包括标识列,常常会遇到以下3种提示: 一.“当 IDENTITY_INSERT 设置为 OFF 时,不能向表 'xxxxxxxx ...
- MAC下安装MAMP后,mysql server无法启动
用MAC下载安装了MAMP,之前使用是很好没问题的,但是突然无法启动mysql server,检查日志,提示InnDB出错,然后删掉了/Application/MAMP/db/mysql56目录下的i ...