目录
- 1. ThreadLocal是什么,它有哪些特性?
- 2. ThreadLocal的底层数据结构包含哪些内容?
- 3. ThreadLocalMap的初始大小、加载因子分别是多少?
- 4. ThreadLocal底层用到的Hash算法是什么?
- 5. ThreadLocal如何解决Hash冲突?
- 6. ThreadLocal底层的扩容机制是什么?
- 7. ThreadLocal的get方法的实现流程?
- 8. ThreadLocalMap的key是强引用,还是弱引用?为什么?
- 9. ThreadLocalMap中的key可能过期么?set、get可能会清理过期key的相关Entry么?
- 10. ThreadLocal的set方法的实现流程?
- 11. 如何防止ThreadLocal发生内存泄漏?
- 12. ThreadLocal的应用场景有哪些?
- 13. ThreadLocal和synchronized有什么区别?
1. ThreadLocal是什么,它有哪些特性?
ThreadLocal是线程变量,ThreadLocal中设置的变量属于当前线程,该变量对其它线程而言是隔离的。 ThreadLocal在每个线程中都创建了一个变量副本,每个线程可以访问自己内部的副本变量。
ThreadLocal具有以下特性:
- 并发性:在多线程并发场景下使用。
- 传递数据:可以通过ThreadLocal在同一线程的上下文中传递参数
- 线程隔离:每个线程变量都相互独立,不会相互影响。
ThreadLocal相关的核心API:
- get() 方法用于获取当前线程的副本变量值。
- set() 方法用于保存当前线程的副本变量值。
- initialValue() 为当前线程初始副本变量值。
- remove() 方法移除当前线程的副本变量值。
ThreadLocal几个重要的变量
// 计算hash值
private final int threadLocalHashCode = nextHashCode();
// 使用原子类记录hash值
private static AtomicInteger nextHashCode =
new AtomicInteger();
// 魔数,更好的分散数据
private static final int HASH_INCREMENT = 0x61c88647;
// 初始化容量
private static final int INITIAL_CAPACITY = 16;
// 存储数据数组
private Entry[] table;
// 数组中的元素个数
private int size = 0;
// 扩容的阈值,默认是数组大小的三分之二
private int threshold;
123456789101112131415
例子1: ThreadLocal在CLHLock自旋锁中的使用:
public class CLHLock implements Lock {
//队列尾部的QNode,用AtomicReference,自带CAS来非阻塞同步?
private AtomicReference<QNode> tail;
//用ThreadLocal来解决跨线程的数据同步性能问题
//当前线程维护自己的QNode
private ThreadLocal<QNode> myNode;
//当前线程的QNode的前驱
private ThreadLocal<QNode> preNode;
//重入次数
//为什么用ThreadLocal呢?每个线程管理自己的count,不用每次读取主内存中读取
//虽然Atomic通过volatile和CAS保证了线程安全,但是还有从主内存中读写的性能消耗
private ThreadLocal<AtomicInteger> count;
public CLHLock() {
//初始化队尾
tail = new AtomicReference<>();
myNode = ThreadLocal.withInitial(new Supplier<QNode>() {
@Override
public QNode get() {
return new QNode();
}
});
myNode.get().lock=false;
preNode = ThreadLocal.withInitial(() -> null);
count = ThreadLocal.withInitial(AtomicInteger::new);
tail.set(myNode.get());
}
//将线程加到队尾去,如何实现重入?如果myNode有了,就不管了
@Override
public void lock() {
QNode node = myNode.get();//这里不可能为null,因为设置了初始值
node.lock = true;
//如果没有重入过,也就是第一次进来
if (count.get().get()==0){
//获取前驱,而后尾结点指向本节点
QNode pre = tail.getAndSet(myNode.get());
//设置它的前驱
preNode.set(pre);
count.get().incrementAndGet();
}else{
//如果重入过,
//如果要吧它调到队伍最后,那么要通过遍历的方式,将它从链表中退出,加到链表最后
//...
}
//判断是否有前驱,如果有前驱,如果前驱要lock则自旋
//注意这里直接引用前驱的Node
if (preNode.get() != null){
while (preNode.get().lock) {
//进入自旋
}
}
}
@Override
public void unlock() {
//lock变为false,释放自己
if(count.get().get() > 0){
count.get().decrementAndGet();
}else{
myNode.get().lock = false;
//回收方案,将当前节点引用置为自己的preNode
myNode.set(preNode.get());
}
}
public static void main(String[] args) {
CLHLock clhLock = new CLHLock();
for (int i = 0; i < 30; i++) {
//开30个线程,都重入一次
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("当前线程 "+Thread.currentThread()+" 请求获得锁");
try {
clhLock.lock();
System.out.println("当前线程 "+Thread.currentThread()+" 获得锁");
Thread.sleep(500);
try{
clhLock.lock();
System.out.println("重入锁 当前线程 "+Thread.currentThread().getName());
Thread.sleep(500);
}finally {
clhLock.unlock();
}
}catch (Exception e){
e.printStackTrace();
}finally {
clhLock.unlock();
}
}
}).start();
}
}
//静态内部类对外不持有引用
private static class QNode {
/**
* true表示该线程需要获取锁,且不释放锁,为false表示线程释放了锁,且不需要锁
*/
private volatile boolean lock = true;
}
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
例子2:原子类自增Demo
public class ThreadLocalDemo1 {
public static ThreadLocal<String> threadlocal = new ThreadLocal<String>();
public static class MyThread extends Thread {
private static AtomicInteger atomicInt = new AtomicInteger();
public void run() {
for(int i=0;i<4;i++) {
threadlocal.set(atomicInt.addAndGet(1) + "");
// threadlocal.set(null);
System.out.println("thread name is :" + this.getName() + " , value is :" + threadlocal.get());
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws Exception {
Thread th1 = new MyThread();
Thread th2 = new MyThread();
Thread th3 = new MyThread();
th1.setName("Thread 1");
th2.setName("Thread 2");
th3.setName("Thread 3");
th1.start();
th2.start();
th3.start();
}
}
1234567891011121314151617181920212223242526272829303132
2. ThreadLocal的底层数据结构包含哪些内容?
-
ThreadLocal 数据结构:
每个Thread线程内部都有一个ThreadLocalMap,ThreadLocalMap是一个初始化的大小为 16 的 Entry数组,Entry对象用来保存每一个 key-value 键值对,key是ThreadLocal对象,value是泛型的 Object。
Thread 源码:每个Thread线程内部都有一个ThreadLocalMap
public class Thread{ ... /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; ... } 123456789101112131415
ThreadLocalMap结构:
ThreadLocalMap构造函数:
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始 16 容量
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
//设置扩容阈值 ( 参数 * 2 / 3)
setThreshold(INITIAL_CAPACITY);
}
123456789
-
调用链:
Thread --> ThreadLocal
--> ThreadLocalMap(ThreadLocal firstKey, Object firstValue) --> Entry[] --> Entry(ThreadLocal k, Object v)
3. ThreadLocalMap的初始大小、加载因子分别是多少?
初始大小为 16,加载因子为 2/3
加载因子:加载因子是衡量哈希表密集程度的一个参数,如果加载因子越大,说明哈希表被装载的越多,出现hash冲突的可能性越大,繁殖,被装载的越少,出现hash冲突的可能性越小,如果过小,内存使用率不高,该值取值应该考虑到内存使用率和hash冲突概率的平衡。
4. ThreadLocal底层用到的Hash算法是什么?
-
ThreadLocal 底层用斐波那契数(黄金分割数)实现 hash 算法
-
ThreadLocal 有属性 HASH_INCREMENT(斐波那契数),当每创建一个ThreadLocal对象,它的下一个 hashcode 就会增长 0x61c88647
private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } 123456
-
用斐波那契数生成Entry环形数组,实现 hash的均匀分布 -> 降低hash冲突发生可能
5. ThreadLocal如何解决Hash冲突?
- ThreadLocalMap 使用开放地址法(线性探测)解决 hash 冲突,HashMap使用链地址法解决 hash 冲突。
- ThreadLocalMap 采用开放地址法(线性探测法)解决hash冲突。ThreadLocalMap 根据初始 key 的哈希值确定元素在 table 数组中的位置,如果发现这个位置上已经被其他 key 占用,则利用固定的算法寻找一定步长的下一个位置,依次判断,直至找到能够存放的位置。
6. ThreadLocal底层的扩容机制是什么?
ThreadLocal扩容——量化、弹性扩容;
- 当 table 中Entry的数量 size >= threshold *3/4 时(size >= cap * 2 / 3 * 3 / 4)(扩容的判断条件),需要对 table 进行 2 倍扩容。扩容原因:降低哈希冲突。
private void rehash(){
expungeStaleEntries();
if(size >= threshold - threshold /4) resize();
}
1234
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; // Help the GC
} 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;
}
123456789101112131415161718192021222324252627
7. ThreadLocal的get方法的实现流程?
- 获取当前线程的 threadLocals变量 (map 结构), 从 threadLocals变量 中获取当前 ThreadLocal 变量对应的 ThreadLocalMap.Entry,非空直接返回对应的 value
- 为空时使用默认值null构造ThreadLocalMap.Entry,放到当前线程的threadLocals变量中,下次再get时直接返回ThreadLocalMap.Entry对应的value即可。
入口为 ThreadLocal 的 get():
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap
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();
}
123456789101112131415
如果map不为空,进入到 map.getEntry():
private Entry getEntry(ThreadLocal<?> key) {
//计算下标
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//如果不为空,而且key一致
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
12345678910
解决冲突问题的开放地址法去寻找Entry
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//按照已订好的线性探测法逐个寻找
while (e != null) {
ThreadLocal<?> k = e.get();
//如果找到一样的key直接返回
if (k == key)
return e;
//如果发现key为null,expungeStaleEntry(i)通过重新散列所有可能发生冲突的项来删除位于i和下一个空槽之间的陈旧的项
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
12345678910111213141516171819
如果ThreadLocal获得的map为空,则调用 setInitialValue()去使用默认值null构造ThreadLocalMap.Entry,放到当前线程的threadLocals中
private T setInitialValue() {
//默认null的value
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
1234567891011
8. ThreadLocalMap的key是强引用,还是弱引用?为什么?
- ThreadLocalMap的 key 是弱引用,弱引用可保证 ThreadLocal 实例对象在每次 GC 时候都能得到清除以释放其占用的内存空间。
9. ThreadLocalMap中的key可能过期么?set、get可能会清理过期key的相关Entry么?
ThreadLocalMap 的 key 可能会过期。由于 ThreadLocalMap 的 key 是弱引用类型,所以可能被 GC 自动回收,从而导致 key 为 null,但该槽对应的Entry并不一定被回收,value 不一定被回收。
过期key对应Entry的清理:
- 在调用 get 和 set方法的过程中,都可能会触发清理过期key对应的Entry。
- 在 get方法中可能的调用链: get --> getEntry --> getEntryAfterMiss --> expungeStaleEntry
- set方法中可能的调用链:
- replaceStaleEntry --> cleanSomeSlots(expungeStaleEntry(slotToExpunge),len);
- cleanSomeSlots --> expungeStaleEntry
- expunge : 清除, stale : 过期的, slot : 槽
- remove rehash等也会有到清理过期key的操作。
10. ThreadLocal的set方法的实现流程?
set方法实现流程的关键点:清理过期key、扩容、设置值
- 根据 hash 值和数组长度 求元素放置的位置,即数组下标
- 从第一步得出的下标开始遍历,如果key相等,覆盖value,如果key为null,用新的key、value覆盖,同时清理历史key = null 的陈旧数据
- 如果当 table 中元素的数量 size >= threshold,遍历 table 并删除 key为null的元素,如果删除后 size>=threshold * 3 /4时,需要对 table 进行 2 倍扩容,把老数据重新哈希散列rehash到新的table中。
ThreadLocal的set方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
12345678
进入到ThreadlocalMap的set方法:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//根据 hash 值和数组长度 求元素放置的位置,即数组下标
int i = key.threadLocalHashCode & (len-1);
//从第一步得出的下标开始遍历
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果key相等,覆盖value
if (k == key) {
e.value = value;
return;
}
//如果key为null,用新的key、value覆盖
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
//启发式的扫描并清除过期的key 清理历史key = null 的陈旧数据
//如果没有需要清除的并且需要扩容,则进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
1234567891011121314151617181920212223242526272829303132
先看到最后的清除过期key的方法 cleanSomeSlots:
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
// 从位置i的下一个开始搜索
i = nextIndex(i, len);
Entry e = tab[i];
// 如果遇到过期的Entry,清除
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
// 只要有过期Entry被移除就会返回true
return removed;
}
123456789101112131415161718
看到里面的rehash()方法,就是清理过期的Entry,并且可能的话还会触发resize
private void rehash() {
expungeStaleEntries();
if (size >= threshold - threshold / 4)
resize();
}
123456
11. 如何防止ThreadLocal发生内存泄漏?
内存泄漏的原因:
- ThreadLocal 里面使用了一个存在弱引用的ThreadLocalMap,当时放掉 ThreadLocal的强引用后(该ThreadLocal只剩下map的key的弱引用),ThreadlocalMap里面的 value 却没有被回收,而这块 value 永远不会被访问到了,所以会存在内存泄漏。
如何防止内存泄漏:
- 在使用完毕后,及时调用remove方法。Entry继承弱引用,但只能实现对Reference的key的回收,而对value的回收则需要手动解决。remove方法中有将其应用置空null的操作,断掉value的强引用使其可被回收。
我们看到ThreadLocal的remove方法,这是主动remove掉以自己这个对象为key的Entry:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
12345
实际是调用ThreadLocalMap的remove操作:
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;
}
}
}
1234567891011121314
e.clear():
public void clear() {
this.referent = null;// Reference<T>中的 referent变量
}
123
除了通过e.clear()让key引用的ThreadLocal置空,还通过expungeStaleEntry方法去删除key == null的槽
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
// 将空槽后面的有实际意义的Entry前移(rehash)
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;
}
12345678910111213141516171819202122232425262728293031323334
可以利用jvisulvm来检测内存泄漏
12. ThreadLocal的应用场景有哪些?
- 线程安全: ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,确保了线程安全。
- 参数传递: ThreadLocal 用作每个线程内需要独立保存上下文参数,以便供其他方法更方便地获取这些上下文参数。 ThreadLocal设计模式/上下文设计模式 。特别的,这种应用在多层架构中,使用比较多。例如:在同一个线程的不同开发层次中共享数据。 如 Web应用开发中表示层、业务层、持久层需要共享数据等。
使用ThreadLocal的注意事项:
- 不要使用 ThreadLocal 存储大对象
- 注意使用 ThreadLocal 的 remove 方法,清理过期数据,否则会产生大量脏数据,发生内存泄漏等问题。
- 使用 private final static 进行修饰,防止多实例时内存的泄漏问题。(可能丢失之前的引用,导致之前那个ThreadLocal对象对应的Entry无法调用remove方法去清理过期数据)
13. ThreadLocal和synchronized有什么区别?
- synchronized 的资源是多个线程共享的,所以访问的时候需要加锁。
- ThreadLocal 是每个线程都有一个副本(作为map中的key,这个map是每个线程维护一个的),是不需要加锁的。
- synchronized 是 时间换空间。
- ThreadLocal 是 空间换时间。