什么是ThreadLocal
ThreadLocal是一个用于创建线程局部变量的类,它有两个特点,其一对于线程A创建的数据只有线程A能获取和修改;其二只要能获取到ThreadLocal的示例,线程就能获取到其中的值。
使用场景
网上有介绍ThreadLocal与synchronized对比的文章,但是我觉得它们之间并没有可以性。ThreadLocal注重的点在于通过线程独有空间来存储和获取数据,而synchronized注重的是多线程同时对同一变量的获取与修改。
简单的比喻是ThreadLocal好比每个人(线程)有自己的口袋存,每个人只通过自己口袋来存放和取东西。而synchronized就好比所有人(多有线程)都在同一个口袋存放和取东西,但是一次只能一个人操作。
public class App6 {
public static Map<Thread,String> globalMap = new HashMap<>();
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
//存数据
globalMap.put(currentThread,"abc");
//取数据
System.out.printf("[%s]=[%s]\n",currentThread.getName(),globalMap.get(currentThread));
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
//存数据
globalMap.put(currentThread,"123");
//取数据
System.out.printf("[%s]=[%s]\n",currentThread.getName(),globalMap.get(currentThread));
}
}).start();
TimeUnit.SECONDS.sleep(1);
System.out.println(globalMap);
}
}
上面的示例中,每个线程将自己的数据放在Map中,然后获取自己数据。但是上面的示例是线程不安全的,千万不要在自己的代码中使用。如果使用上面的方案,我们需要使用线程安全的Map或者加锁来解决。即使解决了线程安全的问题还存在两个问题,其一就是Map中的值并不是当前线程独有的,其他线程也是可以获取和修改它。其二为了保证线程安全Map需要加锁,在性能上时有损失的。
上面所提到的问题使用ThreadLocal都可以解决。现在修改代码如下:
public class App7 {
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
//存数据
threadLocal.set("abc");
//取数据
System.out.printf("[%s]=[%s]\n",currentThread.getName(),threadLocal.get());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
//存数据
threadLocal.set("123");
//取数据
System.out.printf("[%s]=[%s]\n",currentThread.getName(),threadLocal.get());
}
}).start();
TimeUnit.SECONDS.sleep(1);
System.out.println(threadLocal.get());
}
}
通过使用ThreadLocal,它既能保证多线程访问的安全性,同时也能实现无锁。
实现原理
要想实现ThreadLocal这样的效果我们基本山能想到的就两种方式方式来实现。
ThreadLocal维护Thread与数据的映射关系
该实现方式是在ThreadLocal内部维护一个线程安全的Map,然后以当前线程作为Map的Key,而线程的数据作为Value。
ThreadLocal维护Thread与数据的映射关系.jpg上面的这种方式能实现ThreadLocal的功能,但是问题在于通过这种方式实现就必须要保证ThreadLocal中负责维护线程和数据的Map线程安全,这或多或少都需要增加锁的引入,并不能实现无锁。
Thread维护ThreadLocal与数据的映射关系
通过上面的分析我们知道如果在ThreadLocal中维护Thread与数据的映射我们需要必须要保证内部映射关系的线程安全,如果我们在Thread内存维护一个ThreadLocal与数据之前的映射关系,这种映射关系并没有涉及到线程安全问题,这样也就省去了线程同步的操作,相比上面的实现方式,该方式性能上更好。而JDK内部就是使用该方式来实现的。
Thread维护ThreadLocal与数据的映射关系.jpg基本使用
实例化
示例化方式一般就两种,第一种是直接使用无参构造函数创建,第二种则是在1.8的版本提供的静态方法创建。
public class App8 {
/**
* 实例化方式1 构造函数
*/
public static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
/**
* 实例化方式2 静态方法,
*/
public static ThreadLocal<String> threadLocal2 = ThreadLocal.withInitial(() -> {
System.out.println("如果值为空时则会调用该方法获取初始值");
return "abc";
});
public static void main(String[] args) {
System.out.println(threadLocal1.get());
System.out.println(threadLocal2.get());
threadLocal2.remove();
System.out.println(threadLocal2.get());
}
}
常用方法
常用的方法就三个,set()用来往ThreadLocal中设置值,get()用来往ThreadLocal中获取值,而remove()用来删除ThreadLocal中的值。
public class App9 {
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
//设置值
threadLocal.set("abc");
//获取值
String value = threadLocal.get();
System.out.println(value);
//删除值
threadLocal.remove();
System.out.println(threadLocal.get());
}
}
ThreadLocal中提供的方法比较简单,但是在使用时需要特别注意remove方法。当我们使用完ThreadLocal后我们应该调用remove方法将ThreadLocal中的数据清除,如果不这么做容易产生业务数据异常和内存泄漏(后面将说明为什么会导致内存泄漏)。
public class App10 {
public static ThreadLocal<List<Integer>> threadLocal = ThreadLocal.withInitial(() -> new ArrayList<>());
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
int value = new Random().nextInt(1000);
threadLocal.get().add(value);
System.out.println(threadLocal.get());
}
});
}
executor.shutdown();
}
}
最后的打印结果如下:
[562]
[725]
[562, 434]
[725, 590]
[562, 434, 448]
[725, 590, 377]
[562, 434, 448, 712]
[725, 590, 377, 57]
[562, 434, 448, 712, 715]
[725, 590, 377, 57, 580]
因为我们是在线程池中使用ThreadLocal,而线程池中的线程并不是执行完之后就销毁了。而代码中并没有调用remove方法清除ThreadLocal中的值,这就导致了List中保留了上一次任务的执行结果。
源码分析
我们在实现原理中大致讲过ThreadLocal是如何实现的,我们先看如何往ThreadLocal中存储值的。
public void set(T value) {
//获取当前的线程
Thread t = Thread.currentThread();
//获取Map
ThreadLocalMap map = getMap(t);
if (map != null)
//往Map中放值,Map的key就是当前的ThreadLocal实例
map.set(this, value);
else
//如果Map为空则创建Map,并将值放入Map中
createMap(t, value);
}
上面代码中的getMap(t)方法的实现如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
代码很简单,返回的就是ThreadLocal中的字段threadLocals,它的类型就是ThreadLocalMap。我们调用set方法就是往Map中存放存放值,而这个Map的key就是ThreadLocal,它的值就是我们要存的值。
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取Map
ThreadLocalMap map = getMap(t);
if (map != null) {
//获取Entry中的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果Map为空则获取withInitial中supplier返回的值
return setInitialValue();
}
为何会内存泄漏
通过上面我们知道了ThreadLocal的实现原理,但是为何说会内存泄漏呢?我们先看ThreadLocalMap的结构。
ThreadLocalMap内部结构.png上图就是ThreadLocal的结构了,它内部提供了增删改等主要方法,而ThreadLocal与值被封装成Entry对象存放在table数组中。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
从Entry中的结构可以知道,每一个Entry对象都是一个对Key弱的引用(关于什么是弱引用可以参考该文),当没有强引用指向ThreadLocal变量时,ThreadLocal可以被回收。真是通过这种方式保证了ThreadLocal在没有引用时而Thread还没有被销毁时可以被回收。但是上面的问题带来了另一个问题,当Key被回收之后Entry对象并没有被回收而导致内存泄漏。
如何解决
对于ThreadLocal中存在的内存泄漏问题,最简单的解决方案就是我们在每次在使用完ThreadLocal后手动的调用remove清除数据。但是如果你没有这么做,ThreadLocal对于存在的内存泄漏问题也做了部分优化。在set和get方法中,都会间接或直接的调用cleanSomeSlots、expungeStaleEntry、replaceStaleEntry等方法将key为空的Entry清除掉。虽然对于内存泄漏ThreadLocal内部已经做了优化,但是我们在使用最好还是在ThreadLocal不再使用时手动调用remove方法清除掉其中的数据,从而避免内存泄漏。
网友评论