@ThreadLocal原理与Java中的四种引用深入理解 您所在的位置:网站首页 threadlocal为什么要用弱引用 @ThreadLocal原理与Java中的四种引用深入理解

@ThreadLocal原理与Java中的四种引用深入理解

2024-07-17 13:14| 来源: 网络整理| 查看: 265

ThreadLocal是什么?为什么要使用ThreadLocal一个ThreadLocal的使用案例ThreadLocal的原理为什么不直接用线程id作为ThreadLocalMap的key为什么会导致内存泄漏呢?是因为弱引用吗?Key为什么要设计成弱引用呢?强引用不行?InheritableThreadLocal保证父子线程间的共享数据ThreadLocal的应用场景和使用注意点 ThreadLocal是什么?为什么要使用ThreadLocal?

ThreadLocal是什么?

ThreadLocal,即线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是在操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了并发场景下的线程安全问题。

//创建一个ThreadLocal变量 static ThreadLocal localVariable = new ThreadLocal();

为什么要使用ThreadLocal

并发场景下,会存在多个线程同时修改一个共享变量的场景。这就可能会出现线性安全问题。

为了解决线性安全问题,可以用加锁的方式,比如使用synchronized 或者Lock。但是加锁的方式,可能会导致系统变慢。加锁示意图如下:

还有另外一种方案,就是使用空间换时间的方式,即使用ThreadLocal。使用ThreadLocal类访问共享变量时,会在每个线程的本地,都保存一份共享变量的拷贝副本。多线程对共享变量修改时,实际上操作的是这个变量副本,从而保证线性安全。

ThreadLocal的原理 ThreadLocal的内存结构图

为了有个宏观的认识,我们先来看下ThreadLocal的内存结构图

从内存结构图,我们可以看到:

Thread类中,有个ThreadLocal.ThreadLocalMap 的成员变量。ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型对象值。 关键源码分析

回到Thread类源码,可以看到成员变量ThreadLocalMap的初始值是为null

public class Thread implements Runnable { //ThreadLocal.ThreadLocalMap是Thread的属性 ThreadLocal.ThreadLocalMap threadLocals = null; }

ThreadLocalMap的关键源码如下:

static class ThreadLocalMap { static class Entry extends WeakReference k = e.get(); if (k == key) { e.value = value; return; } //如果k等于null,则说明该索引位之前放的key(threadLocal对象)被回收了 //这通常是因为外部将threadLocal变量置为null //又因为entry对threadLocal持有的是弱引用,一轮GC过后,对象被回收。 //这种情况下,既然用户代码都已经将threadLocal置为null,那么也就没打算 //再通过该对象作为key去取到之前放入threadLocalMap的value, //因此ThreadLocalMap中会直接替换调这种不新鲜的entry。 if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; //触发一次Log2(N)复杂度的扫描,目的是清除过期Entry if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }

如ThreadLocal的get方法:

public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { //去ThreadLocalMap获取Entry,方法里面有key==null的清除逻辑 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } private Entry getEntry(ThreadLocal key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else //里面有key==null的清除逻辑 return getEntryAfterMiss(key, i, e); } private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal k = e.get(); if (k == key) return e; // Entry的key为null,则表明没有外部引用,且被GC回收,是一个过期Entry if (k == null) expungeStaleEntry(i); //删除过期的Entry else i = nextIndex(i, len); e = tab[i]; } return null; } key是弱引用,GC回收会影响ThreadLocal的正常工作嘛?

ThreadLocal的key既然是弱引用.会不会GC贸然把key回收掉,进而影响ThreadLocal的正常使用?

弱引用:具有弱引用的对象拥有更短暂的生命周期。如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)

其实不会的,因为有ThreadLocal变量引用着它,是不会被GC回收的,除非手动把ThreadLocal变量设置为null,跑个demo来验证一下:

public class WeakReferenceTest { public static void main(String[] args) { Object object = new Object(); WeakReference testWeakReference = new WeakReference(object); System.out.println("GC回收之前,弱引用:"+testWeakReference.get()); //触发系统垃圾回收 System.gc(); System.out.println("GC回收之后,弱引用:"+testWeakReference.get()); //手动设置为object对象为null object=null; System.gc(); System.out.println("对象object设置为null,GC回收之后,弱引用:"+testWeakReference.get()); } } 运行结果: GC回收之前,弱引用:java.lang.Object@7b23ec81 GC回收之后,弱引用:java.lang.Object@7b23ec81 对象object设置为null,GC回收之后,弱引用:null

结论就是,不会的,因为有ThreadLocal变量引用着它,是不会被GC回收的,除非手动把ThreadLocal变量设置为null

ThreadLocal内存泄漏的demo

看下一个内存泄漏的例子,其实就是用线程池,一直往里面放对象

public class ThreadLocalTestDemo { private static ThreadLocal tianLuoThreadLocal = new ThreadLocal(); public static void main(String[] args) throws InterruptedException { ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue()); for (int i = 0; i < 10; ++i) { threadPoolExecutor.execute(new Runnable() { @Override public void run() { System.out.println("创建对象:"); TianLuoClass tianLuoClass = new TianLuoClass(); tianLuoThreadLocal.set(tianLuoClass); tianLuoClass = null; //将对象设置为 null,表示此对象不在使用了 // tianLuoThreadLocal.remove(); } }); Thread.sleep(1000); } } static class TianLuoClass { // 100M private byte[] bytes = new byte[100 * 1024 * 1024]; } } 创建对象: 创建对象: 创建对象: 创建对象: Exception in thread "pool-1-thread-4" java.lang.OutOfMemoryError: Java heap space at com.example.dto.ThreadLocalTestDemo$TianLuoClass.(ThreadLocalTestDemo.java:33) at com.example.dto.ThreadLocalTestDemo$1.run(ThreadLocalTestDemo.java:21) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)

运行结果出现了OOM,tianLuoThreadLocal.remove();加上后,则不会OOM。

创建对象: 创建对象: 创建对象: 创建对象: 创建对象: 创建对象: 创建对象: 创建对象: ......

没有手动设置tianLuoThreadLocal变量为null,但是还是会内存泄漏。因为使用了线程池,线程池有很长的生命周期,因此线程池会一直持有tianLuoClass对象的value值,即使设置tianLuoClass = null;引用还是存在的。这就好像,把一个个对象object放到一个list列表里,然后再单独把object设置为null的道理是一样的,列表的对象还是存在的。

public static void main(String[] args) { List list = new ArrayList(); Object object = new Object(); list.add(object); object = null; System.out.println(list.size()); } //运行结果 1

所以内存泄漏就这样发生啦,最后内存是有限的,就抛出了OOM了。如果加上threadLocal.remove();,则不会内存泄漏。为什么呢?因为threadLocal.remove();会清除Entry,源码如下:

既然内存泄漏不一定是因为弱引用,那为什么需要设计为弱引用呢?来探讨下:

Entry的Key为什么要设计成弱引用呢?

通过源码,可以看到Entry的Key是设计为弱引用的(ThreadLocalMap使用ThreadLocal的弱引用作为Key的)。为什么要设计为弱引用呢?

先回忆一下四种引用:

强引用:平时new了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。弱引用:具有弱引用的对象拥有更短暂的生命周期。如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)。虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。

官方文档,为什么要设计为弱引用:

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

ThreadLocal的引用示意图:

下面分情况讨论:

如果Key使用强引用:当ThreadLocalMap的对象被回收了,但是Entry还持有ThreadLocal对象的强引用的话,如果没有手动删除,Entry就不会被回收,会出现Entry的内存泄漏问题。如果Key使用弱引用:当ThreadLocalMap的对象被回收了,因为Entry持有ThreadLocal对象的弱引用,即使没有手动删除,ThreadLocal也会被回收。value则在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

因此可以发现,使用弱引用作为Entry的Key,可以多一层保障:弱引用ThreadLocal不会轻易内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

实际上,内存泄漏的根本原因是:不再被使用的Entry,没有从线程的ThreadLocalMap中删除。一般删除不再使用的Entry有这两种方式:

一种就是,使用完ThreadLocal,手动调用remove(),把Entry从ThreadLocalMap中删除另外一种方式就是:ThreadLocalMap的自动清除机制去清除过期Entry.(ThreadLocalMap的get(),set()时都会触发对过期Entry的清除)

为了安全地使用ThreadLocal,必须要像每次使用完锁就解锁一样,在每次使用完ThreadLocal后都要手动调用remove()来清理无用的Entry。

InheritableThreadLocal保证父子线程间的共享数据

 ThreadLocal是线程隔离的,如果希望父子线程共享数据,如何做到呢?可以使用InheritableThreadLocal。先来看看demo:

public class InheritableThreadLocalTest { public static void main(String[] args) { ThreadLocal threadLocal = new ThreadLocal(); InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal(); threadLocal.set("勒布朗詹姆斯-23"); inheritableThreadLocal.set("威斯布鲁克-0"); Thread thread = new Thread(()->{ System.out.println("ThreadLocal value " + threadLocal.get()); System.out.println("InheritableThreadLocal value " + inheritableThreadLocal.get()); }); thread.start(); } } //运行结果 ThreadLocal value null InheritableThreadLocal value 威斯布鲁克-0

可以发现,在子线程中,是可以获取到父线程的 InheritableThreadLocal 类型变量的值,但是不能获取到 ThreadLocal 类型变量的值。

获取不到ThreadLocal 类型的值,好理解,因为它是线程隔离的嘛。InheritableThreadLocal 是如何做到的呢?原理是什么呢?

在Thread类中,除了成员变量threadLocals之外,还有另一个成员变量:inheritableThreadLocals。它们两类型是一样的:

public class Thread implements Runnable { ThreadLocalMap threadLocals = null; ThreadLocalMap inheritableThreadLocals = null; }

Thread类的init方法中,有一段初始化设置:

private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { ...... if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */ this.stackSize = stackSize; /* Set thread ID */ tid = nextThreadID(); } static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); }

可以发现,当parent的inheritableThreadLocals不为null时,就会将parent的inheritableThreadLocals,赋值给前线程的inheritableThreadLocals。说白了,就是如果当前线程的inheritableThreadLocals不为null,就从父线程哪里拷贝过来一个过来,类似于另外一个ThreadLocal,但是数据从父线程那里来的。

ThreadLocal的应用场景和使用注意点

ThreadLocal的很重要一个注意点,就是使用完,要手动调用remove()。

而ThreadLocal的应用场景主要有以下这几种:

使用日期工具类,当用到SimpleDateFormat,使用ThreadLocal保证线性安全全局存储用户信息(用户信息存入ThreadLocal,那么当前线程在任何地方需要时,都可以使用)保证同一个线程,获取的数据库连接Connection是同一个,使用ThreadLocal来解决线程安全的问题使用MDC保存日志信息。 Java中的四种引用及其应用 强引用:

强引用是使用最普遍的引用,也是通常情况下使用的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。

Object strongReference = new Object();

当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 如果强引用对象不使用时,一般赋值为null以待垃圾回收。

软引用:

如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

// 强引用 String strongReference = new String("abc"); // 软引用 String str = new String("abc"); SoftReference softReference = new SoftReference(str);

软引用对象是在jvm内存不够的时候才会被回收,调用System.gc()方法只是起通知作用,JVM什么时候扫描回收对象是JVM自己的状态决定的。就算扫描到软引用对象也不一定会回收它,只有内存不够的时候才会回收。 

应用:

软引用可用来实现内存敏感的高速缓存。

浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。

如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建;如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出。

这时候就可以使用软引用,很好的解决了实际的问题:

// 获取浏览器对象进行浏览 Browser browser = new Browser(); // 从后台程序加载浏览页面 BrowserPage page = browser.getPage(); // 将浏览完毕的页面置为软引用 SoftReference softReference = new SoftReference(page); // 回退或者再次浏览此页面时 if(softReference.get() != null) { // 内存充足,还没有被回收器回收,直接获取缓存 page = softReference.get(); } else { // 内存不足,软引用的对象已经回收 page = browser.getPage(); // 重新构建软引用 softReference = new SoftReference(page); }

Mybatis 缓存类 SoftCache 用到的软引用

public Object getObject(Object key) { Object result = null; SoftReference softReference = (SoftReference)this.delegate.getObject(key); if (softReference != null) { result = softReference.get(); if (result == null) { this.delegate.removeObject(key); } else { synchronized(this.hardLinksToAvoidGarbageCollection) { this.hardLinksToAvoidGarbageCollection.addFirst(result); if (this.hardLinksToAvoidGarbageCollection.size() > this.numberOfHardLinks) { this.hardLinksToAvoidGarbageCollection.removeLast(); } } } } return result; } 弱引用:

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

String str = new String("abc"); WeakReference weakReference = new WeakReference(str); str = null;

注意:如果一个对象是偶尔(很少)的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用Weak Reference来记住此对象。

下面的代码会让一个弱引用再次变为一个强引用:

String str = new String("abc"); WeakReference weakReference = new WeakReference(str); // 弱引用转强引用 String strongReference = weakReference.get(); 应用:

1.  官方文档这么写的,弱引用常被用来实现规范化映射,JDK 中的 WeakHashMap 就是一个这样的例子----------------------待学习

2. ThreadLocalMap使用ThreadLocal的弱引用作为key,具体看上文。

虚引用:

虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

应用:

虚引用主要用来跟踪对象被垃圾回收器回收的活动。 

巨人的肩膀

面试必备:ThreadLocal详解 - 掘金

四种引用类型在Springboot中的使用 - 掘金 -------------待学习,等SpringBoot源码达到一定量再看

理解Java的强引用、软引用、弱引用和虚引用 - 掘金

阿里面试回顾: 说说强引用、软引用、弱引用、虚引用? - 掘金

Java面试必问:ThreadLocal终极篇 - 掘金



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

      专题文章
        CopyRight 2018-2019 实验室设备网 版权所有