ThreadLocal是Java类库提供的在多线程环境下保证对共享资源安全访问的类
通过对源码分析发现,ThreadLocalMap是每一个线程Thread类的成员变量,里面有一个键值对数据Entry[] table,可以认为是一个map。
一个Thread对象持有一个ThreadLocalMap成员变量,而ThreadLocalMap依托Entry静态类来存储数据,Entry结构中key表示ThreadLocal,value表示要存储的数据
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
initialValue
该方法会返回当前线程对应的初始值。
此方法默认实现返回null。
protected T initialValue() {
return null;
}
可以使用匿名内部类的方式重写initialValue(),以便在后续使用中可以初始化副本对象。
这是一个延迟加载的方法,只有在调动get方法的时候才会出触发。
当线程第一次使用get访问变量时,将调用此方法。若线程先调用了set方法,则不会再调用initialValue方法。
请看源代码,便一目了然为什么这么说了
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;
}
当第一次调用get()时,map为null,代码流转到setInitialValue方法,在setInitialValue方法中首先会去读取initialValue()初始化的值。如果有重写initialValue方法,则会走到我们重写的方法里
每个线程最多调用一次这个方法。但是如果已经调用了remove()后,再调用get()依然可以调用此方法。
public class ThreadLocalDemo {
// private ThreadLocal threadLocal = new ThreadLocal() {
// @Override
// protected Integer initialValue() {
// return 0;
// }
// };
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> {
System.out.println("我是InitialValue方法");
return 0;
});
public static void main(String[] args) {
// threadLocal.set(1);
System.out.println(threadLocal.get());
System.out.println(threadLocal.get());
System.out.println("即将执行remove方法");
threadLocal.remove();
System.out.println(threadLocal.get());
}
}
输出结果:
我是InitialValue方法
0
0
即将执行remove方法
我是InitialValue方法
0
set(T t) 为线程设置一个新值
T get() 得到这个线程对应的value
void remove() 删除对应这个线程的值
1、在ThreadLocal使用之前,一定要使用initialValue
初始化或set(T t)
赋初值,否则可能会报空指针异常
public class ThreadLocalDemo {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
private static int getValue(){
return threadLocal.get();
}
public static void main(String[] args) {
System.out.println(ThreadLocalDemo.getValue());
}
}
结论:ThreadLocal#initialValue默认实现返回null,而Integer->int需要拆箱,诱发空指针异常。若getValue()返回Integer,在上面的程序不会报错,但在使用这个数据时依然可能报错。
2、不要重复造轮子,优先使用框架提供出来的工具类。
就以SimpleDateFormat为例,看看ThreadLocal是怎么帮助其实现线程安全的?
演示多线程下使用SimpleDateFormat格式化时间
public class ThreadNotSafeDemo{
private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static void main(String[] args) {
BasicThreadFactory threadFactory = new BasicThreadFactory.Builder().namingPattern("wojiushiwo-pool-%d").build();
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 15, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(20),
threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 1; i <= 50; i++) {
long num = i;
executor.submit(() -> {
String format = dateFormat.format(System.currentTimeMillis()+num*1000);
System.out.println(Thread.currentThread().getName() + "当前时间:" + format);
});
}
executor.shutdown();
}
}
上面的代码演示了使用线程池执行50个解析时间的任务。由于每个任务中的时间都是System.currentTimeMillis()+num*1000
不会重复,所以解析出来的时间也应该不重复才对。
输出结果:
wojiushiwo-pool-1当前时间:2020-07-04 02:08:19
wojiushiwo-pool-2当前时间:2020-07-04 02:08:20
wojiushiwo-pool-3当前时间:2020-07-04 02:08:21
wojiushiwo-pool-4当前时间:2020-07-04 02:08:22
wojiushiwo-pool-5当前时间:2020-07-04 02:08:23
wojiushiwo-pool-6当前时间:2020-07-04 02:08:24
wojiushiwo-pool-7当前时间:2020-07-04 02:08:25
wojiushiwo-pool-8当前时间:2020-07-04 02:08:26
wojiushiwo-pool-9当前时间:2020-07-04 02:08:27
wojiushiwo-pool-10当前时间:2020-07-04 02:08:28
wojiushiwo-pool-1当前时间:2020-07-04 02:08:29
wojiushiwo-pool-1当前时间:2020-07-04 02:08:30
wojiushiwo-pool-3当前时间:2020-07-04 02:08:31
wojiushiwo-pool-3当前时间:2020-07-04 02:08:32
wojiushiwo-pool-5当前时间:2020-07-04 02:08:33
wojiushiwo-pool-6当前时间:2020-07-04 02:08:35
wojiushiwo-pool-7当前时间:2020-07-04 02:08:35
wojiushiwo-pool-8当前时间:2020-07-04 02:08:36
wojiushiwo-pool-9当前时间:2020-07-04 02:08:37
wojiushiwo-pool-10当前时间:2020-07-04 02:08:39
wojiushiwo-pool-1当前时间:2020-07-04 02:08:39
wojiushiwo-pool-2当前时间:2020-07-04 02:08:40
wojiushiwo-pool-4当前时间:2020-07-04 02:08:41
wojiushiwo-pool-4当前时间:2020-07-04 02:08:42
wojiushiwo-pool-5当前时间:2020-07-04 02:08:43
wojiushiwo-pool-6当前时间:2020-07-04 02:08:44
wojiushiwo-pool-7当前时间:2020-07-04 02:08:45
wojiushiwo-pool-8当前时间:2020-07-04 02:08:46
wojiushiwo-pool-9当前时间:2020-07-04 02:08:47
wojiushiwo-pool-9当前时间:2020-07-04 02:08:48
wojiushiwo-pool-1当前时间:2020-07-04 02:08:50
wojiushiwo-pool-2当前时间:2020-07-04 02:08:50
wojiushiwo-pool-1当前时间:2020-07-04 02:08:52
wojiushiwo-pool-3当前时间:2020-07-04 02:08:52
wojiushiwo-pool-1当前时间:2020-07-04 02:08:53
wojiushiwo-pool-6当前时间:2020-07-04 02:08:55
wojiushiwo-pool-1当前时间:2020-07-04 02:08:55
wojiushiwo-pool-8当前时间:2020-07-04 02:08:56
wojiushiwo-pool-1当前时间:2020-07-04 02:08:57
wojiushiwo-pool-8当前时间:2020-07-04 02:08:58
wojiushiwo-pool-2当前时间:2020-07-04 02:08:59
wojiushiwo-pool-4当前时间:2020-07-04 02:09:00
wojiushiwo-pool-2当前时间:2020-07-04 02:09:01
wojiushiwo-pool-3当前时间:2020-07-04 02:09:02
wojiushiwo-pool-7当前时间:2020-07-04 02:09:03
wojiushiwo-pool-6当前时间:2020-07-04 02:09:04
wojiushiwo-pool-10当前时间:2020-07-04 02:09:05
wojiushiwo-pool-9当前时间:2020-07-04 02:09:07
wojiushiwo-pool-1当前时间:2020-07-04 02:09:07
wojiushiwo-pool-8当前时间:2020-07-04 02:09:08
发现结果中时间2020-07-04 02:08:50有重复现象,说明SimpleDateFormat在多线程环境下不是线程安全的。
令SimpleDateFormat线程安全的方式有多种,这里主要讨论ThreadLocal
下面以ThreadLocal来演示实现SimpleDateFormat线程安全的输出时间
public class ThreadSafeDemo {
private static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
public static void main(String[] args) {
BasicThreadFactory threadFactory = new BasicThreadFactory.Builder().namingPattern("wojiushiwo-pool-%d").build();
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 15, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(20),
threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 1; i <= 50; i++) {
long num = i;
executor.submit(() -> {
String format = threadLocal.get().format(System.currentTimeMillis()+num*1000);
System.out.println(Thread.currentThread().getName() + "当前时间:" + format);
});
}
executor.shutdown();
}
}
输出结果:
wojiushiwo-pool-15当前时间:2020-07-04 02:20:23
wojiushiwo-pool-15当前时间:2020-07-04 02:19:59
wojiushiwo-pool-15当前时间:2020-07-04 02:20:00
wojiushiwo-pool-15当前时间:2020-07-04 02:20:01
wojiushiwo-pool-15当前时间:2020-07-04 02:20:02
wojiushiwo-pool-15当前时间:2020-07-04 02:20:03
wojiushiwo-pool-15当前时间:2020-07-04 02:20:04
wojiushiwo-pool-15当前时间:2020-07-04 02:20:05
wojiushiwo-pool-15当前时间:2020-07-04 02:20:06
wojiushiwo-pool-15当前时间:2020-07-04 02:20:07
wojiushiwo-pool-15当前时间:2020-07-04 02:20:08
wojiushiwo-pool-15当前时间:2020-07-04 02:20:09
wojiushiwo-pool-15当前时间:2020-07-04 02:20:10
wojiushiwo-pool-14当前时间:2020-07-04 02:20:22
wojiushiwo-pool-1当前时间:2020-07-04 02:19:49
wojiushiwo-pool-15当前时间:2020-07-04 02:20:11
wojiushiwo-pool-14当前时间:2020-07-04 02:20:12
wojiushiwo-pool-1当前时间:2020-07-04 02:20:13
wojiushiwo-pool-15当前时间:2020-07-04 02:20:14
wojiushiwo-pool-14当前时间:2020-07-04 02:20:15
wojiushiwo-pool-1当前时间:2020-07-04 02:20:16
wojiushiwo-pool-15当前时间:2020-07-04 02:20:17
wojiushiwo-pool-14当前时间:2020-07-04 02:20:18
main当前时间:2020-07-04 02:20:24
wojiushiwo-pool-5当前时间:2020-07-04 02:19:53
wojiushiwo-pool-5当前时间:2020-07-04 02:20:25
wojiushiwo-pool-4当前时间:2020-07-04 02:19:52
wojiushiwo-pool-15当前时间:2020-07-04 02:20:26
wojiushiwo-pool-14当前时间:2020-07-04 02:20:27
wojiushiwo-pool-5当前时间:2020-07-04 02:20:28
wojiushiwo-pool-4当前时间:2020-07-04 02:20:29
wojiushiwo-pool-1当前时间:2020-07-04 02:20:30
wojiushiwo-pool-15当前时间:2020-07-04 02:20:31
wojiushiwo-pool-14当前时间:2020-07-04 02:20:32
wojiushiwo-pool-5当前时间:2020-07-04 02:20:33
wojiushiwo-pool-4当前时间:2020-07-04 02:20:34
wojiushiwo-pool-1当前时间:2020-07-04 02:20:35
wojiushiwo-pool-7当前时间:2020-07-04 02:19:55
wojiushiwo-pool-7当前时间:2020-07-04 02:20:36
wojiushiwo-pool-14当前时间:2020-07-04 02:20:37
wojiushiwo-pool-5当前时间:2020-07-04 02:20:38
wojiushiwo-pool-10当前时间:2020-07-04 02:19:58
wojiushiwo-pool-11当前时间:2020-07-04 02:20:19
wojiushiwo-pool-6当前时间:2020-07-04 02:19:54
wojiushiwo-pool-3当前时间:2020-07-04 02:19:51
wojiushiwo-pool-12当前时间:2020-07-04 02:20:20
wojiushiwo-pool-8当前时间:2020-07-04 02:19:56
wojiushiwo-pool-13当前时间:2020-07-04 02:20:21
wojiushiwo-pool-2当前时间:2020-07-04 02:19:50
wojiushiwo-pool-9当前时间:2020-07-04 02:19:57
前面有讨论过ThreadLocalMap中静态Entry类,这里详细说下
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
首先Entry继承自弱引用,并且其构造函数中k是使用WeakReference赋值的。所以可以断定Entry中key是弱引用。而value毫无疑问是强引用。
由JVM知识可以得知,弱引用在垃圾回收时会被主动回收,而强引用只有当GC触发时才会被回收。
正常情况下,当线程终止时,保存在ThreadLocal里的value会被垃圾回收。但是,如果线程不终止(如线程需要保持很久),那么key对应的value就不会被回收,就会导致内存无法被回收,最终可能出现OOM。
幸好,ThreadLocal中set、remove、rehash方法中会扫描key为null的Entry,并将对应的value也设置为null,这样value就可以被回收了。
若像上面说的,ThreadLocal不再使用,但线程未终止而且会显式调用set、remove、rehash等方法,那么内存中的调用链就一直存在,极易引起内存泄露。
在使用完ThreadLocal之后手动调用remove方法,删除对应的Entry对象。