上一节你弄懂了ThreadLocal是什么、它的基本使用方式、get方法的底层原理。这一节让继续深入研究下:
你有了阅读threadLocal的get方法的经验,set方法的源码会变得非常简单。set源码如下所示:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
上面的脉络是不是很清楚,相信我都不需要画图大家就能理解,和上一节get方法调用的setInitialValue几乎一模一样,只是没有了initialValue()方法而已。
如果当前线程第一次使用threadLcoal.set(Obejct),(假设当前线程之前也没有调用过get方法),就会创建一个默认大小为16的threadLocalMap,并且将key设为threadLocal对象,value设置为对应的某个Object。
如果是第二次set肯走的是map.set(this, value);这句话的分支,直接向当前线程的threadLocalMap中设置一个key-value对。
如下图所示:
你还记得ThreadLocalMap这个每个Thread都有的本地变量吗?这个Map中的核心的数据结构是一个Entry,代表了Key-Value对的数据,Key值是ThreadLocal对象,value是存储的对象数据。代码如下所示:
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继承了一个WeakReference的对象。如果熟悉JVM的同学,可能了解这个对象是什么,它被称作弱引用。
在java中,对象引用可以强引用、软引用、弱引用、虚引用四种,是jvm回收内存判断的重要标准之一。下面我简单给大家介绍下他们是什么,一般应用在什么场景。
强引用StrongReference,一般声明的一个对象,都是强引用。使用场景,比如 Loan l = new Loan(); l就是一个强引用。gc如果发现一个对象被强引用指向,如果JVM空间不足的时候,就算OOM也不会回收它。
软引用SoftReference,当JVM空间不够的时候,gc会先回收软引用的空间。使用场景:适合用于缓存。
举个例子:Andriod用Map缓存位图数据。
private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>(); public void addBitmapToCache(String path) { // 强引用的Bitmap对象 Bitmap bitmap = BitmapFactory.decodeFile(path); // 软引用的Bitmap对象 SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap); // 添加该对象到Map中使其缓存 imageCache.put(path, softBitmap); }
弱引用WeakReference,只要gc发现了弱引用,就会回收掉它的空间。使用场景:ThreadLocalMap, WeakHashMap中的Entry。。
举个例子:ThreadLocalMap中的entry,这个一会我们重点分析这里的原理,为什么这么做。
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
虚引用PhantomReference,这个引用在gc垃圾回收线程看来,就是没有引用的意思,它的作用是帮助JVM管理直接内存DirectBuffer。经典的使用场景:NIO。
举个例子:比如DirectBuffer中的Cleaner就是继承了PhantomReference。
public abstract interface DirectBuffer { public abstract long address(); public abstract java.lang.Object attachment(); public abstract sun.misc.Cleaner cleaner(); } public class Cleaner extends java.lang.ref.PhantomReference { private static final java.lang.ref.ReferenceQueue<java.lang.Object> dummyQueue; //省略 }
上面这四种引用我给大家画一个图,更好理解,如下图所示:
了解了Java中的四种引用的概念后,我们来看下ThreadLcoalMap中的Entry继承了WeakReference。到底是为什么?我们来看如下一个场景。
一个线程thread使用threadLocal对象tl设置了一个value为30M的对象,之后tl=null,不再使用了。tl指向的区域threadLocal对象被gc回收。此时会如下图所示:
这也就解释了,为什么ThreadLocalMap的Entry中的key使用弱引用:
因为若是强引用,即使tl=null,key是强引用的话,仍会指向threadLocal,导致threadLocal不会被回收,造成内存泄漏。而使用了key弱引用的话,就不会有问题,当tl=null的时候,key是弱引用,gc会直接回收调threadLocal内存中的这个对象。虽然使用了弱引用,但是仍存在内存value指向的强引用,指向了一个堆中的对象,此时key对应的threadLocal已经回收,key=null,此时,也无法访问到value了。
所以如果一个set的value如果不在使用或threadLoacal不在使用了,一定要通过remove方法来删除掉之前的key。不然这么使用不当,还是会造成内存泄漏,导致30M的这个vlaue不会被回收掉
最后给大家提几个ThreadLocal的应用场景。你可以想一下,ThreadLocal具备这样特性,可以用在哪里?
Spring 的Transaction机制中的ThreadLocal
最经典的场景就是Spring 的Transaction机制,将一个线程中的事务放入ThreadLocal中,可以在整个方法调用栈中随时取出事务的信息进行修改和操作,不会影响其他的线程的事务。
// TransactionAspectSupport.java private static final ThreadLocal<TransactionInfo> transactionInfoHolder = new NamedThreadLocal<TransactionInfo>("Current aspect-driven transaction");
Log4j2等日志框架中的MDC
public class LogbackMDCAdapter implements MDCAdapter { final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal<Map<String, String>>(); }
SpringCloud Sleuth的请求链路跟踪
通过ThreadLocal传递Trace数据,值得一提的是,还通过之前提到过的Thread的另一个本地变量副本inheritableThreadLocal。在创建子线程的时候,会将父线程的inheritableThreadLocals继承下来,这样就实现了TraceContext在父子线程中的传递。代码如下:
public static final class Default extends CurrentTraceContext{ ThreadLocal<TraceContext> DEFAULT = new ThreadLocal<>(); // Inheritable as Brave 3's ThreadLocalServerClientAndLocalSpanState was inheritable static final InheritableThreadLocal<TraceContext> INHERITABLE = new InheritableThreadLocal<>(); final ThreadLocal<TraceContext> local; }
HDFS edits_log的txId自增后放入线程本地副本
HDFS每次创建一个文件,目录等操作会记录一条日志到edits_log中,每条edit_log都有一个txId,会把这个txId记录到当前线程的txId方便在整个线程过程中随时取用,和修改。
/** * FSEditLog 维护元数据(文件目录树)也叫命名空间的修改 */ @InterfaceAudience.Private @InterfaceStability.Evolving public class FSEditLog implements LogsPurgeable { // stores the most current transactionId of this thread. private static final ThreadLocal<TransactionId> myTransactionId = new ThreadLocal<TransactionId>() { @Override protected synchronized TransactionId initialValue() { return new TransactionId(Long.MAX_VALUE); } }; private long beginTransaaction() { assert Thread.holdsLock(this); txid++; TransactionId id = myTransactionId.get(); id.txid = txid; return now(); }
还有很多的场景可以使用。其实通过上面的几个场景,你应该能发现,其实ThreadLocal最常用的2个场景就是:
1、 线程中,各个方法需要共享变量时使用。除了方法之间传递入参,通过ThreadLocal可以很方便的做到这一点。
2、 多线程操作时,防止并发冲突,保证线程安全。比如一般会拷贝一份数据到线程本地,自己修改本地变量,是线程安全的。
好了,今天的成长记就到这里,你可以在自己的公司遇到的项目中或者开源代码中找一下或者留意一下。看看它们是怎么使用ThreadLocal的。
欢迎你在评论区,写下你遇见的场景。