在涉及到多线程须要共享变量的时候,通常有两种方法:其一就是使用互斥锁,使得在每一个时刻只能有一个线程访问该变量,好处就是便于编码(直接使用 synchronized
关键字进行同步访问),缺点在于这增长了线程间的竞争,下降了效率;其二就是使用本文要讲的 ThreadLocal
。若是说 synchronized
是以“时间换空间”,那么 ThreadLocal
就是 “以空间换时间” —— 由于 ThreadLocal
的原理就是为每一个线程都提供一个这样的变量,使得这些变量是线程级别的变量,不一样线程之间互不影响,从而达到能够并发访问而不出现并发问题的目的。数据库
首先咱们来看一个客观的事实:当一个可变对象被多个线程访问时,可能会获得非预期的结果 —— 因此先让咱们来看一个例子。在讲到并发访问的问题的时候,SimpleDateFormat
老是会被拿来当成一个绝好的例子(从这点看感谢 JDK 提供了这么一个有设计缺陷的类方便咱们当成反面教材 :) )。由于 SimpleDateFormat
的 format
和 parse
方法共享从父类 DateFormat
继承而来的 Calendar
对象:数组
而且在 format
和 parse
方法中都会改变这个 Calendar
对象:安全
format
方法片断:parse
方法片断:就拿 format
方法来讲,考虑以下的并发情景:多线程
calendar.setTime(date1)
,而后 线程A 被中断;calendar.setTime(date2)
,而后 线程B 被中断;calendar
已经和以前的不一致了,因此便致使了并发问题。因此由于这个共享的 calendar
对象,SimpleDateFormat
并非一个线程安全的类,咱们写一段代码来测试下。并发
(1)定义 DateFormatWrapper
类,来包装对 SimpleDateFormat
的调用:app
public class DateFormatWrapper { private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static String format(Date date) { return SDF.format(date); } public static Date parse(String str) throws ParseException { return SDF.parse(str); } }
(2)而后写一个 DateFormatTest
,开启多个线程来使用 DateFormatWrapper
:ide
public class DateFormatTest { public static void main(String[] args) throws Exception { ExecutorService threadPool = Executors.newCachedThreadPool(); // 建立无大小限制的线程池 List<Future<?>> futures = new ArrayList<>(); for (int i = 0; i < 9; i++) { DateFormatTask task = new DateFormatTask(); Future<?> future = threadPool.submit(task); // 将任务提交到线程池 futures.add(future); } for (Future<?> future : futures) { try { future.get(); } catch (ExecutionException ex) { // 运行时若是出现异常则进入 catch 块 System.err.println("执行时出现异常:" + ex.getMessage()); } } threadPool.shutdown(); } static class DateFormatTask implements Callable<Void> { @Override public Void call() throws Exception { String str = DateFormatWrapper.format( DateFormatWrapper.parse("2017-07-17 16:54:54")); System.out.printf("Thread(%s) -> %s\n", Thread.currentThread().getName(), str); return null; } } }
某次运行的结果:测试
能够发现,SimpleDateFormat
在多线程共享的状况下,不只可能会出现结果错误的状况,还可能会因为并发访问致使运行异常。固然,咱们确定有解决的办法:this
DateFormatWrapper
的 format
和 parse
方法加上 synchronized
关键字,坏处就是前面提到的这会加大线程间的竞争和切换而下降效率;SimpleDateFormat
对象,而是每次使用 format
和 parse
方法都新建一个 SimpleDateFormat
对象,坏处也很明显,每次调用 format
或者 parse
方法都要新建一个 SimpleDateFormat
,这会加大 GC 的负担;ThreadLocal
。ThreadLocal<SimpleDateFormat>
能够为每一个线程提供一个独立的 SimpleDateFormat
对象,建立的 SimpleDateFormat
对象个数最多和线程个数相同,相比于 (1),使用ThreadLocal
不存在线程间的竞争;相比于 (2),使用ThreadLocal
建立的 SimpleDateFormat
对象个数也更加合理(不会超过线程的数量)。咱们使用 ThreadLocal
来对 DateFormatWrapper
进行修改,使得每一个线程使用单独的 SimpleDateFormat
:编码
public class DateFormatWrapper { private static final ThreadLocal<SimpleDateFormat> SDF = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static String format(Date date) { return SDF.get().format(date); } public static Date parse(String str) throws ParseException { return SDF.get().parse(str); } }
若是使用 Java8,则初始化 ThreadLocal
对象的代码能够改成:
private static final ThreadLocal<SimpleDateFormat> SDF = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
而后再运行 DateFormatTest
,便始终是预期的结果:
咱们已经看到了 ThreadLocal
的功能,那 ThreadLocal
是如何实现为每一个线程提供一份共享变量的拷贝呢?
在使用 ThreadLocal
时,当前线程访问 ThreadLocal
中包含的变量是经过 get()
方法,因此首先来看这个方法的实现:
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(); }
经过代码能够猜想:
ThreadLocal
的内部),JDK 实现了一个相似于 HashMap
的类,叫 ThreadLocalMap
,该 “Map” 的键类型为 ThreadLocal<T>
,值类型为 T
;ThreadLocalMap
对象,而且能够经过 getMap(Thread t)
方法来得到 线程t 关联的 ThreadLocalMap
对象;ThreadLocalMap
类有个以 ThreadLocal
对象为参数的 getEntry(ThreadLocal)
的方法,用来得到当前 ThreadLocal
对象关联的 Entry
对象。一个 Entry
对象就是一个键值对,键(key)是 ThreadLocal
对象,值(value)是该 ThreadLocal
对象包含的变量(即 T)。查看 getMap(Thread)
方法:
直接返回的就是 t.threadLocals
,原来在 Thread
类中有一个就叫 threadLocals 的 ThreadLocalMap
的变量:
因此每一个 Thread
都会拥有一个 ThreadLocalMap
变量,来存放属于该 Thread
的全部 ThreadLocal
变量。这样来看的话,ThreadLocal
就至关于一个调度器,每次调用 get
方法的时候,都会先找到当前线程的 ThreadLocalMap
,而后再在这个 ThreadLocalMap
中找到对应的线程本地变量。
而后咱们来看看当 map 为 null
(即第一次调用 get()
)时调用的 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; }
该方法首先会调用 initialValue()
方法来得到该 ThreadLocal
对象中须要包含的变量 —— 因此这就是为何使用 ThreadLocal
是须要继承 ThreadLocal
时并覆写 initialValue()
方法,由于这样才能让 setInitialValue()
调用 initialValue()
从而获得 ThreadLocal
包含的初始变量;而后就是当 map 不为 null
的时候,将该变量(value)与当前ThreadLocal
对象(this)在 map 中进行关联;若是 map 为 null
,则调用 createMap
方法:
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
createMap
会调用 ThreadLocalMap
的构造方法来建立一个 ThreadLocalMap
对象:
能够看到该方法经过一个 ThreadLocal
对象(firstKey)和该 ThreadLocal
包含的对象(firstValue)构造了一个 ThreadLocalMap
对象,使得该 map 在构造完毕时候就包含了这样一个键值对(firstKey -> firstValue)。
为啥须要使用 Map 呢?由于一个线程可能有多个 ThreadLocal
对象,多是包含 SimpleDateFormat
,也多是包含一个数据库链接 Connection
,因此不一样的变量须要经过对应的 ThreadLocal
对象来快速查找 —— 那么 Map 固然是最好的方式。
ThreadLocal
还提供了修改和删除当前包含对象的方法,修改的方法为 set
,删除的方法为 remove
:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
很好理解,若是当前 ThredLocal
尚未包含值,那么就调用 createMap
来初始化当前线程的 ThreadLocalMap
对象,不然直接在 map 中修改当前 ThreadLocal
(this)包含的值。
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
remove
方法就是得到当前线程的 ThreadLocalMap
对象,而后调用这个 map 的 remove(ThreadLocal)
方法。查看 ThreadLocalMap
的 remove(ThreadLocal)
方法的实现:
逻辑就是先找到参数(ThreadLocal
对象)对应的 Entry
,而后调用 Entry
的 clear()
方法,再调用 expungeStaleEntry(i)
,i 为该 Entry
在 map 的 Entry
数组中的索引。
(1)首先来看看 e.clear()
作了什么。
查看 ThreadLocalMap
的源代码,咱们能够发现这个 “Map” 的 Entry
的实现以下:
能够看到,该 Entry
类继承自 WeakReference<ThreadLocal<?>>
,因此 Entry
是一个 WeakReference
(弱引用),并且该 WeakReference
包含的是一个 ThreadLocal
对象 —— 于是每一个 Entry
是一个弱引用的 ThreadLocal
对象(又由于 Entry
包括了一个 value 变量,因此该 Entry
构成了一个 ThreadLocal -> Object
的键值对),而 Entry
的 clear()
方法,是继承自 WeakReference
,做用就是将 WeakReference
包含的对象的引用设置为 null
:
咱们知道对于一个弱引用的对象,一旦该对象再也不被其余对象引用(好比像 clear()
方法那样将对象引用直接设置为 null
),那么在 GC 发生的时候,该对象便会被 GC 回收。因此让 Entry
做为一个 WeakReference
,配合 ThreadLocal
的 remove
方法,能够及时清除某个 Entry
中的 ThreadLocal
(Entry
的 key)。
(2)expungeStaleEntry(i)
的做用
先来看 expungeStaleEntry
的前一半代码:
expungeStaleEntry
这部分代码的做用就是将 i 位置上的 Entry
的 value 设置为 null
,以及将 Entry
的引用设置为 null
。为何要这作呢?由于前面调用 e.clear()
,只是将 Entry
的 key 设置为 null
而且可使其在 GC 是被快速回收,可是 Entry
的 value 在调用 e.clear()
后并不会为 null
—— 因此若是不对 value 也进行清除,那么就可能会致使内存泄漏了。所以expungeStaleEntry
方法的一个做用在于能够把须要清除的 Entry
完全的从 ThreadLocalMap
中清除(key,value,Entry 所有设置为 null
)。可是 expungeStaleEntry
还有另外的功能:看 expungeStaleEntry
的后一半代码:
做用就是扫描位置 staleSlot 以后的 Entry
数组(直到某一个为 null
的位置),清除每一个 key(ThreadLocal
) 为 null
的 Entry
,因此使用 expungeStaleEntry
能够下降内存泄漏的几率。可是若是某些 ThreadLocal
变量不须要使用可是却没有调用到 expungeStaleEntry
方法,那么就会致使这些 ThreadLocal
变量长期的贮存在内存中,引发内存浪费或者泄露 —— 因此,若是肯定某个 ThreadLocal
变量已经不须要使用,须要及时的使用 ThreadLocal
的 remove()
方法(ThreadLocal
的 get
和 set
方法也会调用到 expungeStaleEntry
),将其从内存中清除。