ThreadLocal是为了解决多线程并发访问共享变量时造成数据异常的问题,与加锁的思想方式不同,ThreadLocal是通过为每个线程提供一个变量的副本,以此保证并发访问的安全。
先看一下在没有使用ThreadLocal的情况下对于共享变量的访问结果:
/**
* 启动两个线程,各执行100次对共享变量count加1,得到的结果可能并不是200,而是一个无法确定的数
*/
public class NoUseThreadLocal {
static int count = 0;
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
System.out.println("count: " + count);
}).start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
System.out.println("count: " + count);
}).start();
}
}
我们都知道这样的结果是无法确定的。
count: 199
count: 199
使用ThreadLocal的方式。
public class UseThreadLocal {
private static ThreadLocal<Integer> threadLocal
= ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadLocal.set(threadLocal.get() + 1);
}
System.out.println("count: " + threadLocal.get());
}).start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadLocal.set(threadLocal.get() + 1);
}
System.out.println("count: " + threadLocal.get());
}).start();
}
}
无论执行多少次得到的结果都是两个100。
count: 100
count: 100
通过这个例子就说明了threadlocal可以保证多个线程对共享变量的访问安全,并且由结果也可以看出,每个线程是自己玩自己的,也就是线程内部使用变量副本的方式,所以得到的结果是100而不是200。
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//从当前线程中获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//放入Entry数组中
map.set(this, value);
else
createMap(t, value);
}
//ThreadLocal中的一个静态内部类,线程对象持有这样一个引用
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
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);
}
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();
}
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);
}
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Thread对象中有一个ThreadLocalMap的属性
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal存储结构如下图:
线程对象中有一个ThreadLocalMap属性,ThreadLocalMap中又有一个Entry数组对象,当调用get或者set方法时都是先找到当前线程的ThreadLocalMap属性,然后再通过ThreadLocalMap获取Entry数组对象,数据就是由Entry负责存储的,Entry中的key实际上是通过弱引用的方式引用了ThreadLocal的实例。
根据我们前面对ThreadLocal的分析,我们可以知道每个Thread 维护一个 ThreadLocalMap,ThreadLocalMap内部又维护一个Entry,Entry的 key 是 ThreadLocal实例本身,value 是真正需要存储的数据,而key与ThreadLocal之间又是一种弱引用关系。
先看一张线程对象和ThreadLocal对象的引用关系图。
当把threadlocal变量置为null以后,没有任何强引用指向threadlocal实例,只有一条与key关联的弱引用路径,所以threadlocal将在下一次gc时被回收。这样一来,ThreadLocalMap中就会出现key为null的Entry,而这些key又是无法被访问到的,所以如果当前线程一直存活的话,就会存在这样一条强引用链:CurrentThreadRef -> CurrentThread -> ThreaLocalMap -> Entry -> value,而这块value永远不会被访问到了,数据也一直存在于内存中,内存泄露的问题就产生了。
然而大多数我们使用线程的场景都是通过线程池来管理,而线程池刚好是不会真正销毁线程的。
为什么要使用弱引用?
首先明确一点,内存泄露的问题和弱引用没有任何关系,使用弱引用的原因正是为了方便对象的回收和避免内存泄露,假设threadlocal的实例与key之间是强引用的关系,那么即使threadlocal自身的引用断了以后也不能对threadlocal进行回收,因为threadlocal还和key存在着强引用的关系。
所以如果不使用弱引用,那么当我们threadlocal使用完之后,必须手动清除threadlocal与key之间的引用关系,否则造成的是整个Entry对象级别的内存泄露。
如何避免内存泄露?
现在我们已经知道造成内存泄露的根本原因是因为当前线程引用着Entry对象,导致Entry对象不能被回收,从而导致value中的数据占用着内存空间。
其实要解决此问题,只需要我们使用完之后再手动调用remove方法即可。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocal内存溢出代码演示
public class ThreadLocalDemo {
final static ThreadPoolExecutor threadPoolExecutor
= new ThreadPoolExecutor(10, 10,
1,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>());
static class MyData{
//构建对象就会占用1M的内存
private byte[] a = new byte[1024 * 1024 * 1];
}
ThreadLocal<MyData> threadLocal;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; ++i) {
threadPoolExecutor.execute(() -> {
ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
threadLocalDemo.threadLocal = new ThreadLocal<>();
threadLocalDemo.threadLocal.set(new MyData());
//threadLocalDemo.threadLocal.remove();
});
Thread.sleep(100);
}
}
}
把最后的remove方法加上之后,可以看出堆内存的使用量明显降低了。