文章目录
一、什么是ThreadLocal?二、使用场景三、源码分析四、面试题1、ThreadLocal中的ThreadLocalMap中的key为什么要是弱引用?采用了弱引用还会发生内存泄露问题吗?2、ThreadLocal的原理是什么?怎样做到线程之间互不干扰的?3、ThreadLocalMap存放数据时hash值是用的Object.hashCode吗?为什么?4、为什么ThreadLocal要自定义一款Map而不用JDK中的HashMap?5、每个线程的ThreadLocalMap是什么时候创建的?6、ThreadLocalMap怎么解决hash冲突的?7、ThreadLocalMap的扩容阈值是多少?达到扩容阈值一定会扩容吗?扩容算法过程是啥?8、说一下ThreadLocalMap中的get流程9、探测式过期数据清理过程中遇到正常数据怎么处理?10、说一下ThreadLocalMap中的set流程,碰到过期数据怎么处理?
一、什么是ThreadLocal?
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行访问的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal就提供了一种不需要使用同步措施(加锁等)就能保证线程安全的一种机制。
ThreadLocal提供了线程内部存储变量的能力,ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。ThreadLocal底层为每个线程都维护了一个ThreadLocalMap,key就是threadLocal对象,value就是需要存储的局部变量。
二、使用场景
1、JDBC连接数据库时,为每个线程分配一个连接Connection,这样可以保证每个线程都在各自的Connection上进行数据库的操作,不会出现A线程关闭了B线程正在使用的Connection。
2、当前用户信息需要被线程内的所有方法共享,使用ThreadLocal实现不同方法间的资源共享。避免加锁产生的性能问题。
3、spring中是用ThreadLocal解决线程安全问题,Spring框架并没有对单例进行任何多线程的封装处理,关于bean的线程安全和并发问题需要开发者自行去搞定。但实际上,大部分的Spring bean并没有可变的状态,所以在某种程度上说Spring的单例bean是线程安全的。如果你的bean有多种状态的话(比如View Model对象),就需要自行保证线程安全。将多态bean的作用域由“singleton”变更为“prototype”或者使用ThreadLocal进行处理。
三、源码分析
public class ThreadLocal<T> {
private final int threadLocalHashCode
= nextHashCode();
private static AtomicInteger nextHashCode
=
new AtomicInteger();
private static final int HASH_INCREMENT
= 0x61c88647;
private static int nextHashCode() {
return nextHashCode
.getAndAdd(HASH_INCREMENT
);
}
protected T
initialValue() {
return null
;
}
public static <S> ThreadLocal
<S> withInitial(Supplier
<? extends S> supplier
) {
return new SuppliedThreadLocal<>(supplier
);
}
public ThreadLocal() {
}
public T
get() {
Thread t
= Thread
.currentThread();
ThreadLocalMap map
= getMap(t
);
if (map
!= null
) {
ThreadLocalMap
.Entry e
= map
.getEntry(this);
if (e
!= null
) {
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
;
}
public void set(T value
) {
Thread t
= Thread
.currentThread();
ThreadLocalMap map
= getMap(t
);
if (map
!= null
)
map
.set(this, value
);
else
createMap(t
, value
);
}
public void remove() {
ThreadLocalMap m
= getMap(Thread
.currentThread());
if (m
!= null
)
m
.remove(this);
}
ThreadLocalMap
getMap(Thread t
) {
return t
.threadLocals
;
}
void createMap(Thread t
, T firstValue
) {
t
.threadLocals
= new ThreadLocalMap(this, firstValue
);
}
static ThreadLocalMap
createInheritedMap(ThreadLocalMap parentMap
) {
return new ThreadLocalMap(parentMap
);
}
T
childValue(T parentValue
) {
throw new UnsupportedOperationException();
}
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();
}
}
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal
<?>> {
Object value
;
Entry(ThreadLocal
<?> k
, Object v
) {
super(k
);
value
= v
;
}
}
private static final int INITIAL_CAPACITY
= 16;
private Entry
[] table
;
private int size
= 0;
private int threshold
;
private void setThreshold(int len
) {
threshold
= len
* 2 / 3;
}
private static int nextIndex(int i
, int len
) {
return ((i
+ 1 < len
) ? i
+ 1 : 0);
}
private static int prevIndex(int i
, int len
) {
return ((i
- 1 >= 0) ? i
- 1 : len
- 1);
}
ThreadLocalMap(ThreadLocal
<?> firstKey
, Object firstValue
) {
table
= new Entry[INITIAL_CAPACITY
];
int i
= firstKey
.threadLocalHashCode
& (INITIAL_CAPACITY
- 1);
table
[i
] = new Entry(firstKey
, firstValue
);
size
= 1;
setThreshold(INITIAL_CAPACITY
);
}
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
++;
}
}
}
}
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
);
}
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
)
return e
;
if (k
== null
)
expungeStaleEntry(i
);
else
i
= nextIndex(i
, len
);
e
= tab
[i
];
}
return null
;
}
private void set(ThreadLocal
<?> key
, Object value
) {
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
)]) {
ThreadLocal
<?> k
= e
.get();
if (k
== key
) {
e
.value
= value
;
return;
}
if (k
== null
) {
replaceStaleEntry(key
, value
, i
);
return;
}
}
tab
[i
] = new Entry(key
, value
);
int sz
= ++size
;
if (!cleanSomeSlots(i
, sz
) && sz
>= threshold
)
rehash();
}
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;
}
}
}
private void replaceStaleEntry(ThreadLocal
<?> key
, Object value
,
int staleSlot
) {
Entry
[] tab
= table
;
int len
= tab
.length
;
Entry e
;
int slotToExpunge
= staleSlot
;
for (int i
= prevIndex(staleSlot
, len
);
(e
= tab
[i
]) != null
;
i
= prevIndex(i
, len
))
if (e
.get() == null
)
slotToExpunge
= i
;
for (int i
= nextIndex(staleSlot
, len
);
(e
= tab
[i
]) != null
;
i
= nextIndex(i
, len
)) {
ThreadLocal
<?> k
= e
.get();
if (k
== key
) {
e
.value
= value
;
tab
[i
] = tab
[staleSlot
];
tab
[staleSlot
] = e
;
if (slotToExpunge
== staleSlot
)
slotToExpunge
= i
;
cleanSomeSlots(expungeStaleEntry(slotToExpunge
), len
);
return;
}
if (k
== null
&& slotToExpunge
== staleSlot
)
slotToExpunge
= i
;
}
tab
[staleSlot
].value
= null
;
tab
[staleSlot
] = new Entry(key
, value
);
if (slotToExpunge
!= staleSlot
)
cleanSomeSlots(expungeStaleEntry(slotToExpunge
), len
);
}
private int expungeStaleEntry(int staleSlot
) {
Entry
[] tab
= table
;
int len
= tab
.length
;
tab
[staleSlot
].value
= null
;
tab
[staleSlot
] = null
;
size
--;
Entry e
;
int i
;
for (i
= nextIndex(staleSlot
, len
);
(e
= tab
[i
]) != null
;
i
= nextIndex(i
, len
)) {
ThreadLocal
<?> k
= e
.get();
if (k
== null
) {
e
.value
= null
;
tab
[i
] = null
;
size
--;
} else {
int h
= k
.threadLocalHashCode
& (len
- 1);
if (h
!= i
) {
tab
[i
] = null
;
while (tab
[h
] != null
)
h
= nextIndex(h
, len
);
tab
[h
] = e
;
}
}
}
return i
;
}
private boolean cleanSomeSlots(int i
, int n
) {
boolean removed
= false;
Entry
[] tab
= table
;
int len
= tab
.length
;
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
;
}
private void rehash() {
expungeStaleEntries();
if (size
>= threshold
- threshold
/ 4)
resize();
}
private void resize() {
Entry
[] oldTab
= table
;
int oldLen
= oldTab
.length
;
int newLen
= oldLen
* 2;
Entry
[] newTab
= new Entry[newLen
];
int count
= 0;
for (int j
= 0; j
< oldLen
; ++j
) {
Entry e
= oldTab
[j
];
if (e
!= null
) {
ThreadLocal
<?> k
= e
.get();
if (k
== null
) {
e
.value
= null
;
} else {
int h
= k
.threadLocalHashCode
& (newLen
- 1);
while (newTab
[h
] != null
)
h
= nextIndex(h
, newLen
);
newTab
[h
] = e
;
count
++;
}
}
}
setThreshold(newLen
);
size
= count
;
table
= newTab
;
}
private void expungeStaleEntries() {
Entry
[] tab
= table
;
int len
= tab
.length
;
for (int j
= 0; j
< len
; j
++) {
Entry e
= tab
[j
];
if (e
!= null
&& e
.get() == null
)
expungeStaleEntry(j
);
}
}
}
}
四、面试题
1、ThreadLocal中的ThreadLocalMap中的key为什么要是弱引用?采用了弱引用还会发生内存泄露问题吗?
当我们在方法中创建一个threadLocal对象时,栈中就有一个强引用指向这个对象,而threadLocalMap中的key也是threadLocal对象,当调用set方法时,就会有threadLocalMap中的key的一个引用指向它,如果这个key是强引用,那么当栈帧中的强引用销毁了,因为threadLocalMap中还有这个对象的引用,这个对象不能被垃圾回收,会造成严重的内存泄露。
注意:虽然是弱应用,保证了key指向的threadLocal对象能被及时回收,但是value对象还是需要threadLocalMap调用set或get方法才会去回收整个散列表,因此弱引用不能完全保证内存不泄露。
解决办法:在不使用某个threadLocal对象时,要及时的手动调用remove方法来删除它,尤其是在线程池中,线程是重复使用的,要特别注意这个问题。
2、ThreadLocal的原理是什么?怎样做到线程之间互不干扰的?
ThreadLocal类里面有一个ThreadLocalMap的静态内部类,每个线程都有一个自己的ThreadLocalMap类型的引用threadLocals。
ThreadLocalMap
getMap(Thread t
) {
return t
.threadLocals
;
}
ThreadLocalMap中维护的是一个Entry数组,每个entry有两个节点key和value,key表示threadLocal对象,它是一个弱引用。value是当前线程要保存的局部变量。当当前线程被销毁时,ThreadLocalMap也就会被垃圾回收。
3、ThreadLocalMap存放数据时hash值是用的Object.hashCode吗?为什么?
用的不是Object.hashCode,而是自己定义的获取hashCode的方法,并且它的初始值是一个黄金分割数,每次hash时都使用原子类在初始值上加上这个黄金分割数。
private static final int HASH_INCREMENT
= 0x61c88647;
private static int nextHashCode() {
return nextHashCode
.getAndAdd(HASH_INCREMENT
);
}
这样做的好处:使元素在散列表中的分布更加均匀,个人认为:也与它采用的是开发地址法来解决哈希冲突有关,为发生哈希冲突后的元素预留位置。
4、为什么ThreadLocal要自定义一款Map而不用JDK中的HashMap?
(1)ThreadLocalMap中的key是threadLocal类型,并且是弱引用。
(2)ThreadLocalMap在写数据和查数据的过程中提供了清理过期数据的功能,从某种意义上来说,解决了内存泄漏的问题。
5、每个线程的ThreadLocalMap是什么时候创建的?
采用的是一种延迟初始化的思想,在当前线程第一个get或者set方法时才会进行判断,如果ThreaLocalMap为空,就会创建一个map。
void createMap(Thread t
, T firstValue
) {
t
.threadLocals
= new ThreadLocalMap(this, firstValue
);
}
6、ThreadLocalMap怎么解决hash冲突的?
采用的是开放地址法:当元素发生哈希冲突时,会线性向数组的后面查找,一直找到Entry为空的槽位停止寻找,将当前元素放入。当然还有其他情况,比如遇到过期数据的处理,后面会讲解。
7、ThreadLocalMap的扩容阈值是多少?达到扩容阈值一定会扩容吗?扩容算法过程是啥?
扩容阈值是数组长度的2/3,达到扩容阈值不一定会扩容,它会调用一个rehash方法,这个方法中会对全表进行一次扫描,进行探测式数据清理,清理过期数据,清理完之后,如果当前数组中的元素数量还是大于扩容阈值的3/4,才会真正触发扩容。
private void rehash() {
expungeStaleEntries();
if (size
>= threshold
- threshold
/ 4)
resize();
}
扩容的过程:
创建一个新数组,是原数组的二倍,遍历原来的数组,如果当前元素不为空(空就不迁移),就获取它的key,如果key为null(过期数据不需要迁移),则把它的value赋为null,帮助GC;如果不为空,根据key计算index,判断是否发生哈希冲突,如果发生,线性向后查找直到找到合适位置,然后放入元素,如果没有冲突,直接放入元素。最后重新计算新的扩容阈值(数组长度的2/3)
8、说一下ThreadLocalMap中的get流程
首先会根据传入的key计算出一个index,比较index位置的key是否和传入的key相等,相等的话直接返回table[index],不相等需要向后遍历进行查找。以计算出的这个index处的元素开始,向后查找,如果遇到元素的key=null的情况,对着这个位置进行一次探测式过期数据清理(不光是清理数据的功能)。直到找到这个数据或者没找到返回null。
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
)
return e
;
if (k
== null
)
expungeStaleEntry(i
);
else
i
= nextIndex(i
, len
);
e
= tab
[i
];
}
return null
;
}
9、探测式过期数据清理过程中遇到正常数据怎么处理?
首先会重新计算一个key的index位置,判断是不是等于当前这个位置,等于不做操作(写入时没有发生hash冲突),不等于说明发生过哈希冲突了,考虑到当前正在执行清理数据的逻辑,当前位置的前面可能有过期数据被干掉。所以这个正常的数据需要重新寻找一个更合适的位置去存放。这个位置理论上应该接近于正确的index。就是从这个位置开始向后遍历,找到一个槽位为空的位置,然后放入这个正常数据。以此来优化散列表的查询性能。
int h
= k
.threadLocalHashCode
& (len
- 1);
if (h
!= i
) {
tab
[i
] = null
;
while (tab
[h
] != null
)
h
= nextIndex(h
, len
);
tab
[h
] = e
;
}
10、说一下ThreadLocalMap中的set流程,碰到过期数据怎么处理?
首先使用key计算出index,以index开始遍历数组,直到遇到entry为null时,循环结束:
遍历过程中key一致时,直接替换value。遇到过期数据时:看下面遇到entry为null时,直接创建一个新的entry,放入当前位置,然后判断是否需要进行rehash操作。
set过程中遇到过期数据:采用的是一种替换过期数据的思想
用slotExpunge表示向前遍历过程中数组中的第一个过期数据的下标,方便后面采用启发式清理方法进行数据清理
以当前结点(过期的)开始向前去检查是否有过期key,如果有更新slotExpunge值,直到碰到null结束从当前结点向后查找是否存在key一致的情况 如果存在:替换value,并且和刚开始的过期数据进行位置交换,然后从slotExpunge开始向后检查清理过期数据(使用的是启发式清理)。 如果不存在:则创建一个新的entry,替换刚开始的过期数据,然后从slotExpunge向后进行数据清理。