ThreadLocal源码及引用类型
一、前言
如果每个线程在创建的时候都有个初始值,如每个线程都分配一个线程号;针对每个请求线程需要包含请求的参数等一些信息,所以可以构造这样一个对象,将这个对象设置为共享
变量,统一设置初始值,但是每个线程对这个值的修改都是相互独立的。这个对象就是ThreadLocal,可以理解为:CopyValueIntoEveryThread,即复制值到每个线程中。
ThreadLocal初衷是在线程并发时,解决变量共享问题,但由于过度设计,比如弱引用和哈希碰撞,导致理解难度大、使用成本高,反而称为故障高发点,容易出现内存泄漏、脏数据、共享对象更新等问题。
二、引用类型(强软弱虚)
对象在堆上创建之后所持有的引用其实是一种变量类型,引用之间可以通过引用赋值构成一条引用链。从GC Roots开始遍历,判断引用是否可达。引用的可达性是判断能够被垃圾回收的基本条件。JVM会据此自动管理内存的分配和回收。
但在某些情况下,即使引用可达,也希望能够根据语义的强弱进行有选择的回收,以保证系统的正常运行。根据引用类型语义的强弱来决定垃圾回收的阶段,我们
可以把引用分为强引用、软引用、弱引用和虚引用四类(强软弱虚,不溢二每)。后三类开发可以通过代码方式来决定对象的垃圾回收时机。
1)强引用
Java默认的引用类型,最为常见,如 Object obj=new Object(); obj就是Object对象的一个强引用。
这样的变量声明和定义就会产生对该对象的强引用。只要对象有强引用指向,并且GCRoots可达,那么Java内存回收时,即使内存频临耗尽,也不会回收该对象
2)软引用 (SoftReference<T>)
一些还有用但非必须的对象引用。软引用指向的对象,内存足够时不会被回收,OOM内存溢出之前进行回收。
主要用来保存缓存服务器中间计算结果及不需要实时保存的用户行为等。
3)弱引用(WeakReference<T>)
一些非必须的对象引用,比软引用更弱一些。如果弱引用指向的对象只存在弱引用这一条线路,那么则在第二次YoungGC时会被回收。
主要用来指向某个易消失的对象,在强引用断开后,此引用不会劫持对象(在强引用置为null时能自动感知,并且主动断开引用指向的对象)。
调用WeekReference.get()可能返回null,要注意NPE。
4)虚引用(PhantomReference<T>)
是极弱的一种引用关系,定义完成后,就无法通过该引用获取指向的对象,即无法通过get方法来获得目标对象的强引用从而使用目标对象,
其get()方法获取到的数据永远为null,因此也被成为幽灵引用。
每次垃圾回收的时候都会被回收,为一个对象设置虚引用的唯一目的就是希望能在这个对象被回收时收到一个系统通知。
虚引用必须与引用队列联合使用,当垃圾回收时,如果发现存在虚引用,就会在回收对象内存前,把这个虚引用加入与之关联的引用队列中。
它们父类Reference源码:
public abstract class Reference<T> { private T referent; /* Treated specially by GC */ volatile ReferenceQueue<? super T> queue; Reference(T referent) { this(referent, null); } Reference(T referent, ReferenceQueue<? super T> queue) { this.referent = referent; this.queue = (queue == null) ? ReferenceQueue.NULL : queue; } public T get() { return this.referent; } }
如:创建一个User对象的弱引用:
WeakReference<User> userWeakReference=new WeakReference<>(new User());
WeakReference的get()方法继承自Reference<T>,用来获取继承自父类的私有属性referent(子类可以继承父类的私有属性,但是不能直接访问(子类对象.属性名的方式),但是可以通过方法访问)。
软引用与弱引用区别:
User user=new User(); // user将new User()的引用分别传递给下面的软引用和弱引用 SoftReference<User> userSoftReference=new SoftReference<>(user); WeakReference<User> userWeakReference=new WeakReference<>(user);
在中间引用user置为null后,软引用userSoftReference还一直持有其传递过来的new User()的有效引用。
在中间引用user置为null后,弱引用能自动感知,并且主动断开指向其传递过来的new User()对象的引用。
三、弱引用与ThreadLocal:
弱引用的特性被用到了ThreadLocal上,JDK的设计愿景是在ThreadLocal对象消失后,线程再持有这个ThreadLocal对象是没有任何意义的,应该进行回收,从而避免内存泄漏。但实际业务场景中却并非如此,弱引用的这种设计方式反而增加了对ThreadLocal和Thread体系的理解难度。
在线程Thread中,所有Entry对象都被ThreadLocalMap类实例化对象threadLocals持有。当线程执行完毕后,线程执行完毕后,线程对象内的实例属性均会被垃圾回收,包括threadLocals。
Entry继承了WeakReference<ThreadLocal<?>>,所以Entry对象是ThreadLocal对象的一个弱引用,即使线程正在执行中,只要ThreadLocal对象引用被置为null,Entry的key即ThreadLocal对象就会在下一次YGC时被垃圾回收。
而在ThreadLocal使用set()和get()时,又会自动地将那些key==null的value置为null,使value能够被垃圾回收,避免内存泄漏。
但是,ThreadLocal对象通常作为私有静态变量使用,其生命周期不会至少不会随着线程结束而结束。所以ThreadLocal对象引用被置为null不成立,value被自动回收行不通,只能在线程结束前调用remove()方法来避免内存泄漏。
如果ThreadLocal是非静态的,属于某个线程实例类,那么就失去了线程间共享的本质。
ThreadLocal的作用:通常用于一个线程内,跨类、跨方法传递数据。如果没有ThreadLocal,那么相互之间的信息传递,必须要靠返回值和参数,必然造成代码耦合。
四、ThreadLocal
防止任务在共享资源上产生冲突的有两种方式:
1)一种是加锁
2)第二种方式是根除对变量的共享。
线程本地存储是一种自动化机制没可以为使用相同变量的每个不同的线程都创建不同的存储。因此,如果你有5个线程都要使用变量x所表示的对象,那线程本地存储就会生成5个用于x的不同的存储块。主要是,它们使得你可以将状态与线程关联起来。
相比加锁而言,是用空间换时间。
创建和管理线程本地存储可以由java.lang.ThreadLocal类来实现。
ThreadLocal对象通常当作静态域存储在线程的局部变量表中。
Thread源码:
public class Thread implements Runnable { ... /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; ... }
ThreadLocal源码:
ThreadLocal内部维护一个类似Map的ThreadLocalMap数据结构,ThreadLocalMap中包含一个Entry[] 数组(Entry是TreadLocalMap的内部静态类),实际用来存储键值对的是数组中的Entry对象(是当前ThreadLocal对象的一个弱引用),值为泛型的Object。
Entry数组的下标为ThreadLocal的hashcode。ThreadLocalMap的key为ThreadLocal对象,通过ThreadLocal对象计算出Entry数组的下标,获取存储的值。
ThreadLocal中的静态内部类ThreadLocalMap(有个Entry[] 属性)
及ThreadLocalMap内部的静态内部类Entry:
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. * Entry继承了WeakReference,key是ThreadLocal对象,当key为null时, * 表示key引用的ThreadLocal对象已经被回收,这时这个entry可以从table中删除 */ static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } // Entry中只有一个value属性 /** * The initial capacity -- MUST be a power of two. */ private static final int INITIAL_CAPACITY = 16; /** * The table, resized as necessary. * table.length MUST always be a power of two. */ private Entry[] table; // 一个ThreadLocalMap对象内包含多个Entry对象 /** * The number of entries in the table. */ private int size = 0; /** * The next size value at which to resize. */ private int threshold; // Default to 0 ... }
initialValue方法: 返回该线程局部变量的初始值,缺省是null;
可以在实例化ThreadLocal的时候重写initialValue方法指定局部变量初始值。
protected T initialValue() { return null; }
set方法:设置当前线程的线程局部变量的值
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocal.ThreadLocalMap threadLocals属性值 // 如果不为空,重新赋值;如果为空,直接赋值 if (map != null) map.set(this, value); else createMap(t, value); }
getMap(获取当前线程的ThreadLocal.ThreadLocalMap threadLocals属性):
// 获取当前线程t的ThreadLocalMap属性,t.threadLocals
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
每个线程都有自己的ThreadLocalMap
Thread: /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
get()方法:返回当前线程的局部变量(ThreadLocalMap)中对应Key为当前ThreadLocal对象的Value
如果为null,则将当前ThreadLocal的对象作为Key,initialValue作为Value put进当前线程的ThreadLocalMap中
public T get() { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程的ThreadLocalMap属性 ThreadLocal.ThreadLocalMap map = getMap(t); // 如果ThreadLocalMap属性不为空 if (map != null) { // 根据当前ThreadLocal对象的hashCode作为下标,获取ThreadLocalMap中Entry数组中的key为当前ThreadLocal对象的Entry对象 ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this); // 如果entry不为空,获取其的value,即线程的本地变量值 if (e != null) { @SuppressWarnings("unchecked") T result = (T) e.value; return result; } } // 如果当前线程的ThreadLocalMap为空,则赋初始值 return setInitialValue(); }
如果map==null,则直接执行setInitialValue()。如果map已经创建,就表示Thread类的threadLocals属性已经初始化,
如果e==null,依然会执行setInitialValue()。
setInitialValue:
/** * 初始化当前线程的ThreadLocalMap属性 * @return */ private T setInitialValue() { // 获取ThreadLocal对象重写的initialValue方法赋予的本地变量初始值 T value = initialValue(); // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程的ThreadLocalMap属性 ThreadLocal.ThreadLocalMap map = getMap(t); // 如果ThreadLocalMap不为空 if (map != null) // 判断ThreadLocalMap中的Entry数组中是否包含key为当前ThreadLocal对象的Entry对象, // 若有,替换值;若没有,则新建一个Entry对象并添加到Entry数组中 map.set(this, value); else // 如果当前线程的ThreadLocalMap属性为空,那么则初始化 createMap(t, value); return value; } /** * 初始化当前线程的ThreadLocalMap属性,初始化ThreadLocalMap中的Entry数组,并往数组中添加一个Entry对象 * Entry对象的key属性为当前ThreadLocal对象,value为给定的初始值 * * @param t * @param firstValue */ void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue); }
所以在调用get方法的时候会为每个线程的ThreadLocalMap设置以当前ThreadLocal对象为key,ThreadLocal对象中的初始值作为value。
初始值为initialValue()方法返回的值,因此在实例化ThreadLocal的时候最好重写initialValue()方法(个人觉得)。
remove方法:将当前线程局部变量的值删除(删除key为当前ThreadLocal对象元素)。
ThreadLocal: public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } ThreadLocalMap: /** * 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; } }
ThreadLocal是每一个线程私有的,每一个线程都有独立的变量副本,其他线程不能访问,所以不存在线程安全问题。也不会影响程序的性能。
ThreadLocal对象通常都是由private static修饰的,因为都需要复制进入本地线程,所以非static意义不大。但是ThreadLocal无法解决共享对象的更新问题。
ThreadLocal有个静态内部类ThreadLocalMap,而ThreadLocalMap还有一个静态内部类Entry【 static class Entry extends WeakReference<ThreadLocal> 】,ThreadLocal的ThreadLocalMap属性的赋值是在createMap方法中进行的。ThreadLocal和ThreadLocalMap有三组对应的方法:get、set和remove;在ThreadLocal中只对它们做校验和判断,
最终的实现会落在ThreadLocalMap上,具体是在其内的Entry[]数组上,ThreadLocalMap的key是ThreadLocal对象,通过ThreadLocal对象计算出Entry[]数组的下标。Entry继承自WeakReference,没有方法,只有一个value属性【Object value;】,也就是最终存储的值。
ThreadLocal总结:
1、一个Thread有且仅有一个ThreadLocalMap对象(ThreadLocalMap中的每个Key都是一个ThreadLocal对象,也就是说ThreadLocalMap可持有多个ThreadLocal)
2、一个ThreadLocalMap对象存储一个Entry[]数组
3、Entry[]数组的下标为ThreadLocal对象的hashCode
4、每个Entry对象都是一个ThreadLocal对象的弱引用
5、一个ThreadLocal对象可以被多个线程所共享(共享时,每个线程的key相同,Value不同)
5、ThreadLocal对象不持有Value,Value由线程的Entry对象持有
每个线程都有一个ThreadLocalMap属性。ThreadLocalMap包含一个Entry[]数组,ThreadLocalMap的key属性是一个ThreadLocal对象,Value为Entry[]数组中的一个Entry对象,Entry对象是ThreadLocal对象的一个弱引用,其属性value是ThreadLocal<T> 泛型中具体传入的参数类型对象。
ThreadLocalMap属性的赋值是在ThreadLocal类中的createMap()中进行的。
ThreadLocal的set()方法即获取当前线程,然后拿到当前线程的ThreadLocalMap,然后以当前ThreadLoca对象为key,设置传入的Value。
调用不同的ThreadLocal的set()方法会为当前线程存入以对应的ThreadLocal对象为Key,传入的参数为Value
所以通过ThreadLocal可以为每个线程设置线程本地变量
ThreadLocal的副作用:
为了使线程安全的共享某个变量,JDK提供了ThreadLocal,但是ThreadLocal有一定的副作用,主要问题是会产生脏数据和内存泄漏。这两个问题通常是在线程池中的线程
使用ThreadLocal引发的,因为线程池有线程复用和内存常驻两个特点。
1、脏数据,线程复用会产生脏数据。由于线程池会重用Thread对象,那么与Thread绑定的类的静态属性ThreadLocal变量也会被重用。如果在实现的线程run()方法体
中没有显式地调用remove清理线程相关的ThreadLocal信息,那么倘若下一个线程不调用set()设置初始值,就可能get()到重用的线程信息。包括ThreadLocal所关联的线程
对象的value值。
2、内存泄漏,在源码注释中提示使用static关键字来修饰ThreadLocal(放在常量池中了)。在此场景下,寄希望于ThreadLocal对象失去引用后,触发弱引用机制来
回收Entry的Value就不现实了。
所以,解决上面两个问题的方法就是在每次用完ThreadLocal时,必须要及时调用remove()方法清理。
ThreadLocal应用:
ThreadLocal可用来为每个请求存储上下文,如session、线程号;
ThreadLocal用来保存每次请求的线程号的例子:
ThreadContext:线程上下文
/** * 线程上下文,一个线程内所需的上下文变量参数,使用ThreadLocal保存副本 * * @author yangyongjie * @date 2019/9/12 * @desc */ public class ThreadContext { /** * 每个线程的私有变量,每个线程都有独立的变量副本,所以使用private static final修饰,因为都需要复制进入本地线程 */ private static final ThreadLocal<ThreadContext> THREAD_LOCAL = new ThreadLocal<ThreadContext>() { @Override protected ThreadContext initialValue() { // 每个ThreadContext对象将被put进每个线程的ThreadLocalMap中,key为当前threadLocal对象。 // 经过get之后每个线程中都拥有一个独立的ThreadContext对象 return new ThreadContext(); } }; public static ThreadContext currentThreadContext() { /*ThreadContext threadContext = THREAD_LOCAL.get(); if (threadContext == null) { THREAD_LOCAL.set(new ThreadContext()); threadContext = THREAD_LOCAL.get(); } return threadContext;*/ return THREAD_LOCAL.get(); } public static void remove() { THREAD_LOCAL.remove(); } private String threadId; public String getThreadId() { return threadId; } public void setThreadId(String threadId) { this.threadId = threadId; } @Override public String toString() { return JacksonJsonUtil.toString(this); } }
AOP拦截每次请求,并设置线程号:
/** * 为每一个的HTTP请求添加线程号 * * @author yangyongjie * @date 2019/9/2 * @desc */ @Aspect @Component public class LogAspect { @Pointcut(value = "@annotation(org.springframework.web.bind.annotation.RequestMapping)") private void webPointcut() { // doNothing } /** * 为所有的HTTP请求添加线程号 * * @param joinPoint * @throws Throwable */ @Around(value = "webPointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { // 方法执行前加上线程号,并将线程号放到线程本地变量中 MDCUtil.init(); // 执行拦截的方法 Object result; try { result = joinPoint.proceed(); } finally { // 方法执行结束移除线程号,并移除线程本地变量,防止内存泄漏 MDCUtil.remove(); } return result; } }
优化后的AOP,加上了异常捕捉,返回结果打印,方法耗时统计
@Aspect @Component public class LogAspect { private static final Logger LOGGER = LoggerFactory.getLogger(LogAspect.class); @Pointcut(value = "@annotation(org.springframework.web.bind.annotation.RequestMapping)") private void webPointcut() { // doNothing } /** * 为所有的HTTP请求添加线程号 * * @param joinPoint * @throws Throwable */ @Around(value = "webPointcut()") public Object around(ProceedingJoinPoint joinPoint) { // 执行开始的时间 Long beginTime = System.currentTimeMillis(); // 方法执行前加上线程号,并将线程号放到线程本地变量中 MDCUtil.init(); // 获取切点的方法名 String methodName = joinPoint.getSignature().getName(); // 执行拦截的方法 Object result = null; try { result = joinPoint.proceed(); } catch (Throwable throwable) { LOGGER.error("{}方法执行异常:" + throwable.getMessage(), methodName, throwable); CommonResult commonResult = new CommonResult("9999", "系统异常"); result = JacksonJsonUtil.toString(commonResult); } finally { LOGGER.info("{}方法返回结果:{}", methodName, result); Long endTime = System.currentTimeMillis(); LOGGER.info("{}方法耗时{}毫秒", methodName, endTime - beginTime); // 方法执行结束移除线程号,并移除线程本地变量,防止内存泄漏 MDCUtil.remove(); } return result; } }
MDCUtil:设置线程号并移除线程局部变量
import org.slf4j.MDC; import java.util.UUID; /** * 日志相关工具类 * * @author yangyongjie * @date 2019/9/17 * @desc */ public class MDCUtil { private MDCUtil() { } private static final String STR_THREAD_ID = "threadId"; /** * 初始化日志参数并保存在线程副本中 */ public static void init() { String uuid = UUID.randomUUID().toString().replaceAll("-", ""); MDC.put(STR_THREAD_ID, uuid); ThreadContext.currentThreadContext().setThreadId(uuid); } /** * 初始化日志参数 */ public static void initWithOutContext() { String uuid = UUID.randomUUID().toString().replaceAll("-", ""); MDC.put(STR_THREAD_ID, uuid); } /** * 移除线程号和线程副本 */ public static void remove() { MDC.remove(STR_THREAD_ID); ThreadContext.remove(); } /** * 移除线程号 */ public static void removeWithOutContext() { MDC.remove(STR_THREAD_ID); } }
Spring的事务管理,用ThreadLocal存储Connection,从而各个Dao可以获取同一个Connection,可以进行事务回滚,提交等操作。
Spring动态数据源-AbstractRoutingDataSource将当前处理对应的数据源放在ThreadLocal中
END.