threadLocal
1. ThreadLocal辨析
ThreadLocal与Synchronize的比较
ThreadLocal和Synchronize都用于解决多线程并发访问,但是ThreadLocal与Synchronize有本质的区别。
-
Synchronized是利用锁的机制,使变量或代码块在某一时刻仅仅能被一个线程访问。
-
ThreadLocal则是为每个线程都提供了一个变量的副本,使得每个线程在某一时刻访问到的并非是同一个对象,这样就隔离了多个线程对数据的共享。
2. ThreadLocal的应用场景
Spring的事务就借助了ThreadLocal类。
Spring会从数据库连接池中获得一个Connection,然后会把Connection放到ThreadLocal中,也就和线程绑定了,事务需要提交或回滚的时候,只需要从ThreadLocal中拿到Connection进行操作
3.ThreadLocal的简单使用
ThreadLocal有四个方法
-
void set(Object value)
设置当前线程的线程局部变量的值
-
public Object get()
该方法返回当前线程所对应的线程局部变量
-
public void remove()
将当前线程句柄变量的值删除,目的是为了减少内存的占用。该方法是JDK5.0新增的方法。需要指出的是,当线程结束后,对应线程的局部变量将自动被垃圾回收,所以显示调用该方法清楚线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
-
protected Object initialValue()
返回该线程句柄变量的初始值,该方法是一个Protected方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第一次调用get()或set(Object)时才执行,并且只执行一次。ThreadLocal中的缺省实现是直接返回一个null。
public final static ThreadLocal<String> resource = new ThreadLocal<String>;
resource代表一个能存放String类型的ThreadLocal对象。此时不论什么一个线程都能够并发访问这个变量,对它进行写入,读取操作,都是线程安全的。
4. 实现解析
public T get() {
Thread t = Thread.currentThread();
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 T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- 调用ThreadLocal的set方法的时候,会获取当前线程,根据当前线程调用getMap(t)方法,第一次是为空的,调用createMap(t,value)方法;创建一个ThreadLocalMap对象,以ThreadLocal为key,传入进来的value为值。再次set的时候会覆盖
- 调用ThreadLocal的get方法的时候。会根据当前的线程找到对应的ThreadLocalMap,获取对应key为ThreadLocal的Map
- ThreadLocalMap里面是一个Entry对象,和Map的Entry类似,是一个K,V键值对。key为ThreadLocal对象,V为传进来的value,但是key为弱引用,当发生垃圾回收的时候,如果内存不足的话,key会被回收
所以,我们调用ThreadLocal的get方法,其实就是拿到当前线程对应的ThreadLocalMap对象,根据ThreadLocal为key找到对应的值
5. ThreadLocal内存泄漏分析
内存泄漏:
不再使用的对象无法被垃圾回收器回收
package com.wxc.thread.threadlocal;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* ThreadLocal造成内存泄漏演示
*/
@Slf4j
public class ThreadLocalOOM {
private static final int TASK_LOOP_SIZE = 500;
final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,
5,
1,
TimeUnit.MINUTES,
new LinkedBlockingDeque<>());
static class LocalVariable {
private byte[] bytes = new byte[1024 * 1024 * 5];
}
static ThreadLocal<LocalVariable> threadLocal = new ThreadLocal<LocalVariable>();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < TASK_LOOP_SIZE; i++) {
poolExecutor.execute(new Runnable() {
@Override
public void run() {
threadLocal.set(new LocalVariable());
// new LocalVariable();
log.info("use local variable");
// threadLocal.remove();
}
});
Thread.sleep(1000);
}
log.info("pool execute over...");
}
}
每个Thread都维护一个ThreadLocalMap,这个映射表的key就是ThreadLocal本身,value是真正要存储的值,也就是说ThreadLocal本身并不存储值。它只是作为一个key来让线程从ThreadLocalMap获取Value。仔细观察ThreadLocalMap,这个Map是使用ThreadLocal的弱引用作为key的,弱引用的对象在GC时会被回收。因此,使用了ThreadLocal之后,引用链如下:
图中的虚线表示弱引用。
这样,当把threadLocal变量置为null以后,没有任何强引用指向ThreadLocal实例,所以ThreadLocal将会被gc回收。这样一来,ThreadLocalMap就会出现key为null的Entry,就没有办法访问这些key为null的value,如果当前线程迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Current Thread Ref -> Current Thread->ThreadLocalMap->Entry->Value,而这块value永远不会被访问到了,所以存在内存泄漏。
只有当前Thrad结束后,强引用断开,Current Thread,Map Value将全部被GC回收。最好的做法就是在不需要使用ThreadLocal变量后,都调用它的remove()方法,清除数据。
所以在代码中,虽然线程池里面的任务执行完毕了,但是线程池里面的5个线程会一直存在直到JVM退出,我们set了线程的localVariable变量后没有调用localVariable.remove()方法,导致线程池里面的5个线程的threadLocals变量里面的new LocalVariable()实例没有被释放。
其实考察ThreadLocal的实现,我们可以看见,无论是get(),set()在某些时候,都会调用expungeStaleEntry()方法来清除Entry中key为null的value,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄漏。只有remove()方法中显示调用了expungeStaleEntry方法
6. ThreadLocal为什么使用弱引用而不是使用强引用?
1.key 使用强引用
引用ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal对象的实例不会被回收,导致Entry内存泄漏
2.Key使用弱引用
引用ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal实例也会被回收。Value在下一次ThreadLocalMap调用set,get,remove()方法的时候都有机会被回收
比较两种情况,我们发现,由于ThreadLocalMap的生命周期和Thread的一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是弱引用可以多一层保障。
因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏。
当使用线程池+ThreadLocal时要小心,因为这种情况下,线程是不断的重复运行的,从而也就造就了value可能造成积累的情况。
7.错误使用ThreadLocal导致线程不安全
package com.wxc.thread.threadlocal;
import com.wxc.thread.tools.SleepTools;
public class ThreadLocalUnSafe implements Runnable{
public static Number number = new Number(0);
public void run() {
//每个线程计数加一
number.setNum(number.getNum()+1);
//将其存储到ThreadLocal中
value.set(number);
SleepTools.ms(2);
//输出num值
System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());
}
public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
};
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new ThreadLocalUnSafe()).start();
}
}
private static class Number {
public Number(int num) {
this.num = num;
}
private int num;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
@Override
public String toString() {
return "Number [num=" + num + "]";
}
}
}
因为Number是static,不是线程私有的,是每个线程所共享的。ThreadLocalMap保存的是一个对象的引用,每次线程修改的时候都是修改同一个引用对应的对象的值,所有线程的值值最终都会变成最后一个线程所修改的值。