文章目录
- 用法
- Example1
- Example2
- Springboot @Transcation 注解的原理
- Entry 的 Key 设置为弱引用有什么好处
- 内存泄漏问题
- 为什么 ThreadLocal 不需要 ReferenceQueue
- get()
- getEntry
- getEntryAfterMiss
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).
该类提供线程局部变量。这些变量与普通变量的不同之处在于,每个访问一个变量(通过其 get 或 set 方法)的线程都有自己的、独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段,希望将状态与线程关联(例如,用户 ID 或事务 ID)。
每个线程中维护一个 ThreadLocalMap,ThreadLocalMap 虽然名字里有 Map,实际上是 Entry 数组。Entry 数组中 Entry 的 key 是 ThreadLocal,Value 是 保存的属性。下面以一段代码举例:
public static void main(String[] args) {
ThreadLocal<Integer> tl = new ThreadLocal<>();
tl.set(1);
}
用法
Example1
import java.util.concurrent.TimeUnit;
public class ThreadLocal2 {
static ThreadLocal<Person> tl = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(()->{
try {
//先睡两秒
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(tl.get());
}).start();
new Thread(()->{
try {
//先睡一秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
tl.set(new Person());
}).start();
}
static class Person {
String name = "zhangsan";
}
}
输出:
null
Example2
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
ThreadLocal.withInitial(nextId::getAndIncrement);
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
输出:
get() = 0
Springboot @Transcation 注解的原理
@Transcation 注解实现原理是基于 AOP 编程思想。AOP 编程思想是将与业务无关的功能抽离出来,并在业务处理前后进行插入。在 Springboot 源码中,@Transcation 注解就是使用 AOP 编程思想实现的。
当一个方法上有 @Transcation 注解时,在 Springboot 源码中会为这个方法生成一个代理对象。在这个代理对象中,会添加两个通知:前置通知和后置通知。前置通知会在真实方法执行前执行,后置通知会在真实方法执行后执行。
在前置通知中,会开启一个事务,并将事务挂起。在真实方法执行完毕后,会在后置通知中提交事务。如果真实方法抛出异常,则会在后置通知中回滚事务。
这样,当一个方法上有 @Transcation 注解时,方法的执行过程就会被包装在一个事务中,从而可以实现事务的控制。
在 Spring 中,每个线程都有一个 ThreadLocal 对象,可以用来存储线程私有的数据。在 Spring 的事务管理器中,会将当前线程所使用的数据源放入 ThreadLocal 对象中。所以,在前置通知中,可以从 ThreadLocal 对象中获取当前线程所使用的数据源。
获取数据源:
DataSource dataSource = TransactionSynchronizationManager.getResource(dataSource);
设置数据源:
Entry 的 Key 设置为弱引用有什么好处
我们先假设不是弱引用,是强引用。当 tl 在外部的强引用被回收以后,因为 ThreadLocalMap 的 Entry 的 Key 还是强引用,只要线程不结束,这个 ThreadLocal 一直不会被回收。
如果是弱引用,tl 在外部的强引用被回收以后,GC 时就会回收这个弱引用的 ThreadLocal 对象。但是此时 Value 还没有被回收,这就导致了内存泄漏问题。
内存泄漏问题
ThreadLocal 类本身并不会导致内存泄漏。但是,如果使用不当,可能会导致内存泄漏。上文中就描述了 Value 没有被回收导致的内存泄漏问题,那么我们看什么时候 Value 会被回收。
- 线程结束,ThreadLocalMap 清除
由于在 JAVA 中线程对象与 ThreadLocal 对象绑定在一起,所以只要线程没了,空间自然回收。因为线程对象回收时,ThreadLocalMap 的强引用也没了,所以内部全部回收。 - 线程未结束
- 调用 remove 方法,释放 Entry 的内存。
- 调用 get 和 set 方法时,释放 key == null 的 Entry 的内存。
remove、get和set方法都会调用 expungeStaleEntry 方法来释放 value 的内存。
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
下面是 expungeStaleEntry 方法的源码。
Expunge a stale entry by rehashing any possibly colliding entries lying between staleSlot and the next null slot. This also expunges any other stale entries encountered before the trailing null. See Knuth, Section 6.4
- staleSlot : index of slot known to have null key
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
若调用完 ThreadLocal 后,线程未结束,且没有调用 remove、set 和 get 方法。才会导致内存泄漏。虽然 get 和 set 时,会回收 value。但是我们不能依赖该机制回收内存,这只是个兜底的方案。开发中建议使用完 ThreadLocal 后就 remove 即可。
为什么 ThreadLocal 不需要 ReferenceQueue
为什么 [[01 JDK8 源码/WeakHashMap]] 需要 ReferenceQueue 来保存需要被回收的 Entry,而 ThreadLocal 不需要。
因为 Entry 对象的强引用本身就保存在 ThreadLocalMap 中了,只要 Entry 的 key 等于 null,就表明需要回收这个 Entry。具体参考源码中 getEntryAfterMiss 方法:
if (k == null)
expungeStaleEntry(i);
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 initialValue method.
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 每个线程维护一个 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
getEntry
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
return getEntryAfterMiss(key, i, e);
}
getEntryAfterMiss
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;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}