问题一. 同一个线程方法之间的传参方式有那些?
1. 通过方法参数进行传参,与业务逻辑无关的通过参数,不方便。
2. 通过公共变量进行传参 如: static object param = null,这样写有很多问题,如并发问题,gc问题,几乎不这样写。
ThreadLocal按照第二种解决方式方式进行了优化。
优化:把object 放在了Thread类里面,这样object的生命周期就可以依赖Thread的生命周期,因为是线程的内部属性,天生线程安全。
问题二:线程中有多个参数的化怎么办,是不是要创建多个object属性呢?
很自然联想到用Map, 那key又如何解决呢? 就这样引入了ThreadLocal类, ThreadLocal一个最主要作用就是组map中的key。
ps: ThreadLocal取名不直观,不能见名知意,个人觉得改为ThreadParamKey之类的更加合适。
问题三:Thread与ThreadLocal两个类是如何配合的?
talk is cheap, picture is visual;
如上图,Thread持有ThreadLocalMap, ThreadLocal作为ThreadLocalMap的key(其实是数组中单个对象的一个属性, 和hashMap实现逻辑类似)。
下面上demo:
public class ThreadLocalTest {
static ThreadLocal<String> localString = new ThreadLocal<String>();
static ThreadLocal<Integer> localInteger = new ThreadLocal<Integer>();
public static void main(String[] args) {
System.out.println("Thread name :" +Thread.currentThread().getName());
Thread t1 = new Thread(new Runnable() {
public void run() {
System.out.println("start");
//设置线程1中本地变量的值
localString.set("a");
localInteger.set(1);
}
},"mythread");
Thread t2 = new Thread(new Runnable() {
public void run() {
//设置线程2中本地变量的值
localString.set("b");
localInteger.set(2);
}
});
t1.start();
t2.start();
}
}
看下在内存中的数据结构:
一图顶万言。
参照上图,看源码应该很容易理解get与set的操作流程了。
接下来看几个有意思的实现细节。
1.Entry类
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entery的key实现了WeakReference,为什么要实现呢?
WeakReference: 一个具有弱引用的对象,与软引用对比来说,前者的生命周期更短。当垃圾回收器扫描到弱引用的对象的时候,不管内存空间是否足够,都会直接被垃圾回收器回收。不过也不用特别担心,垃圾回收器是一个优先级比较低的现场,因此不一定很快可以发现弱引用的对象。
上面的例子会出现Entry中key为null的现象吗?
弱引用是对ThreadLocal对象来说的,当只有弱引用只想ThreadLocal时,gc的时候就会把该出现清除掉。上面的例子localString和localInteger两个对象是静态的,属于类级别,不会被gc掉。
如果localString已经被gc掉了,说明localString对象已经不引用该对象了,localString.get()不会调用了,调用的话也会保npe了,所以Entery的key实现了WeakReference。
弱引用是为了防止ThreadLocal对象内存溢出。
2.使用ThreadLoca有内存溢出的风险,到底指的是什么溢出,什么情况下才溢出。
从图中可以看出,如果线程是朝三暮四的化,是不可能有能存溢出的。
内存溢出实际上是ThreadLocalMap中的table一直不销毁,而且一直在增加导致的。
使用remove()方法可以化解改内存溢出风险。
3.remove()的到底是什么东西呢?
public void remove() {
//拿到ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
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) {
//设置key为null
e.clear();
//正在去清空table
expungeStaleEntry(i);
return;
}
}
}
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();
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.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
流程如下:
1.获得threadLocalMap
2.找到Entry在table数组中的位置
3.把Entry中的key设置为null
4.把Entry中的val设置为null
5.table中对应的Entry设置为null
6.rehash table
上面5步很好理解, 为什么最后要rehash呢。
接下来我看看下ThreadLocalMap是如何存储呢?
Map中出现hash冲突的时候如何解决的?
1.链表(包括树结构),HashMap实现
2.线性探测
key4计算key4.hash&(table.length-1),位置已经被可用暂用了, 所以往后需要空余的位置直接放进去。
rehash做的就是,如果key3被remove了,就应该把key4移到key3的位置。
为什么ThreadLocal有private static final int HASH_INCREMENT = 0x61c88647; 和private static AtomicInteger nextHashCode = new AtomicInteger();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
HASH_INCREMENT增加hash的离散度,单hash冲突的时候时会,减少线性探测次数,提高性能
get和set的流程