ThreadLocal:线程本地变量,针对同一变量可以保存线程私有的值,保证多线程之间的数据隔离性。为什么不用方法内的局部变量?局部变量作用域为当前方法,而ThreadLocal可以跨方法获取。例如框架作者可以事先在threadlocal中设置好一些经常用到的变量,使用者就可以直接在自己的业务方法中获取到。列举几个在spring中的应用场景:
ThreadLocal本身不会存储线程设置的值,而是通过往Thread对象的成员变量threadLocals
(一个ThreadLocalMap类)增加键值对来实现的。不知道作者为什么是这样实现的。如果是我的话,就直接在ThreadLocal中加一个线程安全map,然后将线程对象作为map的key,线程设置的值为value。不过这样子做的话就会把重心从线程对象往ThreadLocal对象转移了。
与HashMap相类似,ThreadLocalMap也是一个entry数组,存储键值对,其中key是ThreadLocal对象,value是一个object对象。不过它没有链表和红黑树,几个核心的数据结构见如下源码。需要注意的是:entry中对key的引用是弱引用,为什么需要用到这个引用呢?这个会在下文”弱引用“小节分析。
每个ThreadLocal对象会自动生成一个threadLocalHashCode,用于决定它在ThreadLocalMap中的位置。当hash冲突时采取线性查询(即下标递增)的方式找寻空位。因此ThreadLocalMap不适合存储大量的键值对。和HashMap一样,ThreadLocalMap也有一个负载因子为2/3,当map容量超过table.length*2/3时会触发扩容。
public class ThreadLocal<T> {
// 和HashMap一样hash值用于决定元素的位置
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
// hash增长策略
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//...
static class ThreadLocalMap {
// key是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
// 核心数据结构
private Entry[] table;
// 存储kv对的数量
private int size = 0;
private int threshold; // Default to 0
// 容量超过阈值时会触发扩容
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
//...
}
}
当使用ThreadLocal.set的时候,会检查当前线程是否已经有一个map了,如果有的话就把kv放到map中,如果没有就创建一个map并置入kv。
// java.lang.ThreadLocal#set
public void set(T value) {
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap
ThreadLocalMap map = t.threadLocals;
if (map != null)
// 当前线程的ThreadLocalMap不为空时,将kv对插入该map,该set方法类似于put,详细步骤见下面一个方法
map.set(this, value);
else
// 当前线程的ThreadLocalMap为空时(第一次设置threadLocal变量),构造该线程的ThreadLocalMap并初始化
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// java.lang.ThreadLocal.ThreadLocalMap#set
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// kv对位置的算法
int i = key.threadLocalHashCode & (len-1);
/*
递增i,线性查询空位子。有两种情况进入for循环:
1. 重新设置ThreadLocal变量
2. 哈希冲突
*/
for (Entry e = tab[i];
e != null;
// i递增,直到找到空位置
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 重新设置ThreadLocal变量的场景
if (k == key) {
e.value = value;
return;
}
// ThreadLocal已经被置为null
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 找到空位子,插入该键值对
tab[i] = new Entry(key, value);
int sz = ++size;
// 超出阈值时扩容两倍,并重新计算entry位置,阈值为table.length*2/3其中2/3可以看做是负载因子
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
获取ThreadLocal变量就是从当前线程的threadLocalMap中找到该threadLocal对象对应的value
// java.lang.ThreadLocal#get
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = t.threadLocals;
if (map != null) {
// 通过key获取到entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
// 构建ThreadLocalMap,并用null初始化
return setInitialValue();
}
// java.lang.ThreadLocal#setInitialValue
private T setInitialValue() {
T value = null;
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 构建ThreadLocalMap,并用null初始化
createMap(t, value);
return value;
}
类似的,删除threadLocal变量的值的流程就是从当前线程的ThreadLocalMap中获取该threadLocal对应的entry然后将key和value置为null
// java.lang.ThreadLocal#remove
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 取出当前线程的ThreadLocalMap,然后移除键值对
m.remove(this);
}
// java.lang.ThreadLocal.ThreadLocalMap#remove
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)]) {
// 考虑到hash冲突的情况需要判断key是否是同一个(比较地址)
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
// java.lang.ThreadLocal.ThreadLocalMap#expungeStaleEntry
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// key和value置为null等待gc,size减一
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 重新计算entry的位置
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;
}
强引用是我们日常使用最普遍的引用方式例如Object o = new Object()
,只要强引用存在,其指向的对象无论何时都不会被jvm回收,换句话说只要引用不指向null该对象就会一直存活。
/*
输出:
obj cleaned...
*/
public static void testStrongRef() throws InterruptedException {
MyObj myObj = new MyObj("obj", null);
System.gc();
myObj = null;
System.gc();
TimeUnit.MILLISECONDS.sleep(100);
}
static class MyObj {
private String name;
public Reference<byte[]> padding;
MyObj(String name, Reference<byte[]> padding) {
this.name = name;
this.padding = padding;
}
@Override
public String toString() {
return name;
}
@Override
protected void finalize() throws Throwable {
System.out.println(this.name + " cleaned...");
}
}
软引用指向的对象在jvm堆内存不足时会被释放。比较适用于有用但不是必须的对象,可以用作缓存。
/*
说明:
MyObj类就是上面那个片段代码中的类
需要增加虚拟机参数-Xmx10M
下面不会输出'soft3 cleaned...',因为软引用对象本身不会被回收,回收的是它指向的对象,即字节数组
输出:
null
[B@2f4d3709
null
null
[B@4e50df2e
null
null
[B@1d81eb93
*/
public static void testSoftRef() throws InterruptedException {
MyObj myObj1 = new MyObj("soft1", new SoftReference<>(new byte[5 * 1024 * 1024]));
MyObj myObj2 = new MyObj("soft2", new SoftReference<>(new byte[5 * 1024 * 1024]));
System.out.println(myObj1.padding.get());
System.out.println(myObj2.padding.get());
SoftReference<byte[]> bytes1 = new SoftReference<>(new byte[5 * 1024 * 1024]);
SoftReference<byte[]> bytes2 = new SoftReference<>(new byte[5 * 1024 * 1024]);
System.out.println(myObj2.padding.get());
System.out.println(bytes1.get());
System.out.println(bytes2.get());
SoftObj softObj1 = new SoftObj("soft3", new byte[5 * 1024 * 1024]);
SoftObj softObj2 = new SoftObj("soft4", new byte[5 * 1024 * 1024]);
System.out.println(bytes2.get());
System.out.println(softObj1.get());
System.out.println(softObj2.get());
TimeUnit.MILLISECONDS.sleep(100);
}
static class SoftObj extends SoftReference<byte[]> {
private String name;
SoftObj(String name, byte[] bytes) {
super(bytes);
this.name = name;
}
@Override
protected void finalize() throws Throwable {
System.out.println(this.name + " cleaned...");
}
}
弱引用指向的对象只要在gc之后就会被释放回收,即使这个gc不是由内存不足产生的,或者gc也没有清除弱引用指向的对象。
/*
输出:
[B@2f4d3709
null
*/
public static void testWeakRef(){
WeakReference<byte[]> weak = new WeakReference<>(new byte[1]);
System.out.println(weak.get());
System.gc();
System.out.println(weak.get());
}
像上文ThreadLocalMap中对key的引用就是弱引用,目的是为了防止内存泄露。因为当threadLocal的引用被指向null后,若ThreadLocalMap的key还是强引用的话则这对entry就永远得不到释放了,可以思考下面一段代码。
tl.set(”test“)会在当前线程内部的threadLocalMap设置一对kv(key为tl对象,v为test字符串),当tl指向null后,若threadLocalMap对key(也就是tl对象)的引用是强引用则这一对kv永远也释放不了了,即使该tl不再使用了。
private static ThreadLocal<String> tl = new ThreadLocal<>();
public void testThreadLocal() {
tl.set("test");
System.out.println(tl.get());
tl = null;
}
虚引用是四类引用中最为特殊的一种,它创建的时候除了要传递包裹的对象外还要增加一个引用队列参数。并且你无法获取虚引用指向的对象。那么虚引用究竟有什么作用呢?它是jvm管理直接内存(direct memory,是堆外内存)的一种方式,直接内存的分配和回收都是有Unsafe类去操作,java在申请一块直接内存之后,会在堆内存分配一个对象保存这个堆外内存的引用,这个对象被垃圾收集器管理,一旦这个对象被回收,相应的用户线程会收到通知并对直接内存进行清理工作。工作方式类似于下面这段代码。
/*
说明:
phantomReference.get()永远返回null
当虚引用指向的对象被gc时,虚引用对象就会进入到引用队列中。
*/
public static void testPhantomRef() throws InterruptedException {
Object obj = new Object(); // 可以看做是堆外内存
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(obj, referenceQueue);
System.out.println(phantomReference.get());
System.out.println(referenceQueue.poll());
Thread thread = new Thread(() -> {
Reference reference;
for (reference = referenceQueue.poll(); reference == null; ) {
reference = referenceQueue.poll();
}
System.out.println(reference); // 感知到堆外内存被gc了,unsafe清理堆外内存
});
thread.start();
obj = null;
System.gc(); //堆外内存被gc
thread.join();
}
对象被回收时间 | 用途 | |
---|---|---|
强引用 | 永远不会 | 对象常用的引用方式 |
软引用 | 内存不足时 | 缓存可以缓存但非必须缓存的对象 |
弱引用 | gc发生后 | ThreadLocal |
虚引用 | 所指向的对象被gc | jvm管理直接内存 |
Java:强引用,软引用,弱引用和虚引用
强软弱虚引用,只有体会过了,才能记住