今天来聊一下ThreadLocal,废话不多说直奔主题。
1、ThreadLocal是什么?
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
ThreadLocal是线程本地变量,每个线程都有一个自己的、独立初始化的变量副本。
2、为什么要用到ThreadLocal?
并发场景下,会遇到多个线程操作同一个同享变量的状况,就可能会发生线程安全问题。这种场景通常可运用加锁来处理。
ThreadLocal处理这个问题换了个思路,已然多线程操作同享变量,那我将同享变量改成线程本地变量,每个线程都有一个变量副本,自己操作自己的副本。。。 采用了空间换时刻的办法,省掉了加锁发生的时刻开支,完成了线程阻隔。
3、ThreadLocal原理
咱们凭借源码来看一下ThreadLocal的履行原理。
3.1 源码分析
首要,来看一下从ThreadLocal的set()
办法开端说起。
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
// 获取当时线程
Thread t = Thread.currentThread();
// 获取线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
// 设置到ThreadLocalMap中
map.set(this, value);
else
// 创立一个ThreadLocalMap,并增加
createMap(t, value);
}
能够看到操作的是当时线程的ThreadLocalMap,这个ThreadLocalMap又是什么呢?
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.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
// ThreadLocalMap维护的是一个Entry数组,能够回想一下Map
private Entry[] table;
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
// 将ThreadLocal做为key存入Entry[]中
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);
}
}
回到刚才的set()
办法,set办法便是将ThreadLocal设置到当时线程的实例变量ThreadLocalMap中(java.lang.Thread目标的实例变量threadLocals),ThreadLocal作为key,ThreadLocal是支撑泛型的,泛型值作为value。
然后看一下ThreadLocal的get()
办法。
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
// 获取当时线程
Thread t = Thread.currentThread();
// 获取线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 根据key(即ThreadLocal)获取value(泛性值)
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 初始化ThreadLocalMap
return setInitialValue();
}
get办法便是用key(即ThreadLocal)获取value(泛性值)泛性质的过程。
3.2 原理小结
- ThreadLocal的
set()
和get()
操作的是Thread类的实例变量ThreadLocalMap。 - ThreadLocalMap内部维护着一个Entry数组,Entry的key是ThreadLocal,value是ThreadLocal的值。
- 每个线程都有自己的一个变量副本,采用了空间换时刻的办法,完成了线程阻隔。
4、ThreadLocal注意事项
不怕最牛的规划,就怕运用的不当。ThreadLocal有些当地需要特别了解一下。
4.1 ThreadLocalMap中的key是ThreadLocal目标
咱们来看一下源码。上文源码分析说到的ThreadLocal的set()
办法中map.set(this, value)
调用的是这个私有办法。
public class ThreadLocal<T> {
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 假如发生哈希抵触,则找下一个值为null的下标(开放地址法)
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;
}
}
// key是传入的ThreadLocal<?>目标
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
}
4.2 ThreadLocalMap中的key是弱引证
首要咱们先了解一下JAVA中的引证。
4.2.1 JAVA中的引证
JAVA中的引证类型有四种:
-
强引证:当咱们new一个目标时,
Object object = new Object()
,被创立的目标便是强引证,是最常见的一种引证。废物收回器不会收回还存在强引证联系的目标,即便抛出OOM也不会。 -
软引证:SoftReference是JAVA中完成软引证的类,
SoftReference<Object> objectSoftReference = new SoftReference(new Object())
,GC是假如内存空间足够,软引证目标不会被收回,否则软引证目标收回。 -
弱引证:WeakReference是JAVA中完成弱引证的类,
WeakReference<Object> objectWeakReference = new WeakReference<>(new Object())
, 弱引证目标被GC检测到了,会当即收回。 -
虚引证:PhantomReference是JAVA中完成虚引证的类,虚引证和几乎没有引证一样,而且
get()
办法回来不了目标实例,能够用来当目标被finalize
之后做一些工作。
// 与其他引证不同的是,PhantomReference有必要传入ReferenceQueue
PhantomReference phantomReference = new PhantomReference(new Object(),null);
// get办法总是回来null
System.out.println("phantomReference:"+phantomReference.get());
4.2.2 为什么是WeakReference
防止内存走漏供给一层保障。
1、一种状况,假如key是强引证,当ThreadLocal没有外部引证时,还保持着强引证,GC不会收回,假如线程完毕前也没有删去,会导致内存走漏。
2、一种状况,假如key是弱引证,当ThreadLocal没有外部引证时,GC会收回弱引证,当然假如线程完毕前也没有GC也没有调用删去,还是会导致内存走漏,所以说供给了一层保障。
4.3 ThreadLocal运用不当会导致内存走漏!
4.3.1 什么是内存走漏?
内存走漏:程序中已动态分配的堆内存由于某种原因程序未开释或无法开释,造成体系内存的浪费,导致程序运转速度减慢甚至体系崩溃等严重后果。
4.3.1 ThreadLocal如何导致内存走漏?
上边介绍为什么ThreadLocalMap中的key是弱引证时其实已经说了,总结一下便是假如ThreadLocal线程完毕前没有GC也没有调用删去,导致内存没有及时开释。
4.4 ThreadLocal的remove()
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
删去此线程本地变量的值。线程完毕前一定要调用此办法。
4.5 ThreadLocal要声明为static?
阿里巴巴开发规约中说到,ThreadLocal建议运用static
修饰。
ThreadLocal是针对一个线程内一切操作共有的,所以设置为静态变量,所以此类实例同享此静态变量,也便是说在类第一次运用时装载,只分配一块存储空间,一切此类的目标(只要是这个线程内界说的)都能够操纵这个变量。
总结一下便是设置为静态,能够省频频创立目标的内存开支。
5、一个ThreadLocal导致内存走漏的比如
创立一个线程为1的线程池。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 60l, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10));
ThreadLocal<Integer> threadLocal = new ThreadLocal();
for (int i = 0; i < 3; i++) {
int finalI = i;
threadPoolExecutor.execute(() -> {
System.out.println("获取到的值为:" + threadLocal.get());
if(null == threadLocal.get()){
threadLocal.set(finalI);
System.out.println("设置的值为:" + finalI);
}
});
}
检查履行结果。
获取到的值为:null
设置的值为:0
获取到的值为:0
获取到的值为:0
能够看到只有第一次设置了值,第二次开端用的都是上一个线程设置的值,第一个的内存没有开释掉还在持续运用。
6、ThreadLocal运用场景
6.1、常见运用场景
- 日志
MDC
类。 - 业务注解
@Transational
。 - 。。。
6.2、项目中运用
项目中能够根据不同场景灵活运用ThreadLocal。
在这里简略完成一个经过增加一个注解就能计算接口履行时刻的比如。
首要声明注解。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TimeConsumingLog {
}
编写AOP。
@Aspect
@Component
public class TimeConsumingLogAop {
@Pointcut(value = "@annotation(com.shuaijie.config.annotation.TimeConsumingLog)")
public void pointcut() {
}
private static ThreadLocal<StopWatch> threadLocal = new ThreadLocal();
@Before("pointcut()")
public void beforeAdvice() {
StopWatch stopWatch = new StopWatch("计算接口耗时");
stopWatch.start();
threadLocal.set(stopWatch);
}
@After("pointcut()")
public void afterAdvice() {
StopWatch stopWatch = threadLocal.get();
if (null != stopWatch) {
if (stopWatch.isRunning()) {
stopWatch.stop();
}
System.out.println(stopWatch.prettyPrint());
}
threadLocal.remove();
}
}
StopWatch是Spring供给的一个工具类,能够计算耗时。
运用注解。
@TimeConsumingLog
@PostMapping("/user/add")
public Boolean addUser(@RequestBody User user) {
return Boolean.TRUE;
}
检查履行结果。
StopWatch '计算接口耗时': running time = 13842700 ns
---------------------------------------------
ns % Task name
---------------------------------------------
013842700 100%
自界说注解具体运用能够看下我的另一篇高雅编码技巧之Spring AOP。
7、总结
- ThreadLocal采用了空间换时刻的办法,完成了同享变量的线程阻隔。
- ThreadLocal建议运用static修饰。
- 线程完毕前一定要调用
remove()
办法。