学习笔记:Java并发编程(补)ThreadLocal
- 【尚硅谷】学习视频:https://www.bilibili.com/video/BV1ar4y1x727
- 【黑马程序员】学习视频:https://www.bilibili.com/video/BV15b4y117RJ
参考书籍
- 《实战 JAVA 高并发程序设计》 葛一鸣 著
- 《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著
系列目录
- 学习笔记:Java 并发编程①_基础知识入门
- 学习笔记:Java 并发编程②_共享模型之管程
- 学习笔记:Java 并发编程③_共享模型之内存
- 学习笔记:Java 并发编程④_共享模型之无锁
- 学习笔记:Java 并发编程⑤_共享模型之不可变
- 学习笔记:Java 并发编程⑥_共享模型之并发工具_线程池
- 学习笔记:Java 并发编程⑥_共享模型之并发工具_JUC
- 学习笔记:Java并发编程(补)CompletableFuture
- 学习笔记:Java并发编程(补)ThreadLocal
前言
系列文章目录中的前七篇文章的相关学习视频都是:黑马程序员深入学习 Java 并发编程,JUC 并发编程全套教程。然而这个时长为 32h 的视频中并没有 ThreadLocal 的相关知识,这个知识点是非常重要的,需要掌握。故找了相关的视频来学习这个重要的知识点,写在了系列文章目录中的第九篇博客里。第九篇的相关学习视频是 尚硅谷 JUC 并发编程(对标阿里 P6-P7)
- 若文章内容或图片失效,请留言反馈。
- 部分素材来自网络,若不小心影响到您的利益,请联系博主删除。
- 写这篇博客旨在制作笔记,方便个人在线阅览,巩固知识。无他用。
ThreaLocal<T>
:该类提供线程局部变量
常问问题
- 请描述 ThreadLocal 中 ThreadLocalMap 的数据结构和关系
- ThreadLocal 中的 key 是弱引用,这是为什么?
- ThreadLocal 中的内存泄露问题的原因
- 在 ThreadLocal 中,最后为什么要加 remove() 方法
1.基本概念
ThreadLocal 提供局部变量。这些变量和正常的变量是不同的,每一个线程在访问 ThreadLocal 实例的时候(通过 ThreadLocal 提供 的 get() 或 set() 方法)都有着属于自己的、独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段,使用它的目的是把状态(例:用户ID 或 事务ID)与线程关联起来。
类似于填表:100 个人和一支笔,每个人都得挨个填写,必须保证不出现哄抢现象,负责谁也填不完;如果是 100 个人,人手一支笔,那么大家就可以很快的完成填表操作。
ThreadLocal 可以让每一个线程都有自己专属的本地变量副本(人人有份)。每一个线程都绑定了自己的值,通过 get() 方法、set() 方法获取到默认值,或是将其值更改为当前线程中所存的副本的值,从而避免了线程安全的问题。
不过,为每一个线程分配一个对象的工作并不是由 ThreadLocal 完成的,而是需要在应用层面保证的。如果在应用中,为每一个线程分配相同的对象实例,此时 ThreadLocal 也不能保证线程安全。
2.相关 API
变量和类型 | 方法 | 描述 |
---|---|---|
T | get() | 返回当前线程的线程局部变量副本中的值 |
protected T | initialValue() | 返回此线程局部变量的当前线程的“初始值” |
void | remove() | 删除此线程局部变量的当前线程值 |
void | set(T value) | 将此线程局部变量的当前线程副本设置为指定值 |
static <S> ThreadLocal<S> | withInitial(Supplier<? extends S> supplier | 创建一个线程局部变量 |
3.实操
3.1.注意事项
3.1.1.当前线程的 “初始值”
- 这里提一下
withInitial(Supplier<? extends S> supplier
和initialValue()
上面的这俩方法的功能是差不太多的
initialValue()
可以通过匿名内部类的方式 返回此线程局部变量的当前线程的 “初始值”
ThreadLocal<Integer> saleVolume = new ThreadLocal<Integer>() {@Overrideprotected Integer initialValue() {return 0;}
};
但是匿名内部类这种写法的还是过于繁琐了,更推荐 withInitial(Supplier<? extends S> supplier
的方式来创建一个线程局部变量
ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);
3.1.2.回收自定义的 ThreadLocal 变量
- 这里提一下 ThreadLocal 的相关要求(阿里巴巴 Java开发手册)
一般来说,我们 必须要回收自定义的 ThreadLocal 变量。尤其是在线程池的场景下,线程经常会被复用,如果不清理掉自定义的 ThreadLocal 变量,很可能会影响后续业务逻辑和造成内存泄露等问题。尽量在 try-finally
块进行回收
objectThreadLocal.set(userInfo);try { // ...
} finally { objectThreadLocal.remove();
}
3.2.案例一
案例:5 个销售卖房子,公司按照总销售额来统计,最终五个人平均分配奖金,那么此时我们只需要知道最终的销售额就行
然而,时代变了。现在是希望各位销售凭本事抽提成了,按照各自的出单数来发放对应的奖金了。
House.java
public class House {int saleCount = 0;public synchronized void saleHouse() {++saleCount;}ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);public void saleVolumeByThreadLocal() {saleVolume.set(1 + saleVolume.get());}
}
ThreadLocalDemo_1.java
@Slf4j(topic = "c.Demo_1")
public class ThreadLocalDemo_1 {public static void main(String[] args) throws InterruptedException {House house = new House();for (int i = 1; i <= 5; i++) {new Thread(() -> {int size = new Random().nextInt(5) + 1;try {for (int j = 1; j <= size; j++) {house.saleHouse();house.saleVolumeByThreadLocal();}log.info("【{} 号销售卖出了 {} 套】", Thread.currentThread().getName(), house.saleVolume.get());} finally {house.saleVolume.remove();}}, String.valueOf(i)).start();}TimeUnit.MILLISECONDS.sleep(300);System.out.println("----------------------------------------------------------------");log.info("【一共卖出了 {} 套】", house.saleCount);}
}
控制台输出信息
19:13:46.044 [1] INFO c.Demo_1 - 【1 号销售卖出了 2 套】
19:13:46.044 [4] INFO c.Demo_1 - 【4 号销售卖出了 4 套】
19:13:46.044 [5] INFO c.Demo_1 - 【5 号销售卖出了 1 套】
19:13:46.044 [2] INFO c.Demo_1 - 【2 号销售卖出了 2 套】
19:13:46.044 [3] INFO c.Demo_1 - 【3 号销售卖出了 5 套】
----------------------------------------------------------------
19:13:46.345 [main] INFO c.Demo_1 - 【一共卖出了 14 套】
3.3.案例二
此处仍然是在证明回收自定义的 ThreadLocal 变量的重要性。
MyData.java
public class MyData {ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(() -> 0);public void add() {threadLocalField.set(1 + threadLocalField.get());}
}
ThreadLocalDemo_2
@Slf4j(topic = "c.ThreadLocalDemo_2")
public class ThreadLocalDemo_2 {public static void main(String[] args) {MyData myData = new MyData();ExecutorService threadPool = Executors.newFixedThreadPool(3);try {for (int i = 0; i <= 10; i++) {try {threadPool.submit(() -> {Integer beforeInt = myData.threadLocalField.get();myData.add();Integer afterInt = myData.threadLocalField.get();log.info("【beforeInt:{}、afterint:{}】", beforeInt, afterInt);});} finally {myData.threadLocalField.remove();}}} catch (Exception e) {e.printStackTrace();} finally {threadPool.shutdown();}}
}
控制台输出信息
21:23:43.817 [pool-1-thread-3] INFO c.Demo_2 - 【beforeInt:0、afterint:1】
21:23:43.817 [pool-1-thread-1] INFO c.Demo_2 - 【beforeInt:0、afterint:1】
21:23:43.817 [pool-1-thread-2] INFO c.Demo_2 - 【beforeInt:0、afterint:1】
21:23:43.828 [pool-1-thread-1] INFO c.Demo_2 - 【beforeInt:1、afterint:2】
21:23:43.828 [pool-1-thread-2] INFO c.Demo_2 - 【beforeInt:1、afterint:2】
21:23:43.828 [pool-1-thread-3] INFO c.Demo_2 - 【beforeInt:1、afterint:2】
21:23:43.829 [pool-1-thread-2] INFO c.Demo_2 - 【beforeInt:2、afterint:3】
21:23:43.829 [pool-1-thread-1] INFO c.Demo_2 - 【beforeInt:2、afterint:3】
21:23:43.829 [pool-1-thread-3] INFO c.Demo_2 - 【beforeInt:2、afterint:3】
21:23:43.829 [pool-1-thread-2] INFO c.Demo_2 - 【beforeInt:3、afterint:4】
21:23:43.829 [pool-1-thread-1] INFO c.Demo_2 - 【beforeInt:3、afterint:4】
如果没有调用 ThreadLocal 的 remove() 的话,则会造成如下的结果
21:24:53.053 [pool-1-thread-3] INFO c.Demo_2 - 【beforeInt:0、afterint:1】
21:24:53.053 [pool-1-thread-1] INFO c.Demo_2 - 【beforeInt:0、afterint:1】
21:24:53.053 [pool-1-thread-2] INFO c.Demo_2 - 【beforeInt:0、afterint:1】
21:24:53.064 [pool-1-thread-3] INFO c.Demo_2 - 【beforeInt:1、afterint:2】
21:24:53.064 [pool-1-thread-2] INFO c.Demo_2 - 【beforeInt:1、afterint:2】
21:24:53.064 [pool-1-thread-1] INFO c.Demo_2 - 【beforeInt:1、afterint:2】
21:24:53.064 [pool-1-thread-3] INFO c.Demo_2 - 【beforeInt:2、afterint:3】
21:24:53.064 [pool-1-thread-2] INFO c.Demo_2 - 【beforeInt:2、afterint:3】
21:24:53.064 [pool-1-thread-3] INFO c.Demo_2 - 【beforeInt:3、afterint:4】
21:24:53.064 [pool-1-thread-2] INFO c.Demo_2 - 【beforeInt:3、afterint:4】
21:24:53.064 [pool-1-thread-1] INFO c.Demo_2 - 【beforeInt:2、afterint:3】
如上所示,后一个任务被前一个任务影响到了,这不符合线程池中提交的任务分别运行在独立的线程中的想法。
3.4.小结
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前线程自己使用
- 既然其他 Thread 不可访问,那就不存在多线程共享的问题
- 统一设置初始值,但是每个线程对这个值的修改都是互相独立的(也就是各改各的)
如何才能不争抢?
- 加入 synchronized 或者 lock 控制线程的访问顺序
- ThreadLocal 人手一份,大家各自安好,无需抢夺
3.5.SimpleDateFormat
既然上面提到了 阿里巴巴 Java开发手册 的并发处理,那就再提一个 SimpleDateFormat 的介绍
SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,则必须加锁,或者使用 DateUtils 工具类。
正例:注意线程安全,使用 DateUtils。亦推荐如下处理:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); }
};
说明:如果是 JDK 8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat
官方给出的解释:simple beautiful strong immutable thread-safe
。
4.ThreadLocal 源码
4.1.ThreadLocalMap
- Thread 和 ThreadLocal
java/lang/Thread.java
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
- ThreadLocal 和 ThreadLocalMap
java/lang/ThreadLocal.java
static class ThreadLocalMap {... ...
}
ThreadLocalMap 是 ThreadLocal 中的一个静态内部类
ThreadLocalMap 实际上就是一个以 threadLocal 实例为 key,任意对象为 value 的 Entry 对象
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}
当我们为 threadLocal 变量赋值时,实际上就是把当前 threadLocal 实例为 key,值为 value> 的 Entry 对象,存在 threadLocalMap 中
4.2.get() 方法
get() 方法先取得当前线程的 ThreadLocalMap 对象,然后将自己作为 key 来取得内部的实际数据
ThreadLocal.get()
源码(java/lang/ThreadLocal.java
)
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();
}
相关方法 |
java/lang/Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;
java/lang/ThreadLocal.java
ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}
private ThreadLocalMap(ThreadLocalMap parentMap) {... ...private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);}... ...
}
private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;
}
如果不给 ThreadLocal 设值的话,一般会默认设 null 值(protected T initialValue()
)
为了避免日后程序运行时报空指针异常,一定要给 ThreadLocal 设置初始值。
protected T initialValue() {return null;
}
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}
private static final int INITIAL_CAPACITY = 16;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);
}
4.3.set() 方法
在 set() 方法中,首先获得当前线程对象,然后通过 getMap() 方法获取到线程的 ThreadLocalMap,并将值存入到 ThreadLocalMap 中
java/lang/ThreadLocal.java
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
}
private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();if (k == key) {e.value = value;return;}if (k == null) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}
4.4.remove() 方法
java/lang/ThreadLocal.java
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);
}
// Remove the entry for key.
private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {e.clear();expungeStaleEntry(i);return;}}
}
4.4.小结
分析
可以这样理解:ThreadLocalMap 从字面上就可以看出这是一个保存了 Threadlocal 对象的 map(准确的说,ThreadLocalMap 是以 ThreadLocal 为 Key 的 Entry),一个经历了两层包装的 ThreadLocal 对象。
可以这样理解:JVM 内部维护了一个线程版的 Map<ThreadLocal, Value>
。其通过 ThreadLocal 对象的 set() 方法,以 ThreadLocal 对象自己为 key,存入 ThreadLocalMap 中;在每一个线程要用到这个 T 的时候,都需要当前的线程去 Map 中获取;通过这样的方式,使得每一个线程都有自己独属的变量,人手一份,不争不抢。既然竞争条件都被消除了,那么它在并发模式下就是绝对安全的变量。
总结
ThreadLocal 是一个壳子,真正的存储结构是 ThreadLocal 中的静态内部类 ThreadLocalMap。
每个 Thread 对象维护着一个 ThreadLocalMap 的引用,ThreadLocalMap 是 ThreadLocal 的内部类,用 Entry 来进行存储。
- 调用 ThreadLocal 的 set() 方法时,实际上就是往 ThreadLocalMap 设置值,key 是 ThreadLocal 对象,值 Value 是传递进来的对象
- 调用 ThreadLocal 的 get() 方法时,实际上就是往 ThreadLocalMap 获取值,key 是 ThreadLocal 对象
ThreadLocal 本身并不存储值(ThreadLocal 是一个壳子),它只是自己作为一个 key 来让线程从 ThreadLocalMap 获取 value。
正因为这个原理,所以 ThreadLocal 能够实现 “数据隔离”,获取当前线程的局部变量值,不受其他线程影响。
5.内存泄露问题
不会再次被使用的对象 或 变量占用的内存 不能被回收,就是内存泄露。
5.1.四种引用
有关四种引用的更多信息,我都写在了这篇博客里:【Java 虚拟机②】垃圾回收,此处只是提个大概。
JAVA 技术允许使用 finalize() 方法 在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
java/lang/Object.java
protected void finalize() throws Throwable { }
参考书籍:《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著
在 JDK 1.2 版之后,Java 对引用的概念进行了扩充,将引用分为四种类型。
强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)。
这 4 种引用的强度依次逐渐减弱。
强引用是最传统的 “引用” 的定义,是指在程序代码之中普遍存在的引用赋值
- 类似于
Object obj=new Object()
这种引用关系。- 无论处于何种情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
- 因此,强引用是造成 JAVA 内存泄露的主要原因之一。
软引用是用来描述一些还有用,但非必须的对象。
- 只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收。
- 如果这次回收还没有足够的内存,才会抛出内存溢出异常。
- 在 JDK 1.2 版之后提供了 SoftReference 类来实现软引用。
弱引用也是用来描述那些非必须对象。
- 但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
- 当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
- 在 JDK 1.2 版之后提供了 WeakReference 类来实现弱引用。
虚引用也称为 “幽灵引用” 或者 “幻影引用”,它是最弱的一种引用关系。
- 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
- 为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
- 在 JDK 1.2 版之后提供了 PhantomReference 类来实现虚引用。
- 强引用
只有所有 GC Roots 对象都不通过 强引用 引用该对象,该对象才能被垃圾回收。 - 软引用(SoftReference)
仅有软引用引用该对象时,在垃圾回收后内存仍不足的情况下,会再次触发垃圾回收,回收软引用对象。
可以配合 引用队列 来释放软引用自身。
软引用通常用在对内存敏感的程序中,比如高速缓存中就有用到软引用。
简言之,对于只有软引用的对象而言,内存足够的时候就保留它,内存不够用就会回收它。 - 弱引用(WeakReference)
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象。
但若要释放弱引用自身,则需要配合 引用队列 来释放弱引用自身 - 虚引用(PhantomReference)必须配合 引用队列 使用(主要配合 ByteBuffer)。
被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放 直接内存
例如:由 Reference Handler 线程通过 Cleaner 的 clean 方法调用 Unsafe.freeMemory 来释放直接内存
注意细节
- 虚引用必须要和引用队列(ReferenceQueue)联合使用
虚引用需要java.lang.PhantomReference
类来实现。与其他几个引用不同,虚引用不会决定对象的生命周期。
如果一个对象只是持有虚引用,那么它在任何时候都可能被垃圾回收期回收。它不可以被单独使用,也不可以通过它来访问对象。 - PhantomReference 的 get() 方法总是返回 null,因此无法访问对应的引用对象。
虚引用的主要作用是跟踪对象被垃圾回收的状态。
它仅仅是提供了一种确保对象被 finalize 之后,做某些事情的通知机制。 - 处理监控,通知使用
设置虚引用的唯一目的,就是为了在这个对象被收集器回收的时候,收到一个系统通知或者后续添加进一步的处理,用来实现比 finalize 机制更灵活的回收操作。
小结
5.2.Thread.exit()
参考书籍:《实战 JAVA 高并发程序设计》 葛一鸣 著
在了解了 ThreadLocal 的内部实现后,我们自然会引出一个问题。
那就是这些变量是维护在 Thread 类内部的(ThreadLocalMap 定义所在类),这也意味着只要线程不退出,对象的引用将一直存在。
当线程退出时,Thread 类会进行一些清理工作,其中就包括清理 ThreadLocalMap
java/lang/Thread.java
private void exit() {if (group != null) {group.threadTerminated(this);group = null;}/* Aggressively null out all reference fields: see bug 4006245 */target = null;/* Speed the release of some of these resources *//* ******************************************************************** */threadLocals = null;inheritableThreadLocals = null;/* ******************************************************************** */inheritedAccessControlContext = null;blocker = null;uncaughtExceptionHandler = null;
}
因此,如果我们使用线程池,那就意味着当前线程未必会退出(比如固定大小的线程池,线程总是存在)。
如果是这样的话,将一些大的对象设置到 ThreadLocal 中(它实际保存在线程持有的 threadLocalMap 内),可能会使系统出现内存泄露(这里我的意思是:你设置了对象到 ThreadLocal 中,但是不清理它,在你使用几次后,这个对象也不再有用了,但是它却无法被回收)。
此时,如果你希望及时回收对象,最好使用ThreadLocal.remove()
方法将这个变量移除。就像我们习惯性地关闭数据库连接一样。如果你确实不需要这个对象了,那么就应该告诉虚拟机,请把它回收掉,防止内存泄露。
另外一种有趣的情况是 JDK 也可能允许你像释放普通变量一样释放 ThreadLocal。
例:我们有时候为了加速垃圾回收,会特意写出类似obj = null
之类的代码。如果这么做,obj 所指向的对象就会更容易地被垃圾回收器发现,从而加速回收。同理,如果对于 ThreadLocal 的变量,我们也手动将其设置为 null,比如tl = null
。那么这个 ThreadLocal 对应的所有线程的局部变量都有可能被回收。
5.3.源代码使用弱引用
public class T {volatile boolean flag;public static void main(String[] args) {ThreadLocal<String> t1 = new ThreadLocal<>();t1.set("zzyybs@126.com");t1.get();}
}
为什么源代码要用弱引用?
当 function01 方法执行完毕后,栈帧销毁,强引用 t1 也就没有了。
但问题在于此时的 ThreadLocalMap 里的某个 entry 的 key 依旧指向这这个对象。
- 如果这个 key 引用是强引用,就会导致 key 指向的 ThreadLocal 对象及 v 指向的对象不能被 GC 回收,造成内存泄漏
- 如果这个 key 引用是弱引用就大概率会减少内存泄漏的问题(还有一个 key 为 null 的坑)。
使用弱引用,就可以使 ThreadLocal 对象在方法执行完毕后顺利被回收且 Entry 的 key 引用指向为 null
此后,我们调用 get()、set()、remove() 方法的时候,就会尝试去删除 key 为 null 的 entry,可以释放 value 对象所占用的内存。
这里我再贴一下 《实战 JAVA 高并发程序设计》 书中的内容。
参考书籍:《实战 JAVA 高并发程序设计》 葛一鸣 著
ThreadLocalMap 的实现使用了弱引用。弱引用是比强引用弱得多的引用。Java 虚拟机在垃圾回收时,如果发现弱引用,就会立即回收。ThreadLocalMap 内部由一系列 Entry 构成,每一个 Entry 都是WeakReference<ThreadLocal>
java/lang/ThreadLocal.java
static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}... ...}
这里的参数 k 就是 Map 的 key,v 就是 Map 的 value。
其中 k 也就是 ThreadLocal 实例,作为弱引用使用(super(k)
就是调用了 WeakReference 的构造函数)。
因此,虽然这里使用 ThreadLocal 作为 Map 的 key,但是实际上,它并不真的持有 ThreadLocal 的引用。
而当 ThreadLocal 的外部强引用被回收时,ThreadLocalMap 中的 key 就会变成 null。
当系统进行 ThreadLocalMap清理时(比如将新的变量加入表中,就会自动执行一次清理。不过 JDK 不一定会进行一次彻底清理)时,就会自然将这些垃圾数据回收。
5.4.key 为 null 的 entry
我们为 threadLocal 变量赋值,实际上就是当前的 Entry(threadLocal 实例为 key,值为 null)往这个 threadLocalMap 中存放。
Entry 中的 key 是弱引用,在 threadLocal 外部强引用被置为 null(t1 = null
)后,如果系统发生 GC 时,根据可达性分析,这个 threadLocal 就没有办法如何一条链路能够引用到它,这个 ThreadLocal 势必会被回收。这样一来,ThreadLocalMap 中就会出现 key 是 null 的 Entry,就没有办法访问到这些 key 是 null 的 Entry 的 value。如果当前线程迟迟不结束的话(比如正好在使用线程池),这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value
,这样就无法回收,造成了内存泄露。
当然,如果当前 thread 运行结束,threadLocal、threadLocalMap、Entry 没有引用链是可达的,在 GC 时就可以被系统回收。
然而在实际开发中,我们通常会使用线程池来维护我们的线程,比如 Executors.newFixedThreadPool()
创建线程的时候,为了复用线程,我们是不会让它结束的,此时 threadLocal 的内存就值的我们小心了。
虽然弱引用保证了 key 指向的 ThreadLocal 对象可以被及时回收,但是 v 指向的 value 对象是需要 ThreadLocalMap 调用 get()、set() 时发现 key 为 null,才会去回收整个 entry、value,因此弱引用不能百发百保证内存不会泄露。
我们需要在不使用某个 ThreadLocal 对象后,手动调用 remove() 方法来删除它,尤其是在线程池中。这里不仅有内存泄露的问题,还有旧线程遗留的数据对当前线程造成影响的问题(因为线程池中的线程是复用的,这就意味着这个线程的 ThreadLocalMap 的对象也是重复使用的,如果我们不手动调用 remove() 方法,那么后面的线程就有可能获取到上个线程遗留下来的 value 值,从而造成 Bug)。
5.5.清除脏 Entry
在 java/lang/ThreadLocal.java
中的静态内部类 ThreadLocalMap 有这样一个方法 expungeStaleEntry()
这个方法会找到脏 Entry,即 key = null
的 Entry,,然后进行删除。
private int expungeStaleEntry(int staleSlot) {... ...Entry e;int i;for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)){ThreadLocal<?> k = e.get();if (k == null) {e.value = null;tab[i] = null;size--;} else { ... }}return i;
}
在 ThreadLocal 的生命周期里,针对 ThreadLocal 的内存泄露问题,ThreadLocal 中的 get()、set()、remove() 方法都会间接地调用到这个 expungeStaleEntry() 方法,来清理掉 key 是 null 的脏 entry
这也就是 阿里巴巴 Java开发手册 强制要求使用 ThreadLocal.remove()
的原因所在。
5.6.小结
- 一定要设置初始值(
ThreadLocal.withInitial(() -> 初始值)
) - 建议用 static 修饰
- 用完之后一定要手动 remove() 操作 ThreadLocal
阿里巴巴 Java开发手册
ThreadLocal 可以实现线程的数据隔离,但这不在它本身,而在于 Thread 的 ThreadLocalMap。
故 ThreadLocalMap 可以只初始化一次,分配一次内存空间足以,没必要作为成员变量多次被初始化。
总结(尚硅谷版)
学习视频:【尚硅谷】ThreadLocal 之小结
- ThreadLocal 并不解决线程间共享数据的问题
- ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
- ThreadLocal 通过隐式的在不同线程内创建独立实例副本,从而避免了实例线程安全的问题。
- 每个线程持有一个只属于自己的专属 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题
- ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题
- 都会通过 expungeStaleEntry(),cleanSomeSlots(),replaceStaleEntry() 这三个方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法(清除脏 entry,防止内存泄露)
- 群雄逐鹿起纷争,人各一份天下安
总结(黑马程序员版)
学习视频:【黑马程序员】Java 面试宝典_ThreadLocal
要求
- 掌握 ThreadLocal 的作用与原理
- 掌握 ThreadLocal 的内存释放时机
作用
- ThreadLocal 可以实现 资源对象 的线程隔离,让每个线程各用各的 资源对象,避免争用引发的 线程安全问题
- ThreadLocal 同时实现了线程内的资源共享
原理:每个线程内有一个 ThreadLocalMap 类型的成员变量(静态内部类),用来存储资源对象
- 调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
- 调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
- 调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值
ThreadLocalMap 的一些特点
- key 的 hash 值 统一分配
- 初始容量 16,扩容因子 2/3,扩容容量翻倍
- key 索引冲突后用开放寻址法解决冲突
弱引用 key:ThreadLocalMap 中的 key 被设计为弱引用,原因如下
- Thread 可能需要长时间运行(如线程池中的线程),如果 key 不再使用,需要在内存不足(GC)时释放其占用的内存
内存释放时机(ThreadLocalMap 中的 key 为弱引用,value 是强引用)
- 被动 GC 释放 key
- 仅是让 key 的内存释放,关联 value 的内存并不会释放
- 懒惰被动释放 value
get key
时,发现是 null key,则释放其 value 内存set key
时,会使用启发式扫描,清除临近的 null key 的 value 内存,启发次数与元素个数,是否发现 null key 有关- 主动 remove 释放 key、value
- 会同时释放 key、value 的内存,也会清除临近的 null key 的 value 内存
- 推荐使用它,因为一般使用 ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收
相关文章:
学习笔记:Java并发编程(补)ThreadLocal
【尚硅谷】学习视频:https://www.bilibili.com/video/BV1ar4y1x727【黑马程序员】学习视频:https://www.bilibili.com/video/BV15b4y117RJ 参考书籍 《实战 JAVA 高并发程序设计》 葛一鸣 著《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著…...
HashMap底层实现原理及面试题
文章目录1. 常见的数据结构有三种结构1.1 各自数据结构的特点2. HashMap2.1 概述2.2 底层结构2.2.1 HashMa实现原理:2.2.1.1 map.put(k,v)实现原理2.2.1.2 map.get(k)实现原理2.2.1.3 resize源码2.2.2 HashMap常用的变量2.2.3 HashMap构造函数2.3 JDK1.8之前存在的问…...
【STM32】进阶(二):DMA+ADC实现模拟量检测
1、简述 DMA:Direct Memory Access,直接内存访问 ADC:Analog to Digital Converter,模数转换器,模拟信号转换成数字信号的电路(采样-量化-编码) 参考博客: STM32DMA功能详解 STM32…...
Lab2_Simple Shell_2020
Lab2: 实验目的:给xv6添加新的系统调用 并理解系统调用是如何工作的,并理解xv6内核的一些内部特征 实验准备: 阅读xv6的第2章以及第4章的4.3,4.3小节熟悉下面的源码 用户态相关的代码:user/user.h和user/usys.pl内核态相关的代…...
2023最全电商API接口 高并发请求 实时数据 支持定制 电商数据 买家卖家数据
电商日常运营很容易理解,就是店铺商品维护,上下架,评价维护,库存数量,协助美工完成制作详情页。店铺DSR,好评率,提升客服服务等等,这些基础而且每天都必须做循环做的工作。借助电商A…...
MySQL 的索引类型
1. 按照功能划分 按照功能来划分,索引主要有四种: 普通索引唯一性索引主键索引全文索引 普通索引就是最最基础的索引,这种索引没有任何的约束作用,它存在的主要意义就是提高查询效率。 普通索引创建方式如下: CREATE…...
< Linux > 进程信号
目录 1、信号入门 生活角度的信号 技术应用角度的信号 前台进程 && 后台进程 信号概念 用kill -l命令察看系统定义的信号列表 信号处理的方式 2、信号产生前 用户层产生信号的方式 3、产生信号 3.1、通过终端按键产生信号 3.2、核心转储core dump 3.3、调用系统函数…...
Pyspark基础入门7_RDD的内核调度
Pyspark 注:大家觉得博客好的话,别忘了点赞收藏呀,本人每周都会更新关于人工智能和大数据相关的内容,内容多为原创,Python Java Scala SQL 代码,CV NLP 推荐系统等,Spark Flink Kafka Hbase Hi…...
C/C++每日一练(20230307)
目录 1. 国名排序 ★★ 2. 重复的DNA序列 ★★★ 3. 买卖股票的最佳时机 III ★★★ 🌟 每日一练刷题专栏 C/C 每日一练 专栏 Python 每日一练 专栏 1. 国名排序 小李在准备明天的广交会,明天有来自世界各国的客房跟他们谈生意,…...
一条SQL查询语句是如何执行的?
平时我们使用数据库,看到的通常都是一个整体。比如,你有个最简单的表,表里只有一个ID字段,在执行下面这个查询语句时: mysql> select * from T where ID10; 我们看到的只是输入一条语句,返…...
tcsh常用配置
查看当前的shell类型 在 Linux 的世界中,有着许多 shell 程序。常见的有: Bourne shell (sh) C shell (csh) TC shell (tcsh) Korn shell (ksh) Bourne Again shell (bash) 其中,最常用的就是bash和tcsh,本次文章介绍tcsh的…...
YOLOv5源码逐行超详细注释与解读(2)——推理部分detect.py
前言 前面简单介绍了YOLOv5的项目目录结构(直通车:YOLOv5源码逐行超详细注释与解读(1)——项目目录结构解析),对项目整体有了大致了解。 今天要学习的是detect.py。通常这个文件是用来预测一张图片或者一…...
什么叫个非对称加密?中间人攻击?数字签名?
非对称加密也称为公钥密码。就是用公钥来进行加密,撒子意思? 非对称加密 在对称加密中,我们只需要一个密钥,通信双方同时持有。而非对称加密需要4个密钥,来完成完整的双方通信。通信双方各自准备一对公钥和私钥。其中…...
2023.03.07 小记与展望
碎碎念系列全新改版! 以后就叫小记和展望系列 最近事情比较多,写篇博客梳理一下自己3月到5月下旬的一个规划 一、关于毕设 毕设马上开题答辩了,准备再重新修改一下开题报告,梳理各阶段目标。 毕设是在去年的大学生创新训练项目…...
MyBatis源码分析(七)MyBatis与Spring的整合原理与源码分析
文章目录写在前面一、SqlSessionFactoryBean配置SqlSessionFactory1、初识SqlSessionFactoryBean2、实现ApplicationListener3、实现InitializingBean接口4、实现FactoryBean接口5、构建SqlSessionFactory二、SqlSessionTemplate1、初始SqlSessionTemplate2、SqlSessionTemplat…...
基于声网 Flutter SDK 实现多人视频通话
前言 本文是由声网社区的开发者“小猿”撰写的Flutter基础教程系列中的第一篇。本文除了讲述实现多人视频通话的过程,还有一些 Flutter 开发方面的知识点。该系列将基于声网 Fluttter SDK 实现视频通话、互动直播,并尝试虚拟背景等更多功能的实现。 如果…...
IT服务管理(ITSM) 中的大数据
当我们谈论IT服务管理(ITSM)领域的大数据时,我们谈论的是关于两件不同的事情: IT 为业务提供的大数据工具/服务 - 对业务运营数据进行数字处理。IT 运营中的大数据 – 处理和利用复杂的 IT 运营数据。 面向业务运营的大数据服务…...
Validator校验之ValidatorUtils
注意:hibernate-validator 与 持久层框架 hibernate 没有什么关系,hibernate-validator 是 hibernate 组织下的一个开源项目 。 hibernate-validator 是 JSR 380(Bean Validation 2.0)、JSR 303(Bean Validation 1.0&…...
C++---背包模型---采药(每日一道算法2023.3.7)
注意事项: 本题是"动态规划—01背包"的扩展题,dp和优化思路不多赘述。 题目: 辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。 为此,他想拜附近最有威望的医师为师。 医师为了判断他的资质&…...
Java各种锁
目录 一、读写锁(ReentrantReadWriteLock) 二、非公平锁(synchronized/ReentrantLock) 三、可重入锁/递归锁(synchronized/ReentrantLock) 四、自旋锁(spinlock) 五、乐观锁/悲观锁 六、死锁 1、死锁代码 2、死锁的检测(jps -l 与 jstack 进程号) 七、sychronized-wait…...
TryHackMe-Tardigrade(应急响应)
Tardigrade 您能否在此 Linux 端点中找到所有基本的持久性机制? 服务器已遭到入侵,安全团队已决定隔离计算机,直到对其进行彻底清理。事件响应团队的初步检查显示,有五个不同的后门。你的工作是在发出信号以使服务器恢复生产之前…...
导出GIS | 将EXCEL表格中坐标导出成GIS格式文件
一 前言 EXCEL是我们日常工作学习数据处理的办公软件,操作易上手,几乎人人都会用。EXCEL表格能够处理各种数据,包括经纬度坐标数据,地址数据等等。 有时因工作需要需将表格中地址数据处理为GIS格式的文件,以便能够将数…...
new set数组对象去重失败
我们知道Set是JS的一个种新的数据结构,和数组类似,和数组不同的是它可以去重,比如存入两个1或两个"123",只有1条数据会存入成功,但有个特殊情况,如果添加到set的值是引用类型,比如数组…...
Acwing: 一道关于线段树的好题(有助于全面理解线段树)
题目链接🔗:2643. 序列操作 - AcWing题库 前驱知识:需要理解线段树的结构和程序基本框架、以及懒标记的操作。 题目描述 题目分析 对区间在线进行修改和查询,一般就是用线段树来解决,观察到题目一共有五个操作&…...
DD-1/40 10-40mA型【接地继电器】
系列型号: DD-1/40接地继电器 DD-1/50接地继电器 DD-1/60接地继电器 一、 用途及工作原理 DD-1型接地继电器为瞬时动作的过电流继电器,用作小电流接地电力系统高电压三相交流发电机和电动机的接地零序过电流保护。继电器线圈接零序电流互感器(电缆式、母…...
【女神节】简单使用C/C++和Python嵌套for循环生成一个小爱心
目录 前言实现分析代码实现代码如下效果如下优化效果代码如下效果如下总结尾叙前言 女神节马上到了,有女朋友的小伙伴是不是已经精心准好礼物了呢!对于已婚男士,是不是整愁今天又该送什么礼物呢!说真的,我也整愁着,有什么要推荐么,评论留言下! 实现分析 可以先在纸上或…...
Biome-BGC生态系统模型与Python融合技术实践应用
查看原文>>> Biome-BGC生态系统模型与Python融合技术实践应用 Biome-BGC是利用站点描述数据、气象数据和植被生理生态参数,模拟日尺度碳、水和氮通量的有效模型,其研究的空间尺度可以从点尺度扩展到陆地生态系统。 在Biome-BGC模型中…...
ESP32 GPIO使用
ESP32 GPIO使用 #define GPIO_OUT_PIN 2 //定义引脚号 #define GPIO_OUTPUT_PIN_SEL (1<<GPIO_OUT_PIN) //定义输出引脚的宏,用来将输出引脚号转换为位掩码void bsp_gpio_init(){gpio_config_t io_conf;io_conf.pin_bit_mask GPIO_OUTPUT_PIN_SE…...
JavaScript 高级4 :正则表达式
JavaScript 高级4 :正则表达式 Date: January 19, 2023 Text: 正则表达式、正则表达式特殊字符、正则表达式中的替换 目标: 能够说出正则表达式的作用 能够写出简单的正则表达式 能够使用正则表达式对表单进行验证 能够使用正则表达式替换内容 正则…...
如何让AI帮你干活-娱乐(3)
背景今天的话题会偏代码技巧一些,对于以前没有接触过代码的朋友或者接触代码开发经验较少的朋友会有些吃力。上篇文章介绍了如何广视角的生成相对稳定的视频。昨天的实现相对简单,主要用的是UI界面来做生成。但是生成的效果其实也显而易见,不…...
房源哪个网站最靠谱/优化公司网站
说明是一组键值对的结构,用于存储key-value的数据创建map//创建方式1 var m new Map([[michael,95],[bob,75],[tracy,85]]); //创建方式2 var m new Map();map添加元素m.set(Adam, 67);map判断元素是否存在m.has(Adam);map获取元素m.get(Adam);map删除元素m.delete(Adam);转载…...
安徽合肥网站制作公司/sem是什么基团
转自:http://www.cnblogs.com/xiekeli/archive/2012/09/06/2674199.html Apache MINA(Multipurpose Infrastructure for Network Applications) 是 Apache 组织一个较新的项目,它为开发高性能和高可用性的网络应用程序提供了非常便利的框架。 这个框架的…...
五分钟自己创建网站的方法/优化设计电子课本下载
简介: 1.RTMP(实时消息传输协议)是Adobe 公司开发的一个基于TCP的应用层协议。 2.RTMP协议中基本的数据单元称为消息(Message)。 3.当RTMP协议在互联网中传输数据的时候,消息会被拆分成更小的单元,称为消息块ÿ…...
郑州树标网站建设/腾讯云域名购买
类内部Hack: IE6 识别下划线 _ 和 * IE7 识别 * (不能识别 _ ) FF两个都不能识别 选择器Hack: IE6能识别 *html IE7能识别 *html 或 *:first-childhtml HTML头部引用(if IE)Hack: 所有IE:&…...
wordpress获取文章发布日期/网络推广网站大全
binlog介绍 我们的SQL执行时,会将sql语句的执行逻辑记录在我们的bin-log当中,什么是bin-log呢? binlog是Server层实现的二进制日志,他会记录我们的cud操作。Binlog有以下几个特点: 1、Binlog在MySQL的Server层实现(引擎…...
嘉兴seo报价/南京seo收费
现在用wordpress的人越来越多,从个人到企业的博客。这Wordpress众多Wordpress资源有一定的关系。你可以通过选择一个适合自己的模板然后快速的建立自己的网站或者博客,甚者不需要任何专业的技术,就可以建立一个漂亮的站点。但是面对众多的模板…...