本文简单分析下ThreadLocal实现原理,再附上小例子。
ThreadLocal提供线程级别的私有局部变量。这些变量和普通变量不同之处在于,通过get或set方法访问这类变量的每个线程都拥有一份独立初始化的变量副本。
ThreadLocal通常用private static
修饰,可以将状态与该线程建立一对一的关系。
下面这个小例子,当第一次调用ThreadId.get()
时会为每个线程生成一个全局唯一的、以1为步长自增的标识符,往后都会保持不变。
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
}
在上面的例子中,只要线程存活且该ThreadLocal实例可访问,那么每个线程都会拥有一个隐式引用,指向自己拥有的ThreadLocal变量副本值。
总的来说,其实就是多个线程共用一个某个类的静态ThreadLocal Instance,由线程级别的Map中的Entry的弱引用来指向这个共同的ThreadLocal Instance,而这个Entry的Value就是存的用户自定义的Value,所以可以达到线程独有一份的目的。关于Java内的各种引用可参考Java-内存模型-引用总结
ThreadLoca原理要点如下:
每个线程有自己的一个全局ThreadLocalMap
实例,ThreadLocalMap的key为ThreadLocal对象,value为用户自定义值。也就是说这个map可以放多个ThreadLocal对象。
但是一般来说,每次使用时,每个ThreadLocal
实例被多个线程共享而不是每个线程一个ThreadLocal实例。而且,我们应用的类对象里面有一个强引用指向ThreadLocal实例。
比如以下代码,就是多个TestThread线程实例共享一个ThreadLocal Pet对象,但每个线程拥有一份独占的受ThreadLocal保护的Dog实例:
public class Demo2
{
private static ThreadLocal<Dog> pet = new ThreadLocal<Dog>(){
@Override
protected Dog initialValue()
{
return new Dog("tom", 1);
}
};
private static class TestThread implements Runnable{
@Override
public void run()
{
// 获取由ThreadLocal保护的线程级别的初始值
Dog dog = pt.get();
// 改变该值
pt.set(new Dog("newName", 19));
// 从线程的ThreadLocalMap中清理该ThreadLocal对象
pt.remove();
}
}
ThreadLocalMap有一个Entry
数组,内嵌的Entry
类其实是继承自WeakReference
,即弱引用。这个弱引用指向该ThreadLocal实例(注意不是key
去指向,因为这个Entry里根本就没有key
这样一个对象)。
ThreadLocalMap的Entry
构造方法是Entry(ThreadLocal> k, Object v) { super(k); value = v; }
。
其中value是用户自定义的存储的值。
ThreadLocalMap的数据结构只有个Entry数组,而且放入元素发生Hash冲突时不是放入这个位置的链表而是调用nextIndex
方式继续查找适合的位置,在此过程中会调用一些方法清理失效(指Entry指向ThreadLocal实例的弱引用已经被GC清理)的Entry对,如果还是空间不够就扩容。
注意:网上很多文章说ThreadLocal原理是有个Map以每个Thread实例为key,这是绝对错误的。
还有人说Entry的key有一个弱引用指向ThreadLocal实例,也是错误的,因为Entry对象本身就是一个指向ThreadLocal实例的弱引用,存放在ThreadLocalMap的Entry数组内。
private final int threadLocalHashCode = nextHashCode();
// 两个连续的hashcode差值
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode = new AtomicInteger();
ThreadLocalMap类型的Thread.threadLocals和inheritableThreadLocals依赖于附属于每个线程的线性探测哈希映射。
ThreadLocal对象充当key,通过threadLocalHashCode搜索。 这是一个自定义哈希代码(仅在ThreadLocalMaps中有用),它消除了在相同线程使用连续构造的ThreadLocals的常见情况下的冲突,同时在不太常见的情况下保持良好行为。
关于HASH_INCREMENT = 0x61c88647
这个魔数,网上查了一些资料,大致原因是因为此数为黄金分割,可以让数组内的hash分布更均匀。更多内容可以参考:What is the meaning of 0x61C88647 constant in ThreadLocal.java
public ThreadLocal() {}
这里什么都没做。
protected T initialValue() {
return null;
}
这个方法会在第一次使用get
方法时调用setInitialValue
时执行,除非在此之前已经调用了set
方法。
一般来说该方法只会被调用一次,但如果使用remove
清空随后又调用get
方法时,又会再次调用initialValue
。
可以看到该方法默认返回的初值为null,如果我们想自定义一个初值,一般就是用匿名内部类的方式重写该方法实现自定义初值,比如以下代码定义了返回一个new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
的DateFormat
类型的初值的ThreadLocal:
ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
} };
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();
}
get
方法很重要,他被用来获取当前thread私有的threadlocal
保护的变量副本。
如果还没有当前线程的ThreadLocalMap或者该map中不存在以该ThreadLocal对象为key的Entry,那此时该线程的ThreadLocal保护的变量副本肯定不存在,则此时首先将其初始化为调用initialValue
方法的返回值。
ThreadLocal.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
方法去拿到初始值,然后获取当前线程的ThreadLocalMap
。如果map不存在就以当前thread
和value
创建ThreadLocalMap;如果已经存在就以当前ThreadLocal
实例为key,value为值放入该ThreadLocalMap。
ThreadLocal.getMap
方法如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
注意,这里的t就是当前线程对象,t.threadLocals
在Thread类里的声明如下:
ThreadLocal.ThreadLocalMap threadLocals = null;
可以看到,threadLocals
其实就是每个线程对象独有的一个ThreadLocalMap类型的实例对象。具体可参考后文ThreadLocalMap
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
该方法用来设置ThreadLocal保护的值,跟前面提到的setInitialValue
方法差不多,只不过这里指定了放入ThreadLocalMap的自定义value。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
该方法用于获取当前线程的ThreadLocalMap,然后从其中移除当前ThreadLocal实例为key的Entry
。
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
这个类的主要作用是配合withInitial
设定初始值,其实就是利用Supplier的lambda表达式写法。示例如下:
private static ThreadLocal<Dog> pt = ThreadLocal.withInitial(() -> new Dog("tom", 1));
ThreadLocalMap是一个自定义的类HashMap,他只适合维护threadlocal values
,没有向外暴露任何方法。
为了应对长期和高负荷的使用,所以采用了WeakReference
来修饰该map的key。也就是说当这些key无其他强引用时,GC会将他们回收。但需要注意的是,因为创建弱引用key的时候没有采用RefereceQueue
,所以只能保证那些已经没用的的Entry
会在entry table
超出大小限制时被移除。
static class ThreadLocalMap
可以看到,ThreadLocalMap是ThreadLocal里的一个静态内部类,而且限制使用域是本类、和同包(java.lang,包括Thread类)。
以下是ThreadLocalMap的一些成员标量,和HashMap类似:
// Entry Table初始容量
private static final int INITIAL_CAPACITY = 16;
// Entry数组,按需库容,且长度必须是2的n次方
private Entry[] table;
// Entry数组元素个数
private int size = 0;
// Entry数组扩容时的阈值,默认为0
private int threshold;
static class Entry extends WeakReference<ThreadLocal<?>> {
// 存放指定的value
Object value;
Entry(ThreadLocal<?> k, Object v) {
// 调用父类WeakReference构造方法,构建一个指向ThreadLocal实例的弱引用
super(k);
// 保存指定的value值
value = v;
}
}
Entry是ThreadLocalMap的静态内部类,他继承自WeakReference,也就是说Entry本身是个指向ThreadLocal对象的弱引用,记住这一点十分重要。
可以看到Entry内部只显示存放了指定的value
对象,而构造方法Entry(ThreadLocal> k, Object v)
里构建了一个到ThreadLocal实例的弱引用。
调用Entry.get
方法其实就是调用其祖先类Reference.get
方法,获取到的referent值为null
时,说明已经该弱引用已经不存在了,代表此时该Entry可以被Entry数组剔除,这种无效的Entry被称为stale entry
即过期的Entry。
有两个:一个是public一个是private。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始长度为16的Entry数组
table = new Entry[INITIAL_CAPACITY];
// 用每个key的threadLocalHashCode和(1111)按位做与操作得到Entry应该放的下标
// 这样做的好处就是不管你threadLocalHashCode再大,计算结果也不会超过15
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 初始化Entry,并放入数组对应位置
table[i] = new Entry(firstKey, firstValue);
// Entry数组大小更改为1
size = 1;
// 根据容量重设扩容阈值
setThreshold(INITIAL_CAPACITY);
}
这个方法就是传入第一个ThreadLocal对象作为key,value作为值,构建一个ThreadLocalMap。需要注意的是,ThreadLocalMap是懒创建的,也就是说直到有Entry需要加入才会调用此方法。可以参考之前ThreadLocal.setInitialValue
和ThreadLocal.set
方法,里面有调用ThreadLocal.createMap
方法:void createMap(Thread t, T firstValue) {
// 这里就以本ThreadLocal对象为key,首个自定义值为value来构造ThreadLocalMap
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
这个构造方法由createInheritedMap
方法调用,传入的参数是父线程的ThreadLocalMap。将会创建一个ThreadLocalMap包括所有父map内的ThreadLocal。// 设置扩容阈值为容量的三分之二
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
// 下标i+1的方式从小往大的下标方向递推
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
// 下标i-1的方式从大往小的下标方向递推
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
将Entry数组扩容为以前大小的两倍,顺便清理发生hash碰撞时key已为null的entry,具体步骤如下:
代码如下:
private void resize() {
// 1.创建新的Entry[],长度为旧数组两倍
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
// 2.然后循环遍历老的Entry[]
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
// 3.如果Entry元素存在就去获取弱引用的ThreadLocal
ThreadLocal<?> k = e.get();
if (k == null) {
// 4.1此时如果ThreadLocal引用已经为空,代表已经被GC回收,那么直接把当前Entry置为null;
e.value = null; // Help the GC
} else {
// 4.2.1否则通过threadLocalHashCode和(新的数组长度-1)按位与的方式得到新的数组下标
int h = k.threadLocalHashCode & (newLen - 1);
// 4.2.2接着判断该下标位置是否已存在元素,若存在就反复调用nextIndex方法求新的下标
while (newTab[h] != null)
h = nextIndex(h, newLen);
// 4.2.3将元素entry放入新的数组中的下标位置,并将临时元素计数器加一
newTab[h] = e;
count++;
}
}
}
// 5.更新扩容阈值,并将临时变量赋值给对应的entry table实例变量。扩容完成。
setThreshold(newLen);
size = count;
table = newTab;
}
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 寻找新entry放入的适合位置。
// hash冲突时再hash方式为nextIndex从下标小的方式往大的方向递推
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果计算出的下标位置存在entry
// 且该ThreadLocal实例key和参数ThreadLocal对象相同,那就更新value。set结束
if (k == key) {
e.value = value;
return;
}
// 如果计算出的下标位置存在entry且该ThreadLocal实例key为null
// 此时说明该entry的弱引用已经失效,就用生成新的entry替换。set结束
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
// 如果计算出的下标位置存在entry且该ThreadLocal实例key和参数ThreadLocal不同,就进入下一次循环
// 否则说明该位置e = null ,跳出循环
}
// 此时e = null,也就是说该数组位置不存在entry
// 用参数生成一个新的entry,放入此位置即可
tab[i] = new Entry(key, value);
// 大小加一
int sz = ++size;
// 如果没有发生清理行为且 当前数组元素个数达到扩容阈值,就需要rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocalMap put元素遇到有效Entry对,发生hash冲突时,不同于hashMap是放入该位置的链表,而是通过nextIndex
方法从下标小的往大的方向递推继续找合适位置。
// 获取ThreadLocal实例对应的Entry
// 如果没能匹配到就调用getEntryAfterMiss
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
// 当没有找到ThreadLocal实例对应的Entry时,只能调用该方法来查找
// 这个方法也会在查找过程中顺便清理无效的弱引用Entry
// i为数组下标,e为当前下标对应的Entry
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
// 找到该entry就返回
return e;
if (k == null)
// 该位置ThreadLocal引用为空,清理该etnry
expungeStaleEntry(i);
else
// 否则继续递推查找
i = nextIndex(i, len);
e = tab[i];
}
// 到这里,说明没有找到
return null;
}
可以看到在没有一次性找到对应位置的元素的时候调用了getEntryAfterMiss
方法,会在查找过程中不断清理无效的弱引用Entry。
// 将传入的参数ThreadLocal实例对应的Entry弱引用去掉,并把Entry从map中移除
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
// 替换失效的数组位置上的Entry,在此过程中将遇到的失效弱引用Entry移除
// key和value为新的值,staleSlot为匹配到的第一个失效Entry下标
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
// 倒推以检查当前运行中的已失效Entry。
// 我们一次清理整个运行,以避免由于垃圾收集器释放串联的refs(即,每当收集器运行时)不断的增量重复。
int slotToExpunge = staleSlot;
// 这里得到的slotToExpunge是从后往前推的最后一个e!=null但是e.get()==null的下标
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 找到的key或尾部的空元素
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
// 如果匹配到键,那么我们需要将它与失效Entry交换以维护哈希表顺序。
// 然后可以将新陈旧的插槽或其上方遇到的任何其他陈旧插槽发送到expungeStaleEntry以删除或重新运行运行中的所有其他条目。
if (k == key) {
// 如果存在该ThreadLocal实例的Entry,就覆盖该value
e.value = value;
// 这里staleSlot为匹配到的第一个失效Entry下标
// 赋值后tab[i]!=null但是tab[i].get()==null
tab[i] = tab[staleSlot];
// 原staleSlot Entry替换为value更新后的e
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
// slotToExpunge == staleSlot的情况是在前面倒推运算中没有找到失效的Entry
if (slotToExpunge == staleSlot)
// 注意这里i下标对应的元素是tab[staleSlot]
slotToExpunge = i;
// 移除slotToExpunge位置的Entry顺便移除一些失效Entry
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
// 该Entry弱引用失效,且在前面倒推查找中没有匹配到失效的Entry
if (k == null && slotToExpunge == staleSlot)
// 那就把当前这个对应失效弱引用的i给slotToExpunge,然后继续循环
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
// 走到这里说明没有匹配到ThreadLocal key
// 把staleSlot处的value设为空(help gc)
tab[staleSlot].value = null;
// 该位置设为由新的ThreadLocal实例为key,新value的Entry
tab[staleSlot] = new Entry(key, value);
// 不相等说明找到了另外的失效的Entry位置,干掉他们并顺便清理出一点空间
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
// 清理一些Entry
// 清理次数由当前数组大小是2的多少倍决定
// 所以叫cleanSomeSlots - -|
// 当发生了清理就返回true,否则返回false
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
// 找到下一个下标位置,如果失效entry就干掉
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
// 先清理所有失效的Entry
// 如果 此时size还是大于之前的四分之三,就扩容
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
// 遍历数组,移除失去弱引用的旧Entry
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
// 找到失去弱引用的Entry,将该Entry所在下标传入expungeStaleEntry将其干掉
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
// 此方法是真正移除失去弱引用的Entry的方法,顺便移除遇到的碰撞位置的失效Entry
// 参数staleSlot 是该Entry所在下标
// 返回下标i,此时tab[i]为null
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
// 数组原始长度
int len = tab.length;
// 移除Entry的value和本身的引用
tab[staleSlot].value = null;
tab[staleSlot] = null;
// 数组大小减一
size--;
// Rehash until we encounter null
Entry e;
int i;
// 循环的方式去掉弱引用失效的entry
// 循环开始条件是让该下标对原始数组长度求模
// 循环结束条件是Entry为null
// 每次循环完又再次nextIndex计算新下标
// 循环过程中还会把清理了entry位置的按nextIndex的往后查找
// 将找到的弱引用为null的清理,
// 非null且为递推移动放入的元素按规则放入新位置
// 这样可以避免清理元素后无法递推查找其他元素
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
// 找到弱引用失效的entry,干掉
e.value = null;
tab[i] = null;
size--;
} else {
// 此时表示e的弱引用存在
// 我们已经熟悉了这种获取下标方式
int h = k.threadLocalHashCode & (len - 1);
// 不相等表示存在hash冲突,放入的位置是用N次nextIndex计算后的新位置
// 现在由于递推之前的位置发生了清理,所以会导致递推查找失败
// 现在必须把该位置的entry放到新的null位置
if (h != i) {
// 将递推i位置的Entry置空
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
// 找到一个新的无entry的数组下标
while (tab[h] != null)
h = nextIndex(h, len);
// 将e移动到新的数组下标位置
tab[h] = e;
}
}
}
// 此时tab[i]为空
return i;
}
因为代码中一般长期存在指向ThreadLocal
对象的强引用,那么,使用了该ThreadLocal的线程的ThreadLocalMap里的Entry之key,虽是指向该ThreadLocal对象的弱引用,但是因为代码对ThreadLocal强引用(就是声明如private static ThreadLocal
的强引用)持续存在,导致这类弱引用Entry无法被及时回收。
也就是说,这类弱引用Entry会随着线程持续存在而存在,造成内存泄露。所以我们应该在每个线程使用完ThreadLocal对象后,调用remove
方法,手动移除该Entry。
线程池中的Thread对象就那么几个,都是复用的。也就是说,他们的ThreadLocalMap对象是不会变的,会导致Runnable运行时的ThreadLocal值交叉混用,出现问题。那么就需要在每个Runnable的run方法执行完后执行threadLocal.remove()
,示例如下:
ExecutorService executorService = new ThreadPoolExecutor(5,5,1, TimeUnit.MINUTES,workQueue,new ThreadPoolExecutor.DiscardPolicy()){
@Override
protected void afterExecute(Runnable r, Throwable t)
{
threadLocalName.remove();
super.afterExecute(r, t);
}
};
SimpleDateFormat并不是线程安全的,所以在阿里的Java开发规范里推荐了用ThreadLocal保证线程安全的做法:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
} };
String startTimeStr = "2018-01-11 02:17:02.806";
try {
Date startTime = df.get().parse(startTimeStr);
System.out.println("startTime=" + startTime);
String dfStr = df.get().format(new Date());
System.out.println("dfStr=" + dfStr);
}catch (ParseException e) {
e.printStackTrace();
}
JavaDoc-java.lang.ThreadLocal
彻底理解ThreadLocal
ThreadLocal原理及内存泄露预防
图解Java多线程