threadlocal为什么是弱引用,threadlocal为什么是安全的

首页 > 技术 > 作者:YD1662023-04-15 23:32:30

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

3.2 关键源码分析

对照着几段关键源码来看,更容易理解一点哈~我们回到Thread类源码,可以看到成员变量ThreadLocalMap的初始值是为null

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

ThreadLocalMap的关键源码如下:

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; } } //Entry数组 private Entry[] table; // ThreadLocalMap的构造器,ThreadLocal作为key 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); } } 复制代码

ThreadLocal类中的关键set()方法:

public void set(T value) { Thread t = Thread.currentThread(); //获取当前线程t ThreadLocalMap map = getMap(t); //根据当前线程获取到ThreadLocalMap if (map != null) //如果获取的ThreadLocalMap对象不为空 map.set(this, value); //K,V设置到ThreadLocalMap中 else createMap(t, value); //创建一个新的ThreadLocalMap } ThreadLocalMap getMap(Thread t) { return t.threadLocals; //返回Thread对象的ThreadLocalMap属性 } void createMap(Thread t, T firstValue) { //调用ThreadLocalMap的构造函数 t.threadLocals = new ThreadLocalMap(this, firstValue); this表示当前类ThreadLocal } 复制代码

ThreadLocal类中的关键get()方法

public T get() { Thread t = Thread.currentThread();//获取当前线程t ThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMap if (map != null) { //如果获取的ThreadLocalMap对象不为空 //由this(即ThreadLoca对象)得到对应的Value,即ThreadLocal的泛型值 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); //初始化threadLocals成员变量的值 } private T setInitialValue() { T value = initialValue(); //初始化value的值 Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //以当前线程为key,获取threadLocals成员变量,它是一个ThreadLocalMap if (map != null) map.set(this, value); //K,V设置到ThreadLocalMap中 else createMap(t, value); //实例化threadLocals成员变量 return value; } 复制代码

所以怎么回答ThreadLocal的实现原理?如下,最好是能结合以上结构图一起说明哈~

了解完这几个核心方法后,有些小伙伴可能会有疑惑,ThreadLocalMap为什么要用ThreadLocal作为key呢?直接用线程Id不一样嘛?

4. 为什么不直接用线程id作为ThreadLocalMap的key呢?

举个代码例子,如下:

public class TianLuoThreadLocalTest { private static final ThreadLocal<String> threadLocal1 = new ThreadLocal<>(); private static final ThreadLocal<String> threadLocal2 = new ThreadLocal<>(); } 复制代码

这种场景:一个使用类,有两个共享变量,也就是说用了两个ThreadLocal成员变量的话。如果用线程id作为ThreadLocalMap的key,怎么区分哪个ThreadLocal成员变量呢?因此还是需要使用ThreadLocal作为Key来使用。每个ThreadLocal对象,都可以由threadLocalHashCode属性唯一区分的,每一个ThreadLocal对象都可以由这个对象的名字唯一区分(下面的例子)。看下ThreadLocal代码:

public class ThreadLocal<T> { private final int threadLocalHashCode = nextHashCode(); private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } }

然后我们再来看下一个代码例子:

public class TianLuoThreadLocalTest { public static void main(String[] args) { Thread t = new Thread(new Runnable(){ public void run(){ ThreadLocal<TianLuoDTO> threadLocal1 = new ThreadLocal<>(); threadLocal1.set(new TianLuoDTO("公众号:捡田螺的小男孩")); System.out.println(threadLocal1.get()); ThreadLocal<TianLuoDTO> threadLocal2 = new ThreadLocal<>(); threadLocal2.set(new TianLuoDTO("公众号:程序员田螺")); System.out.println(threadLocal2.get()); }}); t.start(); } } //运行结果 TianLuoDTO{name='公众号:捡田螺的小男孩'} TianLuoDTO{name='公众号:程序员田螺'} 复制代码

再对比下这个图,可能就更清晰一点啦:

threadlocal为什么是弱引用,threadlocal为什么是安全的(5)

5. TreadLocal为什么会导致内存泄漏呢?5.1 弱引用导致的内存泄漏呢?

我们先来看看TreadLocal的引用示意图哈:

threadlocal为什么是弱引用,threadlocal为什么是安全的(6)

关于ThreadLocal内存泄漏,网上比较流行的说法是这样的:

ThreadLocalMap使用ThreadLocal的弱引用作为key,当ThreadLocal变量被手动设置为null,即一个ThreadLocal没有外部强引用来引用它,当系统GC时,ThreadLocal一定会被回收。这样的话,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如线程池的核心线程),这些key为null的Entry的value就会一直存在一条强引用链:Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object 永远无法回收,造成内存泄漏。

当ThreadLocal变量被手动设置为null后的引用链图:

threadlocal为什么是弱引用,threadlocal为什么是安全的(7)

实际上,ThreadLocalMap的设计中已经考虑到这种情况。所以也加上了一些防护措施:即在ThreadLocal的get,set,remove方法,都会清除线程ThreadLocalMap里所有key为null的value。

源代码中,是有体现的,如ThreadLocalMap的set方法:

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; } //如果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; } 复制代码5.2 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<Object> 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 复制代码

结论就是,小伙伴放下这个疑惑了,哈哈~

5.3 ThreadLocal内存泄漏的demo

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

public class ThreadLocalTestDemo { private static ThreadLocal<TianLuoClass> 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.<init>(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<Object> list = new ArrayList<>(); Object object = new Object(); list.add(object); object = null; System.out.println(list.size()); } //运行结果 1 复制代码

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

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) { //清除entry e.clear(); expungeStaleEntry(i); return; } } } 复制代码

有些小伙伴说,既然内存泄漏不一定是因为弱引用,那为什么需要设计为弱引用呢?我们来探讨下:

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

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

threadlocal为什么是弱引用,threadlocal为什么是安全的(8)

上一页123下一页

栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.