threadlocal存连接对象的目的_ThreadLocal 介绍
概述
ThreadLocal 是 java 提供的一个方便对象在本线程内不同方法中传递和获取的类。用它定义的变量,仅在本线程中可见和维护,不受其他线程的影响,与其他线程相互隔离。
虽然在本线程不同方法中使用变量,可以通过在方法中传入参数解决,但是当涉及多个方法甚至多个类时,为每个方法增加同样的参数将是一场噩梦,此时 ThreadLocal 就能很好地解决这个问题。它可以在本线程内任何一个地方赋值,在任何一个地方获取值,并且不用作为函数参数传入。这看起来像静态成员变量,但是 ThreadLocal 变量相比静态成员变量的一个优势就是,ThreadLocal 是线程隔离的,其值不会受另一个线程的影响,也不用考虑加锁或值被其他线程篡改的问题,而这些问题都是静态成员变量无法做到的。因此当涉及一个对象需要在很多不同方法之间传递时,应该考虑使用 ThreadLocal 对象来简化代码。
使用
ThreadLocal 通过 set 方法可以给变量赋值,通过 get 方法获取变量的值。当然,也可以在定义变量时通过 ThreadLocal.withInitial 方法给变量赋初始值,或者定义一个继承 ThreadLocal 的类,然后重写 initialValue 方法。
示例代码如下
public class TestThreadLocal
{
private static ThreadLocal builder = ThreadLocal.withInitial(StringBuilder::new);
public static void main(String[] args)
{
for (int i = 0; i < 5; i++)
{
new Thread(() -> {
String threadName = Thread.currentThread().getName();
for (int j = 0; j < 3; j++)
{
append(j);
System.out.printf("%s append %d, now builder value is %s, ThreadLocal instance hashcode is %d, ThreadLocal instance mapping value hashcode is %d\n", threadName, j, builder.get().toString(), builder.hashCode(), builder.get().hashCode());
}
change();
System.out.printf("%s set new stringbuilder, now builder value is %s, ThreadLocal instance hashcode is %d, ThreadLocal instance mapping value hashcode is %d\n", threadName, builder.get().toString(), builder.hashCode(), builder.get().hashCode());
}, "thread-" + i).start();
}
}
private static void append(int num) {
builder.get().append(num);
}
private static void change() {
StringBuilder newStringBuilder = new StringBuilder("HelloWorld");
builder.set(newStringBuilder);
}
}
在例子中,定义了一个 builder 的 ThreadLocal 对象,然后启动 5 个线程,分别对 builder 对象进行访问和修改操作,这两个操作放在两个不同的函数 append、change 中进行,两个函数访问 builder 对象也是直接获取,而不是放入函数的入参中传递进来。
代码输出如下
thread-0 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 566157654
thread-0 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 566157654
thread-4 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 654647086
thread-3 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1803363945
thread-2 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1535812498
thread-1 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 2075237830
thread-2 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1535812498
thread-3 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1803363945
thread-4 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 654647086
thread-0 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 566157654
thread-0 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1773033190
thread-4 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 654647086
thread-4 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 700642750
thread-3 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1803363945
thread-3 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1706743158
thread-2 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1535812498
thread-2 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1431127699
thread-1 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 2075237830
thread-1 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 2075237830
thread-1 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1970695360
从输出中 1~6 行可以看出,不同线程访问的是同一个 builder 对象(不同线程输出的 ThreadLocal instance hashcode 值相同),但是每个线程获得的 builder 对象存储的实例 StringBuilder 不同(不同线程输出的 ThreadLocal instance mapping value hashcode 值不相同)。
从输出中 1~2、9~10行可以看出,同一个线程中修改 builder 对象存储的实例的值时,并不会影响到其他线程的 builder 对象存储的实例(thread-4 线程改变存储的 StringBuilder 的值并不会引起 thread-0 线程的 ThreadLocal instance mapping value hashcode 值发生改变)
从输出中 9~13 行可以看出,一个线程对 ThreadLocal 对象存储的值发生改变时,并不会影响其他的线程(thread-0 线程调用 set 方法改变本线程 ThreadLocal 存储的对象值,本线程的 ThreadLocal instance mapping value hashcode 发生改变,但是 thread-4 的 ThreadLocal instance mapping value hashcode 并没有因此改变)。
原理
ThreadLocal 能在每个线程间进行隔离,其主要是靠在每个 Thread 对象中维护一个 ThreadLocalMap 来实现的。因为是线程中的对象,所以对其他线程不可见,从而达到隔离的目的。那为什么是一个 Map 结构呢。主要是因为一个线程中可能有多个 ThreadLocal 对象,这就需要一个集合来进行存储区分,而用 Map 可以更快地查找到相关的对象。
ThreadLocalMap 是 ThreadLocal 对象的一个静态内部类,内部维护一个 Entry 数组,实现类似 Map 的 get 和 put 等操作,为简单起见,可以将其看做是一个 Map,其中 key 是 ThreadLocal 实例,value 是 ThreadLocal 实例对象存储的值。
set
当调用 ThreadLocal 的 set 方法给变量设置值时,ThreadLocal 对象会先获取本线程的 ThreadLocalMap 对象,然后将当前的 ThreadLocal 对象及要设置值作为键值对放入 Map 中。
public void set(T value) {
Thread t = Thread.currentThread();
// 获取当前线程的 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
if (map != null)
// this 指当前的 ThreadLocal 对象
map.set(this, value);
else
// key 不存在,则创建 map 并设置值
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
// threadLocals 是 Thread 中的一个变量,因此是线程隔离的,不会受其他线程影响
// 其在 Thread 类中的定义如下:ThreadLocal.ThreadLocalMap threadLocals = null;
return t.threadLocals;
}
get
获取 ThreadLocal 存储的对象值时,需要调用 get 方法。此方法也是先获取本线程的 ThreadLocalMap 对象,然后将当前的 ThreadLocal 对象作为 key 从 Map 中获取对应的值,如果没有,则返回一个初始 null。
public T get() {
Thread t = Thread.currentThread();
// 获取当前线程的 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// this 指当前的 ThreadLocal 对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
内存泄漏
ThreadLocalMap 中的 key 是一个 ThreadLocal 对象,且是一个弱引用,而 value 却是一个强引用。
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
// 其他代码
}
毫无疑问,如果线程执行完关闭,那么线程的所有对象都会被销毁,此时不会存在内存泄漏的问题。此外,在执行 get、set 操作时,调用进入 ThreadLocalMap 内部的函数,会对 Entry 进行检查,如果 key 为空,也会将 value 设置为空,让其可以被垃圾回收。所以一般情况下也不会造成内存泄漏。
// get 或 set 方法,满足一定条件时会进入 expungeStaleEntry 方法
// 此方法内部会将 key 为 null 的 Entry 的 value 设置为 null,从而使得其可以被垃圾回收
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 设置 value 值为 null,清空引用,让其可以被 GC 回收
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal> k = e.get();
if (k == null) {
// 设置 value 值为 null,清空引用,让其可以被 GC 回收
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
但是,存在一种情况,可能导致内存泄漏。如果在某一时刻,将 ThreadLocal 实例设置为 null,即没有 ThreadLocal 没有强引用了,如果发生 GC 时,由于 ThreadLocal 实例只存在弱引用,所以被回收了,但是 value 仍然存在一个当前线程连接过来的强引用,其不会被回收,只有等到线程结束死亡或者手动清空 value 或者等到另一个 ThreadLocal 对象进行 get 或 set 操作时刚好触发 expungeStaleEntry 函数并且刚好能够检查到本 ThreadLocal 对象 key 为空(概率太小),这样才不会发生内存泄漏。否则,value 始终有引用指向它,它也不会被 GC 回收,那么就会导致内存泄漏。虽然发生内存泄漏的概率比较小,但是为了保险起见,也建议在使用完 ThreadLocal 对象后调用一下 remove 方法清理一下值。
与线程池结合使用
由于线程池是会复用线程的,因此如果在线程任务中对 ThreadLocal 没有经过重新设值而直接读取值的话,可能读取到的是该线程上一个任务赋值的结果,而不是本次任务的初始值,从而导致一些意向不到的错误。如下所示,创建一个固定大小是 3 的线程池,但是往线程池中放入 5 个任务,则最后两个任务会复用之前创建的线程,此时调用 ThreadLocal 的 get 方法获取到的是上一个任务赋值的结果,而不是本线程的初始值(程序输出的第4~5 行就是复用了线程 11 和 13,第一次获取到的是也是上一个任务赋的值 2,而不是本线程的初始值 1)。
public class TestThreadLocalExecutor
{
private static ThreadLocal id = ThreadLocal.withInitial(() -> 1);
public static void main(String[] args)
{
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++)
{
executor.execute(() -> {
long threadId = Thread.currentThread().getId();
// 任务开始时重新赋值,否则可能读取到的是上一个任务的值
// id.set(1);
int before = id.get();
increment();
int after = id.get();
System.out.printf("Thread id: %d, before increment: %d, after increment: %d\n", threadId, before, after);
});
}
executor.shutdown();
}
private static void increment()
{
int result = id.get() + 1;
id.set(result);
}
}
程序输出如下
Thread id: 11, before increment: 1, after increment: 2
Thread id: 13, before increment: 1, after increment: 2
Thread id: 12, before increment: 1, after increment: 2
Thread id: 13, before increment: 2, after increment: 3
Thread id: 11, before increment: 2, after increment: 3
为了避免如上情况的发生,可以在每个任务开始时,为 ThreadLocal 对象重新设置初始值(在 get 方法前先调用 set 方法),或者使用原生的创建线程的方式(跳开线程池的方式)。
threadlocal存连接对象的目的_ThreadLocal 介绍相关推荐
- threadlocal存连接对象的目的_面试官:知道ThreadLocal嘛?谈谈你对它的理解?
在java的多线程模块中,ThreadLocal是经常被提问到的一个知识点,提问的方式有很多种,可能是循序渐进也可能是就像我的题目那样,因此只有理解透彻了,不管怎么问,都能游刃有余. 这篇文章主要从以 ...
- threadlocal存连接对象的目的_终于懂了ThreadLocal,不再害怕面试官问了
ThreadLocal解析 synchronized和ThreadLocal的区别: synchronized:以时间换空间,只提供一份变量,让不同的线程排队访问,失去了并发性,降低了程序效率,着重对 ...
- Spring JDBC-使用Spring JDBC获取本地连接对象以及操作BLOB/CLOB类型数据
概述 如何获取本地数据连接 示例从DBCP数据源中获取Oracle的本地连接对象 相关接口操作 LobCreator LobHandler 插入LOB类型的数据 以块数据的方式读取LOB数据 以流数据 ...
- Java 数据对象(JDO)介绍
Java 数据对象(JDO)介绍 by Jeff Brown, Senior Software Engineer Object Computing, Inc. (OCI) 介绍 Java 数据对象 ...
- SQL优化技巧--远程连接对象引起的CTE性能问题
背景 最近SSIS的开发过程中遇到几个问题.其中使用CTE时,遇到一个远程连接对象,结果导致严重的性能问题,为了应急我就修改了代码. 之前我写了一篇介绍CTE的随笔包含了CTE的用法等: http:/ ...
- COM原理及应用之可连接对象
1.COM的高级特性 COM规范中有一些高级特性,如可连接对象.永久存储.一致的数据传输等,使COM规范具有更强的生命力,它们也是OLE的基础,但它们的应用又不仅仅局限于OLE,这些高级特性已经广泛应 ...
- JDBC工具类,基于C3P0的数据库连接池,提供获取连接池、获取连接对象、释放资源和封装事务操作的方法...
/** * * JDBC工具类,基于C3P0数据库连接池的实现 * * @author 周瑜 * @2018年5月7日 下午2:13:20 */ public final class JD ...
- 工业级加固固态硬盘产品性能-领存2.5 寸 R-SATA SSD介绍
工业级加固固态硬盘产品性能-领存2.5 寸 R-SATA SSD介绍 Z3 系列 2.5 寸 R-SATA 工业级固态硬盘(简称 Z3 R-SATA 盘)一款针对工业.汽车电子.医疗设备.航空航海等特 ...
- 2.1.3 客户端网络连接对象
2.1.3 客户端网络连接对象 客户端网络连接对象(NetworkClient )管理了客户端和服务端之间的 网络通信,包括连接的建立.发送客户端请求 . 读取客户端响应.回顾下2 . 1.2节中第 ...
最新文章
- 算法:详解布隆过滤器的原理、使用场景和注意事项@知乎.Young Chen
- iOS单独集成QQ分享功能
- Nature Microbiology:肠道菌群如何划分肠型
- Mac卸载mysql并安装mysql升级到8.0.13版本
- r4卡2020整合内核_R4卡使用方法!游戏介绍及常见问题!任天堂掌机通用
- slf4j 和 log4j的关系及合用Maven配置
- linux执行jar包命令没有主清单熟悉,jar命令成功完成 java -jar 命令却提示“没有主清单属性”!...
- Java读取多层级xml文件
- swiper移入暂停_react中swiper注意事项及鼠标划入停止轮播
- STM32之通用定时器编码器模式
- 领导给了一堆无序杂乱的数据,我写了个Python自动化脚本
- 关于C#关闭窗体后,依旧有后台进程在运行的解决方法
- 转:12种JavaScript MVC框架之比较
- 浅谈 HTTPS 和 SSL -TLS 协议的背景与基础
- JQuery iframe页面通过parent方法操作父页面中的元素与方法(实例讲解)
- BGP选择路由的策略
- sai厚涂上色教程,sai厚涂绘画人物上色教程!
- 【第十届“泰迪杯”数据挖掘挑战赛】C题:疫情背景下的周边游需求图谱分析 赛后总结、46页论文及代码
- Mac OS X 10.7.x Lion下添加离线词典
- 图像直方图以及直方图的应用