ThreadLocal 的概念,面试的时候容易被问到。它的概念很简单,从类的名字就可以知道,线程本地变量的意思。即该变量运行在线程中时,每个线程都独立拥有它而不和其他线程中的这个值相冲突,其功能就使得这个变量就属于当前线程,和其他线程无关。
ThreadLocal 方法提供泛型的版本, 即该变量中存放的类型可以自己指定。声明该变量的时候,直接通过 new 方法就可以了。
下面的代码演示了如何声明该变量,同时对其初始化。
private ThreadLocal myThreadLocal = new ThreadLocal<String>() {
@Override protected String initialValue() {
return "";
}
};
Java 8 中可以写成:
ThreadLocal<ArrayList<Long>> myThreadLocal = ThreadLocal.withInitial(ArrayList::new);
ThreadLocal 对值的读写很简单,分别是 get 方法和 set 方法。
myThreadLocal.set("Hello");
String threadLocalValue = myThreadLocal.get();
下面演示 ThreadLocal 的作用,先不使用 ThreadLocal。
class DemoThreadLocal {
int num = 0;
public void increase() {
for (int i = 0; i < 5; i++) {
num++;
System.out.println(Thread.currentThread().getName() + " " + num);
}
}
}
public class Main {
public static void main(String args[]) throws Exception {
DemoThreadLocal d = new DemoThreadLocal();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
d.increase();
}
}).start();
}
}
}
上面的代码循环创建 5 个线程,每个线程都使用了 DemoThreadLocal 的同一个实例,因此每次调用 increase,都会将实例的 num 自增 5 次,由于这个变量是共享的,所以,5 个线程调用的自增都会在同一个变量上。
运行结果如下,可以 看到 num 会被加到 25:
Thread-0 1 Thread-0 5 Thread-0 6 Thread-0 7 Thread-0 8 Thread-2 4 Thread-2 9 Thread-2 10 Thread-2 11 Thread-2 12 Thread-4 13 Thread-4 14 Thread-4 15 Thread-4 16 Thread-4 17 Thread-3 3 Thread-3 18 Thread-3 19 Thread-3 20 Thread-3 21 Thread-1 2 Thread-1 22 Thread-1 23 Thread-1 24 Thread-1 25
下面改下代码,演示使用 ThreadLocal 的情况。
class DemoThreadLocal {
ThreadLocal<Integer> num = ThreadLocal.withInitial(() -> 0);
public void increase() {
for (int i = 0; i < 5; i++) {
num.set(num.get() + 1);
System.out.println(Thread.currentThread().getName() + " " + num.get());
}
}
}
public class Main {
public static void main(String args[]) throws Exception {
DemoThreadLocal d = new DemoThreadLocal();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
d.increase();
}
}).start();
}
}
}
上面的代码使用了将 num 变量包入了 ThreadLocal 类,因此每个线程都独立拥有这个 num,每次调用 increase 时,线程各自的 num 自增 5 次,num 变量不是共享的。
运行结果如下:
Thread-0 1 Thread-0 2 Thread-0 3 Thread-0 4 Thread-0 5 Thread-2 1 Thread-2 2 Thread-2 3 Thread-2 4 Thread-2 5 Thread-4 1 Thread-4 2 Thread-4 3 Thread-4 4 Thread-4 5 Thread-1 1 Thread-1 2 Thread-1 3 Thread-1 4 Thread-1 5 Thread-3 1 Thread-3 2 Thread-3 3 Thread-3 4 Thread-3 5
实际上,第一张未使用 ThreadLocal 的情况下,也可以让线程运行时,操作自己独立的 num。方法很简单,就是在线程运行的时候,把 DemoThreadLocal d = new DemoThreadLocal(); 这个变量作为线程自己局部变量。但是坏处就是每次线程运行,都需要实例化 DemoThreadLocal 类。
public class Main {
public static void main(String args[]) throws Exception {
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
<span style="background-color: #ffff00;">DemoThreadLocal d = new DemoThreadLocal();</span>
d.increase();
}
}).start();
}
}
}
如果 DemoThreadLocal 需要设计成单例的对象,无法 new 出来,那么也就无法使用上面这种 new 的方法。这种情况下,想要让每个线程拥有各自的 num,也只能用 ThreadLocal 类。在某些场景下,由于使用 ThreadLocal 可以使得线程拥有各自独立的变量,从而避免使用 synchronized,使得代码简化。大家可以搜一下 ThreadLocal 解决 SimpleDateFormat 非线程安全的问题。
——————————–
ThreadLocal 原理
ThreadLocal 原理并不复杂,网上有很多文章分析的很透彻。
每个 Thread 类中有如下一行代码:
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap 是 ThreadLocal 类中的内部静态类。每个线程都有一个 ThreadLocalMap 类变量,该类存放了键值对,其中键值对的 key 就是 ThreadLocal 的一个引用,value 就是 ThreadLocal 对应的值。ThreadLocal 实例本身并没有存储值,值都是存放在 ThreadLocalMap 中,ThreadLocal 的作用就是作为键值对中的一个 key。
借用网上一个图片,描述了这几个类之间的关系,Thread 包含一个 ThreadLocalMap,ThreadLocalMap 包含了键值对,键值对中把 ThreadLocal 作为 key。
图片来源于网络
简单的说就是每个线程中维护了一个大的键值对的表,用来存储线程本地变量的。
再背一下内存泄漏,可加分
ThreadLocalMap 使用 ThreadLocal 的弱引用作为 key,弱引用 ThreadLocal 被回收后,导致 ThreadLocalMap 中出现 key 为 null 的数据,这些数据的 value 值就会一直存在着引用链,导致无法 GC 回收,造成内存泄漏。所以使用完 ThreadLocal 后,最好调用 remove() 方法,清除数据。
参考:
https://stackoverflow.com/questions/1202444/how-is-javas-threadlocal-implemented-under-the-hood
http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/#ThreadLocal_u4E3A_u4EC0_u4E48_u4F1A_u5185_u5B58_u6CC4_u6F0F