ThreadLocal
初识
首先看一个小程序:
public class TestThreadLocal {
volatile static Person p = new Person();
public static void main(String[] args) {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(p.name);
}, "t1").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
p.name = "p2";
}, "t2").start();
}
}
class Person {
String name = "p1";
}
这个程序最终打印的是p2,很好理解,t2线程先于t1线程修改了同一个对象,所以最终打印的是修改过后的值。如果想让这个对象在每个线程里独有一份,那就可以使用ThreadLocal了:
public class ThreadLocal {
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());
}, "t1").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
tl.set(new Person());
}, "t2").start();
}
static class Person {
}
}
上述代码最终输出的是null,说明t2线程设置的值在t1线程中无法读取到。我们尝试读ThreadLocal的源码来理解。
ThreadLocal源码
首先进入set方法看一下:
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
}
public class Thread implements Runnable{
ThreadLocal.ThreadLocalMap threadLocals = null;
}
首先获取当前线程,然后从当前线程中拿到ThreadLocalMap,最后将value值设置到该ThreadLocalMap中。可以看到ThreadLocalMap是由线程维护的,也就是说ThreadLocalMap是在Thread里,而不是在ThreadLocal里。看到这里就可以明白为什么在当一个线程中的ThreadLocal设置的值在另一个线程无法读取到了。
内存泄漏问题:
key:key本身对应ThreadLocal,若局部方法执行完毕,线程指向ThreadLocalMap,key又指向ThreadLocal对象,如果是强引用则不会被回收。所以在设计时,key的引用为弱引用;
value:普通线程使用ThreadLocal,不remove其实也没事儿,线程销毁就没引用指向ThreadLocalMap,自然也可以回收。但如果线程池中的核心线程使用了ThreadLocal,那就必须remove,因为核心线程不会销毁,会导致内存泄漏。
Java中的四种应用:强软弱虚
首先重写一下finalize()方便System.gc()的时候观察观察:
public class Observation {
@Override
protected void finalize() throws Throwable {
System.out.println("finalize");
}
}
强引用
默认的引用就是强引用,只要有一个应用指向某个对象,那么垃圾回收的时候一定不回回收它。举个例子:
public static void main(String[] args) throws IOException {
Observation observation = new Observation();
// observation = null;
System.gc();
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
可以看到并没有输出“finalize”,说明gc时没有回收observation对象。如果将observation = null,才会触发垃圾回收。
软引用
假如要创建一个Observation对象的软引用,则需要这样写:SoftReference<Observation> m = new SoftReference<>(new Observation())。先试一下如下代码:
public static void main(String[] args) {
SoftReference<Observation> m = new SoftReference<>(new Observation());
m = null;
System.gc();
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
可以看到正常打印了“finalize”,那么软引用到底是干啥的呢?再来看看下面这段代码:
public static void main(String[] args) {
SoftReference<byte[]> m = new SoftReference<>(new byte[1024 * 1024 * 10]);
System.out.println(m.get());
System.gc();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(m.get());
byte[] b = new byte[1024 * 1024 * 15];
System.out.println(m.get());
}
运行上述代码需要设置堆内存为20M:-Xmx20M。可以看到前两个输出都有内容,最后一个输出为null。第一次创建字节数组时分配了10MB,gc时不回回收该对象。第二次创建数组时需要分配15MB,由于内存不够了所以会回收第一个数组,所以最后输出了null。即当有一个对象被一个软引用所指向的时候,如果系统内存不够用的时候就会回收它。
弱引用
弱引用只要遭遇到gc就会回收,举个例子:
public static void main(String[] args) {
WeakReference<Observation> m = new WeakReference<>(new Observation());
System.out.println(m.get());
System.gc();
System.out.println(m.get());
}
可以看到gc后第二次打印的是null。
弱引用最典型的应用就是ThreadLocal,回到ThreadLocalMap的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);
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;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看到set方法中实际存放的是new Entry(key, value),而这个Entry继承自WeakReference,且这个弱引用装的是ThreadLocal,也就是说Entry的key是一个弱引用。假如这里是个强引用,那么当最开始指向new ThreadLocal<>()的引用消失时,ThreadLocal里的对象是无法被回收的,这样就会造成内存泄漏。但如果这个key是弱引用就不会有这样的问题了。
但是这里的key被回收后就会变成null,且原本对应的value值无法访问,依旧存在内存泄漏的问题。所以使用ThreadLocal时如果对象不用了,一定要使用remove方法避免内存泄露。
虚引用
虚引用时用于堆外内存管理的,且构造方法有两个,其中第二个是队列。可以看到虚引用是get不到值的。一旦虚引用里的对象被垃圾回收,那么QUEUE会接收到通知,即被回收的对象会被放入该队列。
public class TestPhantomReference {
private static Object o = new Object();
private static final ReferenceQueue<Object> QUEUE = new ReferenceQueue<>();
private static PhantomReference<Object> phantomReference = new PhantomReference<>(o, QUEUE);
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 3; i++) {
try {
System.out.println(phantomReference.get());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
o = null;
System.gc();
}).start();
new Thread(() -> {
while (true) {
Reference<? extends Object> poll = QUEUE.poll();
if (poll != null) {
System.out.println("--- 虚引用对象被jvm回收了 ---- " + poll);
}
}
}).start();
}
}