ThreadLocal源码探究 (JDK 1.8)
ThreadLocal
类之前有了解过,看过一些文章,自以为对其理解得比较清楚了。偶然刷到了一道关于ThreadLocal
内存泄漏的面试题,居然完全不知道是怎么回事,痛定思痛,发现了解问题的本质还是需要从源码看起。
ThreadLocal
可以保存一些线程私有的数据,从而避免多线程环境下的数据共享问题。ThreadLocal
存储数据的功能是通过ThreadLocalMap
实现的,这是ThreadLocal
的一个静态内部类。ThreadLocal
源码加注释总共700
多行,ThreadLocalMap
就占据了接近400
行,基本上理解了ThreadLocalMap
也就理解了ThreadLocal
。本文先简介ThreadLocalMap
,然后从ThreadLocal
的核心方法开始讲起,需要用到ThreadLocalMap
的地方顺带一起介绍。
1.ThreadLocalMap简介
ThreadLocalMap
本质上仍然是一个Map
,具有普通Map
的特点,则意味着ThreadLocalMap
可以保存多个ThreadLocal:value
的键值对,并且不同的ThreadLocal
对象可能会产生冲突。当遇到hash
冲突的时候,采用线性探测的方式来解决冲突,底层使用数组作为存储结构,它的主要字段如下:
INITIAL_CAPACITY
:初始容量,默认是16
Entry[] table
:存储键值对的数组,其大小是2
的整数幂size
:数组内存储的元素个数threshold
:扩容阈值
ThreadLocalMap
底层数组保存的是Entry
类型键值对,Entry
是ThreadLocalMap
的一个内部类,它是用来存储键值对的对象,值得关注的是Entry
继承了WeakReference
这个弱引用类,这意味着Entry的key
引用的对象,在没有其他强引用的情况下,在下一次GC的时候就会被回收(注意:这里忽略了软引用,因为软引用是在即将因为内存不足而抛出异常的时候才会回收)。并且Entry
的key
是ThreadLocal
对象,通过其祖父类Reference
的构造函数可以看到,key
实际上是被保存在referent
字段中,Entry
对象的get
方法也是从Reference
继承过来的,直接返回该referent
字段。
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//Entry的构造器调用了父类的构造,最终是通过Reference的构造器实现的
Reference(T referent) {
this(referent, null);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
//Reference的get方法
public T get() {
return this.referent;
}
在对Entry
有了初步了解后,现在来思考一下为什么key
要设计成弱引用呢?假设现在采有强引用来设计key
,考虑如下代码:
ThreadLocal<String> tl1 = new ThreadLocal<>();
tl1.set("abc");
tl1=null;
此时,相关的引用情况如下图:
tl1
虽然不再引用堆上的ThreadLocal
对象,但是线程的ThreadLocalMap
里还保留着对该对象的强引用,要获取该对象就需要ThreadLocal
对象作为key
,但是这个key
现在已经是null
了。也就是说,此时已经没有任何办法能够访问到堆上的TheradLocal
对象,但是由于还有强引用的存在,导致这个对象无法被GC
回收。这种情况显然不是我们希望看到的,因此Entry
的key
不能被设计为强引用。设计成弱引用是合理的,一旦外界的强引用被取消,就应当允许key
所引用的对象被回收。
2.ThreadLocal核心方法
get
get
方法用来获取存储在ThreadLocal
中的元素,其源码如下:
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;
}
}
//执行到这里的两种情况:1)map没初始化;2)map.getEntry返回null
return setInitialValue();
}
//从这里可以看到,每个Thread示例内部都有一个ThreadLocalMap类型的字段,线程局部变量就存在这个Map中
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//ThreadLocalMap的getEntry方法
private Entry getEntry(ThreadLocal<?> key) {
//计算key位于哪个桶
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
//执行到这里的两种情况:1)e=null,即桶内没有存数据;2)桶内有数据,
//但不是当前这个ThreadLocal对象的,说明产生了hash冲突,导致键值对被放到了其他位置
else
return getEntryAfterMiss(key, i, e);
}
线程能够保存私有变量的原因就在于其成员变量threadLocals
,每个线程都有这样的结构,互相不干扰。get
方法的代码很简单,根据从线程内取到的ThreadLocalMap
对象,如果ThreadLocalMap
还没初始化,则先初始化;如果已完成初始化,调用其getEntry
方法取元素,取不到的话,就会执行getEntryAfterMiss
方法(ThreadLocal
内部只在getEntry
方法里调用了getEntryAfterMiss
),先看看setInitialValue
方法的逻辑:
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//对应getEntry方法中的情况2:map已经初始化,但是对应的Entry为空的情,此时将键值对存入底层数组
if (map != null)
map.set(this, value);
//对应getEntry方法中的情况1:map没初始化
else
createMap(t, value);
return value;
}
//默认的initialValue返回null,而且该方法是protected,目的显然是让子类进行重写
protected T initialValue() {
return null;
}
setInitialValue
的逻辑很简单,假如map
没有初始化,执行createMap
方法进行初始化,否则将当前ThreadLocal
对象和null
构造成一个新的Entry
放入数组内。接下来看一下createMap
的初始化逻辑:
//可以看到,初始化的过程就是对Thread内部变量threadLocals赋值的过程,用到了ThreadLocalMap的构造器
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//
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);
}
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
//在对threshold初始化的时候使用了2/3作为系数
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
该方法通过ThreadLocalMap
的构造器对内部数组进行初始化,并将对应的值添加到数组中。可以看到,ThreadLocalMap
有容量的概念,但却没有办法指定其初始容量,在构造的时候使用固定值16
作为初始容量。稍后在rehash()
方法中将会看到,在判断是否需要扩容时,是以threshold*0.75
作为标准。
接下来看看getEntryAfterMiss
方法的源码:
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时,说明其对应的ThreadLocal对象已被GC回收,
//此时会通过expungeStaleEntry将一部分key为null的桶清空
if (k == null)
expungeStaleEntry(i);
//走到这里说明存在hash冲突,当前桶被其他元素占了,使用nextIndex向后找一个位置
else
i = nextIndex(i, len);
e = tab[i];
}
//如果e=null,在这里返回null
return null;
}
/nextIndex的主要作用是:查找下一个桶,如果到达末尾,则从头开始
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
//这里顺带将prevIndex的代码也放上来,与nextIndex方向相反
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
在桶内的key=null
时,会调用expungeStaleEntry
方法,从命名可以看出,这个方法主要功能是将ThreadLocalMap
内key=null
的部分元素清理掉,下面是对这个方法的讲解:
expungeStaleEntry
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
//分别将Entry的键和值清空
tab[staleSlot].value = null;
tab[staleSlot] = null;
//元素数量减1
size--;
// Rehash until we encounter null
Entry e;
int i;
//从当前下标的下一个位置开始遍历,清空key=null的桶,并更新hash冲突的元素的位置
//循环终止条件:顺序向后遍历时,找到一个非空的桶则循环终止,因此这里只是作了局部清理
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果key = null,则清空该桶
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
//h!=i说明当前元素是因为hash冲突,之前的桶被占了才放在了i这个桶内,
//那么就从其原来的位置h开始向后查找,找到第一个空桶,就把元素挪过去,
//目的是为了保证元素距离其正确位置最近,减少后续的查找成本
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
//返回值是第一个为空的桶的下标
return i;
}
expungeStaleEntry
方法的逻辑是:先将staleSlot
这个桶置空,然后从下一个位置开始遍历数组元素,遇到key
是null
的桶直接清理,并将长度减1
,遇到因为hash
冲突而放在后面位置的元素,则从该元素本来的位置开始,向后找到第一个空桶,然后把元素移动到这个空桶。expungeStaleEntry
的循环逻辑说明,在对失效元素进行清空时,不是清空所有失效的桶,而是从当前位置向后遍历,只要找到一个非空的桶,清理的过程就结束了。也就是说,这种清理只是部分清理,空桶后面的过期失效的桶无法得到清理。
set
介绍完get
方法后,现在再来看看set
方法的实现逻辑:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//map已创建则直接设值
if (map != null)
map.set(this, value);
//map未创建,则创建map
else
createMap(t, value);
}
来看看map
的set
方法是如何设值的:
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);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果当前ThreadLocal对应的有值,则更新
if (k == key) {
e.value = value;
return;
}
//如果k=null,说明对应的ThreadLocal对象已被GC回收,执行replaceStaleEntry的逻辑
if (k == null) {
//这里是replaceStaleEntry方法的唯一调用点,注意该方法执行完之后,set方法就返回了
replaceStaleEntry(key, value, i);
return;
}
}
//找到一个空位置,将值存进去
tab[i] = new Entry(key, value);
int sz = ++size;
//如果调用cleanSomeSlots没有清理任何桶,并且达到了扩容阈值,就执行扩容逻辑
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
set
的时候需要从对应的下标开始向后遍历,找到一个合适的位置将元素放进去,这里合适的位置是指:a)空桶;b)桶非空,但是key
对应的ThreadLocal
对象已被清理。在key
已经被清理的情况下,会执行replaceStaleEntry
方法的逻辑,接下来看看这个方法的代码:
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// 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).
int slotToExpunge = staleSlot;
//从staleSlot这个桶向前查找,遇到第一个空桶就停止
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
//如果桶内的key=null,说明该桶可以被回收,将slotToExpunge变量指向这个桶
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
//从当前桶向后遍历
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.
// k== key,说明这个ThreadLocal已经存在,但是距离正确的位置太远,需要对其位置进行更正
if (k == key) {
//更新value值
e.value = value;
//交换两个桶内的元素,i位置的元素本来就应该在staleSlot位置,但是hash冲突导致该元素放到了后面的位置,
//这里是把该元素换到正确位置。
//注意,replaceStaleEntry的唯一调用点出现在set方法内,此时staleSlot对应的桶的key=null,
//赋值语句 tab[i] = tab[staleSlot] 是将staleSlot位置上待清理的元素放在i位置,使得i位置变成过期元素。
//个人推测这里不直接赋值tab[i]=null的原因是让下一次expungeStaleEntry能够多清理一些空桶,
//如果这里设置为null的话,下一次清理到这个空位置就终止了
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
//下面的等式成立有两种情况:
//1)staleSlot前一个桶就为空,此时上文中前向遍历的循环体会直接结束;
//2)staleSlot前面的若干桶都不为空,且桶内的key!=null,即对应的ThreadLocal对象都没有被回收;
//出现这两种情况的时候,都只能从i这个位置开始进行清理
if (slotToExpunge == staleSlot)
slotToExpunge = i;
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.
//k!=null的时候,不能清理i这个桶;slotToExpunge != staleSlot时,
//说明在i这个位置之前就已经有需要清理的桶了,不能更新slotToExpunge这个指针的值
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
//执行到这里说明,直到遇到空桶,都没有在数组中找到key,就把新的键值对放在staleSlot的位置。
//需要注意的是,for循环到空桶就停止了,有可能当前这个threadlocal对象已经有键值对了,只是位置在这个空桶后面而已,
//因此这段逻辑可能会使得同一个key在数组中出现多次。
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
//slotToExpunge != staleSlot说明在其他位置找到需要清理的键值对,那么就从对应的位置清理
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
针对代码注释中的内容,有几点需要强调一下。首先replaceStaleEntry
这个方法唯一的调用点就是ThreadLocalMap
的set
方法内部,而且在调用replaceStaleEntry
的时候,参数staleSlot
对应的桶内的Entry
对象的key=null
。其次,replaceStaleEntry
的逻辑是从staleSlot
这个桶开始,先前向遍历,找到第一个空桶就停止遍历,期间如果发现某个桶内的key=null
,就将slotToExpunge
指针指向这个桶,表示下文要从这个桶开始进行过期键清理。前向遍历结束之后开始后向遍历,找到当前的ThreadLocal
对象所在的桶,将其位置更新,调用清理方法之后代码返回,否则就一直向后找直到遇到空桶。另外,根据分析,for
循环在遇到空桶就结束了,但是很可能对应的key
就在空桶后面,replaceStaleEntry
可能会造成同一个key
在数据中出现多次,本文最后将证明,key
是不可能出现多次的。
下面对replaceStaleEntry方法
的执行流程进行梳理。
假设在方法执行时,ThreadLocalMap
的存储结构如下所示:
首先前向遍历,遍历到LL
位置结束,由于在L
位置Entry.key=null
,所以设置slotToExpunge=L
;
接下来开始向后遍历,遍历到R1
位置时,虽然Entry.key=null
,但是由于slotToExpunge
的值已经被修改,不再对其进行赋值。代码接着遍历R2
位置,在这里找到了key
,因此将该位置的值与staleSlot
位置进行交换,如下图:
之后执行expungeStaleEntry
方法将LL
位置清空,然后从L
位置开始执行cleanSomeSlots
的逻辑。
replaceStaleEntry
方法内有两处用到了cleanSomeSlots
方法,接下来对其进行介绍:
//参数i是expungeStaleEntry()方法的返回值,是空桶的位置
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
//循环的逻辑是:从i的下一个位置开始找那些桶不空,但是桶内Entry对应的key=null的元素,
//然后从这些元素开始向后进行清空。循环的过程中会跳过空桶或者桶内元素的key!=null的桶,
//循环的次数由n的大小决定,每次将n减半,直到减为0循环结束。
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
//注意:expungeStaleEntry方法的返回值是第一个空桶的下标,循环的下一次会从这个下标的下一个位置开始遍历
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
//如果清理了元素,则返回true,否则返回false
return removed;
}
rehash
在set
方法内部最后几行,如果调用cleanSomeSlots
没有清理任何桶,并且达到了扩容阈值,就执行扩容逻辑,这段逻辑在rehash
方法中,来看看方法的实现逻辑:
private void rehash() {
//清理所有的过期桶
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
//清理过后剩余元素达到threshold的0.75才进行扩容,回忆一下ThreadLocalMap的初始化过程,
//初始化时threshold=2/3*初始容量,这里在判断是否要扩容时,是以threshold*0.75为标准
if (size >= threshold - threshold / 4)
resize();
}
//这个方法会从头开始遍历整个数组,每遇到一个ThreadLocal对象被回收的桶,就调用expungeStaleEntry方法向后清理一部分桶
//与上文讲到的其他清理方法不同,该方法会清理所有的过期数据
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);
}
}
//扩容逻辑
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
//注意这里并没有对newLen作限制,也就是说有超限的可能,但是一般肯定不会在线程内放这么多本地变量
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;
}
remove
最后来看一下ThreadLocal
的remove
方法,方法很简单,主要功能是将当前ThreadLocal
对象对应的键值对从数组中删掉,底层逻辑仍然是通过ThreadLocalMap
的remove
实现的:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
//计算key的位置
int i = key.threadLocalHashCode & (len-1);
//注意:for循环遇到空桶依然会结束,因此如果需要清理的ThreadLocal对应的键值对在空桶之后,就没法删除
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//如果找到key,则调用Reference类的clear方法,将referent置为null,然后从该位置开始向后清理一部分过期键值对
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
//Reference类的clear方法
public void clear() {
this.referent = null;
}
需要注意的是,for
循环遇到空桶就结束了,有可能清理不掉对应的键值对。
3.内存泄漏问题
ThreadLocal
的底层Entry
数组的key
是弱引用,意味着当ThreadLocal
对象的所有强引用都被去除后,对应的ThreadLocal
对象会被回收,此时Entry
的key=null
,但是value
还维持着堆上数据的强引用,只要当前线程不退出,这个强引用会一直存在。为了尽可能缓解这个问题,ThreadLocal
的get
、set
、remove
方法都会清除一批过期数据,但是从本文的分析可以看出,这种清理只是部分清理,仍然可能遗漏掉部分数据。因此这三个方法只能在一定程度上缓解内存泄漏的问题,并不能避免。另外,如果线程在较长时间内都没有执行上述方法,那过期的数据只会更多。那些在一段时间内都没被清理的过期value
对象仍会继续占用内存空间,这些未被清理的对象就是内存泄漏的源头。当然,过期的数据会在线程退出后全部销毁,但是当使用了线程池之后,线程用完会重复利用,并不会被销毁,这种情况下内存泄漏问题就不得不考虑了。因此,好习惯是在ThreadLocal
对象用完之后及时使用remove
方法进行删除,从而避免内存泄漏问题。
4.疑问总结
set
方法会不会造成同一个key
出现多次呢?
答案是不会。接下来就来详细讨论一下究竟replaceStaleEntry
会不会造成同一个key
出现多次的情况。假设重复多次的key
是k1
,其hash
值对应的位置为i
,在讲解之前,要明确以下两点:1)要使得k1
重复多次,就必然会存在hash
冲突,导致k1
放到了靠后的位置(否则set
方法就能够直接定位到k1
了);2)i
与k1
的实际位置i1
之间要有空桶(将空桶位置记为j
),否则按照上文的分析,代码逻辑也是能够最终定位到k1
的。而j
位置出现空桶的原因有以下三种:- 1)该位置本来就没有存放过键值对。这种情况要排除,因为如果
j
本来就是空的,k1
应该会直接放在j
位置。 - 2)显式调用了
remove
方法 - 3)显式将
k1
指向的ThreadLocal
对象赋值为null
,后文在进行方法调用的时候自动将j
桶清空
因此只需要考虑后面两种情况即可。如果是显式调用了remove
方法清空了j
桶,根据remove
的逻辑,代码会将i1
位置的键值对移动到j
位置,后续在使用set
方法对k1
对应的值进行修改时,由于i
和j
之间没有了空桶,可以定位到k1
。因此显式调用remove
是不会造成k1
重复的。再来看看第三种情况,由于GC清理弱引用导致j
位置变成了空桶,其他的方法在做清理时,底层都会调用expungeStaleEntry
方法,而这个方法会将i1
位置的键值对移动到j
位置。综上分析,在ThreadLocalMap
中不会出现同一个key
出现多次的情况。
- 1)该位置本来就没有存放过键值对。这种情况要排除,因为如果
5.修改日志
- 3.13验证同一个
key
不会在数组中出现多次。
ThreadLocal源码探究 (JDK 1.8)的更多相关文章
- ConcurrentHashMap源码探究 (JDK 1.8)
很早就知道在多线程环境中,HashMap不安全,应该使用ConcurrentHashMap等并发安全的容器代替,对于ConcurrentHashMap也有一定的了解,但是由于没有深入到源码层面,很多理 ...
- CountDownLatch源码探究 (JDK 1.8)
CountDownLatch能够实现让线程等待某个计数器倒数到零的功能,之前对它的了解也仅仅是简单的使用,对于其内部如何实现线程等待却不是很了解,最好的办法就是通过看源码来了解底层的实现细节.Coun ...
- ReentrantReadWriteLock源码探究
ReentrantReadWriteLock实现了可重入的读锁和写锁,其中读锁是共享锁,写锁是互斥锁.与ReentrantLock类似,ReentrantReadWriteLock也提供了公平锁和非公 ...
- CyclicBarrier源码探究 (JDK 1.8)
CyclicBarrier也叫回环栅栏,能够实现让一组线程运行到栅栏处并阻塞,等到所有线程都到达栅栏时再一起执行的功能."回环"意味着CyclicBarrier可以多次重复使用,相 ...
- ThreadLocal源码分析:(三)remove()方法
在ThreadLocal的get(),set()的时候都会清除线程ThreadLocalMap里所有key为null的value. 而ThreadLocal的remove()方法会先将Entry中对k ...
- 并发-ThreadLocal源码分析
ThreadLocal源码分析 参考: http://www.cnblogs.com/dolphin0520/p/3920407.html https://www.cnblogs.com/coshah ...
- Java -- 基于JDK1.8的ThreadLocal源码分析
1,最近在做一个需求的时候需要对外部暴露一个值得应用 ,一般来说直接写个单例,将这个成员变量的值暴露出去就ok了,但是当时突然灵机一动(现在回想是个多余的想法),想到handle源码里面有使用过Th ...
- ReentrantLock源码探究
ReentrantLock是一种可重入锁,可重入是说同一个线程可以多次获取同一个锁,内部会有相应的字段记录重入次数,它同时也是一把互斥锁,意味着同时只有一个线程能获取到可重入锁. 1.构造函数 pub ...
- 面试官:小伙子,听说你看过ThreadLocal源码?(万字图文深度解析ThreadLocal)
前言 Ym8V9H.png (高清无损原图.pdf关注公众号后回复 ThreadLocal 获取,文末有公众号链接) 前几天写了一篇AQS相关的文章:我画了35张图就是为了让你深入 AQS,反响不错, ...
随机推荐
- rest framework-序列化-长期维护
############### 表结构 ############### from django.db import models class Book(models.Model): titl ...
- 直击LG曲面OLED首发现场,高端品质更出众
简直是太棒了,我可以去看LG曲面OLED电视新品发布会了.这可是LG向中国首次推出的曲面OLED电视.在网上我就已经看到其实曲面OLED电视已经在韩国.美国还有欧洲都上市了,听说现在反响还挺不错.真没 ...
- Java类的三大特征
1.三大特征是封装.继承和多态 2.封装 特点: 需要修改属性的访问控制符为private: 创建getter/setter方法用于属性的读写: 在getter/setter方法中加入属性控制语句,用 ...
- VisualStudioAddin2016Setup.rar
本工具是用于Visual Studio 2010 /2012 的外接程序. 功能不太多,常用代码,引用管理等. 动态图: 下载地址: VisualStudioAddin2016Setup.rar
- Laravel5.4 队列简单配置与使用
概述 什么是队列? 百度百科是这样说的 “队列”是在传输过程中保存数据的容器. 举几个生活中例子: * iphone手机新款发布,三里屯iphone进的新货.大家要排队买,不能说一大堆人一起冲进去,那 ...
- 吴裕雄--天生自然python学习笔记:python文档操作插入图片
向 Word 文件中插入图片 向 Word 文件插入图片的语法为: 例如,在 cl ip graph.docx 文件的第 4 段插入 ce ll.jpg 图片,井将图片文件保存于 Word 文件内: ...
- deeplearning.ai 序列模型 Week 3 Sequence models & Attention mechanism
1. 基础模型 A. Sequence to sequence model:机器翻译.语音识别.(1. Sutskever et. al., 2014. Sequence to sequence le ...
- git获取公钥和私钥以及常用的命令
Git简单生成公钥和私钥的方法 Git安装完之后,需做最后一步配置.打开git bash,分别执行以下两句命令 git config --global user.name “用户名” git conf ...
- ffmpeg直播系统
1.HLS协议 http live streaming 将本地文件或者摄像头视频转成hls流文件 https://www.ffmpeg.org/ffmpeg-all.html#hls-2 2.rtmp ...
- 吴裕雄--天生自然操作系统操作笔记:window10显示隐藏文件夹
基于安全考虑,操作系统会隐藏一些文件和文件夹,防止误删除操作.但有可能是个别人为了隐藏一些私密数据,也同样采取隐藏的方式.