Java并发(二十):线程本地变量ThreadLocal
2018-11-28 08:52:57来源:博客园 阅读 ()
ThreadLocal是一个本地线程副本变量工具类。
主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不同的变量值完成操作的场景。
一、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 T initialValue() { return null; }
三、使用场景
简单使用示例1:
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
注意: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永远无法回收,造成内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
但是这些被动的预防措施并不能保证不会内存泄漏:
使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。
内存泄漏实例分析:ThreadLocal 内存泄露的实例分析
解决:
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
参考资料 / 相关推荐
线程管理(九)使用本地线程变量
不共享有时是最好的
【死磕Java并发】—–深入分析ThreadLocal
Java并发编程:深入剖析ThreadLocal
ThreadLocal-面试必问深度解析
深入分析 ThreadLocal 内存泄漏问题
ThreadLocal 内存泄露的实例分析
标签:
版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有
上一篇:servlet生命周期和执行流程
下一篇:Java中的锁分类
- 国外程序员整理的Java资源大全(全部是干货) 2020-06-12
- 2020年深圳中国平安各部门Java中级面试真题合集(附答案) 2020-06-11
- 2020年java就业前景 2020-06-11
- 04.Java基础语法 2020-06-11
- Java--反射(框架设计的灵魂)案例 2020-06-11
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设
网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源
网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它
服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护
软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷
网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash