世界上只有两句真理:1、人一定会死。2、程序一定有Bug。
昨天学习了java中的线程Thread对象的生命周期,以及中断、线程礼让、线程加入以及三个过期方法的了解。
在Thread中有两个成员,
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
可以看到这两个成员都是ThreadLocal.ThreadLocalMap
见名知义:
可以猜到这是本地存储map,并且是与线程绑定的map,并且是线程私有,并不会被多个线程共享,因为每个Thread里都有独立的成员。
class ConnectionManager {
private static Connection connect = null;
public static Connection openConnection() {
if(connect == null){
connect = DriverManager.getConnection();
}
return connect;
}
public static void closeConnection() {
if(connect!=null)
connect.close();
}
}
如上是一个数据库连接管理类,由于没有加锁,在多线程环境下,可以出现多次初始化connection或者一个在操作,另一个线程却close 连接的情况。
这里需要好好想一想,这里真的需要共享连接吗,其实这里使connection私有就可以了,这里就可以用到ThreadLocal
static ThreadLocal local = new ThreadLocal<>();
public static void main(String[] args) {
local.set(1);
System.out.println(local.get());
}
注意main函数本身也是个线程,这个local的使用看起来好像就只是个存储变量的东西,先放置1进去,再取出来,得到1.
可是奇怪了,前面提到Thread中的是ThreadLocalMap,这里写的案例是ThreadLocal,第一次看到ThreadLocal我也挺疑惑的,下面来看看这到底是什么东西?
Thread里维护了一个ThreadLocalMap threadLocals,
这是一个线程私有的map,底层是一个Entry数组,Entry的key是ThreadLocal对象,value是对应的值。注意这里的key ThreadLocal是一个弱引用。所以会存在一些问题。下面再说。
先来细看ThreadLocal的set源码:
public void set(T value) {
//取到当前线程
Thread t = Thread.currentThread();
//取到当前线程的threadLocals对象
ThreadLocalMap map = getMap(t);
//如果不为空就设置覆盖原值
if (map != null)
map.set(this, value);
else
//否则创建map
createMap(t, value);
}
//createMap调用到ThreadLocalMap的构造器
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
//创建entry数组,初始化长度16
table = new Entry[INITIAL_CAPACITY];
//找到对应的索引位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//把entry设置到对应位置
table[i] = new Entry(firstKey, firstValue);
//长度是1
size = 1;
setThreshold(INITIAL_CAPACITY);
}
来看下具体的set方法
private void set(ThreadLocal key, Object value) {
//取到map的 entry数组
Entry[] tab = table;
//取到数组长度
int len = tab.length;
//算出存的key 索引
int i = key.threadLocalHashCode & (len-1);
//循环entry数组,直接找到一个空的entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
//如果找到了这个key,就直接设置并返回
if (k == key) {
e.value = value;
return;
}
//如果发现key为空,说明被回收了,那就
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//在空的位置 放置新的entry
tab[i] = new Entry(key, value);
//size+1
int sz = ++size;
//清理
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
下面来看下 替换entry replaceStaleEntry
private void replaceStaleEntry(ThreadLocal key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//在key为空的节点处往前找,一直到entry为空,找到最前面一个key空null 的entry
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
//一直往后面找,直到出现空的entry
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal k = e.get();
//如果找到了一样的key,就改值,并把他与之前 根据这个key定位到的entry交换。
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//如果slotToExpunge跟staleSlot相等,说明在前面是没有key为null的entry,就设置它为现在的i,需要清理
if (slotToExpunge == staleSlot)
slotToExpunge = i;
//清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
//如果后面没有key为null的entry,就直接设置在之前的entry位置
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
//如果两者不一样,说明找到了最前面的null,就清理
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
//如果一样,说明前后都是空的key,也就不需要清理什么了
}
至于上面的代码里找到了相同的key以后为啥要交换,有一篇博客讲的很清楚,直接分享出来:
https://www.linuxidc.com/Linux/2018-01/149993.htm
下面来看下 expungeStaleEntry 和 cleanSomeSlots
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 e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal k = e.get();
//如果key为空就清理
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//如果不为空就重新计算位置
int h = k.threadLocalHashCode & (len - 1);
//如果位置跟原来不一样就要交换
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale. //找到离h往后找最近的非空位置给他存进去
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
//返回的这个i 是开始为null entry的第一个位置
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];
//如果出现非空且key为null的entry就清掉
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
再看下rehash方法
private void rehash() {
//先清理一波
expungeStaleEntries();
//当size大于等于容量四分之三就resize
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
/**
* Double the capacity of the table.
*/
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();
//k为空的话,就设置value为空,杜绝内存泄露
if (k == null) {
e.value = null; // Help the GC
} else {
//重算索引
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
//把对应的entry放在离重新计算的索引 往后找最近的一个空位置上
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
上面是set方法 的详细执行方法:
简单来说,在set数据进来的时候,会先去对应的entry数组下找到hash得到的索引位置,如果这个位置有值,就一直往后找,直到找到一个空的位置塞进去,这是解决hash冲突的一种方法,线性探测法。在set的同时会去排查key为null的entry,把它清理掉。
至于get方法太简单了,不说了,自己体会
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
ThreadLocal这么好用,有什么缺陷呢?
由于ThreadLocal中的ThreadLocalMap
中entry保存的key对象是弱引用,即在GC过程中必然被回收的对象,所以可以出现entry不为空,key为空,value不为空的情况,这种时候这个value值永远不能拿到,造成内存泄露。
ThreadLocal在set方法的时候做了很多判断去清理这部分可能造成泄露的数据,但是可能还是会有泄露的情况。。
注意这里的前提条件是不能传参 ,不能static
这是一道面试题,当时没有答出来。
我们知道ThreadLocal是线程本地存储变量,他是线程私有的,所以讲道理是不可能通过ThreadLocal在线程间传递参数。先来看下是不是这样呢?
static ThreadLocal local = new ThreadLocal<>();
public static void main(String[] args) {
local.set(1);
System.out.println("father :"+local.get());
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("child :"+ local.get());
}
}).start();
}
结果:
father :1
child :null
可以看到,确实不行。
那么怎么实现呢?
回头看看Thread的成员,既然要获取父线程的数据,那Thread是不是有哪个成员可以保存父线程的数据呢?答案是inheritableThreadLocals。
这个对象的实例其实是InheritableThreadLocal.ThreadLocalMap
来试下,把上面代码中的ThreadLocal换成InheritableThreadLocal。
可以看到子线程确实获取了父线程的1.
如何实现的呢?
1、看InheritableThreadLocal的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);
}
//第一次执行时
//这时候createMap方法调用到
//InheritableThreadLocal的createMap
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
//可以看到确实给Thread的inheritableThreadLocals赋值了,那这个inheritableThreadLocals如何传递给子线程呢
那就要看昨天的Thread的init方法了
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//当父线程inheritableThreadLocals不为空时,子线程根据这个map构造器自己的map,其实是copy数据,copy的是引用,也就是浅拷贝