ThreadLocal的概念,顾名思义:本地线程,实现每一个线程都有自己的专属本地变量。
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK中提供的ThreadLocal类正是为了解决这样的问题。ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
从上面简介中可以看出,ThreadLocal可以避免线程安全问题。那么我们就看一个线程不安全的例子:
SimpleDateFormat 不是线程安全的
public class DateUtils {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static Date parse(String dateStr) {
Date date = null;
try {
date = sdf.get().parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(5);
for (int i = 0; i < 20; i++) {
String a= String.valueOf(i);
service.execute(()->{
System.out.println(DateUtils.parse("2019-06-01 16:34:30") + "######" + a);
});
}
service.shutdown();
}
}
执行这段代码,发现报错了。DateFormat 多线程时抛出NumberFormatException
Exception in thread "pool-1-thread-4" Exception in thread "pool-1-thread-2" Exception in thread "pool-1-thread-5" Exception in thread "pool-1-thread-1" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.tuniu.htl.ctrip.common.util.DateUtils.parse(DateUtils.java:67)
at com.tuniu.htl.ctrip.controller.ThreadLocalTest.lambda$main$0(ThreadLocalTest.java:22)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
造成抛异常的原因:
我们可以看到 sdf变量是声明为static的,被static修饰的变量,他是不依赖类的特定实例,一旦变量的值被修改,被所有类的实例锁共享,在类被JVM classloader的时候分配内存,并且是分配在永久区而非堆内存中。
所以在多线程的情况下,同时实例化一个静态非线程安全的变量的时候造成线程安全问题。
源码:
public class Thread implements Runnable {
......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
......
}
从上面Thread类 源代码,可以看出ThreadLocal并不是一个Thread,而是Thread的局部变量。Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,**我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。**默认情况下这两个变量都是null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()、set()方法。
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();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
通过源码可以看出,
(1)我们放到Thread里的内容,实际上都是放到ThreadLocalMap中的,而不是方法ThreadLocal中。
(2)在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。
(3)每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key的键值对。 比如我们在同一个线程中声明了两个 ThreadLocal 对象的话,会使用 Thread内部都是使用仅有那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。ThreadLocal 是 map结构是为了让每个线程可以关联多个 ThreadLocal变量。
这也就解释了 ThreadLocal 声明的变量为什么在每一个线程都有自己的专属本地变量。
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
只需要将sdf放到ThreadLocal中,实现每个线程都要有自己独立的副本
private static final ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static Date parse(String dateStr) {
Date date = null;
try {
date = sdf.get().parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}