JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序,ThreadLocal并不是一个Thread,而是Thread的局部变量
ThreadLocal可以解决
- 多线程并发问题
- 数据传递
- 线程隔离
ThreadLocal与Synchronized关键字的区别
- ThreadLocal和Synchronized都可以解决多线程并发问题。
- ThreadLocal采用的是以空间换时间,每个线程都提供一个变量副本,从而实现访问互不干扰。
- Synchronized采用的是以时间换空间的方式,只提供一个变量让线程进行排队访问。
- ThreadLocal可以提供更好的并发性
ThreadLocal内部结构
早先jdk设计ThreadLocal是,每一个ThreadLocal都会创建一个Map,当前线程(Thread)作为Map的key,要存储的局部变量作为map的value,这样就能达到各个线程局部变量隔离。
jdk8 ThreadLocal是,每一个Thread维护一个ThreadLocalMap,Map的key就是ThreadLocal,要存储的局部变量作为map的value,来能达到各个线程局部变量隔离。
jdk8 ThreadLocal优化的好处:
- 每个Map存储的Entry数量变少,Hash碰撞随之减少。
- Thread销毁之后,ThreadLocalMap也会随之销毁,减少内存开销。
ThreadLocal不是封装在对象内,而是放在线程中
public T get() {
Thread t = Thread.currentThread();
// 从线程Thread中获得ThreadLocal,再ThreadLocal中获得ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// key:ThreadLocal,获取vlaue
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
/**
* 初始化
* 1.map不存在,当前线程没有关联ThreadLocalMap 对象
* 2.map存在,但是没有与当前ThreadLocal关联的entry
* */
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
// 从线程Thread中获得ThreadLocal,再ThreadLocal中获得ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 创建一个Map,并设置初始值
createMap(t, value);
}
threadLocals是一个ThreadLocal.ThreadLocalMap对象,具有很多Map的特性
// 获取当前线程Thread对应维护的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* 创建当前线程Thread对应维护的ThreadLocalMap
* @param t 当前线程
* @param firstValue 存在在map中的第一个entry值
* */
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- ThreadLocalMap是ThreadLocal的一个静态内部类,ThreadLocalMap内部持有一个Entry数组默认大小16,threadLocal本质上实现了一个简易的map
// Entry初始化容量--必须是2的整数幂,有利于散列分布,减少hash碰撞 private static final int INITIAL_CAPACITY = 16; // 存放数据的entry private Entry[] table; // 进行扩容的阀值,默认 len * 2 / 3 = 10 private int threshold;
- Entry是一个继承了WeakReference的键值对,key是ThreadLocal变量的一个引用,value是保存的值
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
弱引用与内存泄漏
- 内存泄漏
- Memory overflow:内存溢出,没有足够的内存空间提供申请者使用。
- Memory leak:内存泄漏指程序中以动态分配的堆内存由于某种原因程序未释放,造成系统内存的浪费,导致程序运行速度慢,甚至系统崩溃等,内存泄漏的堆积将导致内存溢出。
- 弱引用
- java中的引用类型有4种:强、软、弱、虚,当前主要涉及强引用和弱引用。
- 强引用("Strong"Reference),就是我们最常见的普通对象应用,只要有强引用指向一个对象,就能表明对象还"活着",垃圾回收器就不会回收这种对象。
- 弱引用(WeakReference),垃圾回收器一旦发现,只有一个弱引用的对象,不管当前内存空间是否足够,都会回收它。
- ThreadLocalMap key使用弱引用,如何减少内存泄漏?哪里内存泄漏?
- key使用强引用:引用的ThreadLocal被回收了,但是ThreadLocalMap的ThreadLocal还持有强引用,如果没有手动回收就会存在内存泄漏。
- key使用弱引用:引用的ThreadLocal被回收了,由于ThreadLocalMap只是持有ThreadLocal的弱引用,如果没有手动删除,在下次回收时会被销毁(key),避免了内存泄漏。
- 使用弱引用是不是就没有内存泄漏?当然不是,前面只是把key回收了,整个Entry还没有被回收。
- 使用弱引用ThreadLocal内存泄漏的根本原因是:ThreadLocalMap的生命周期和Thread一样长,如果没有手动删除对应key就会导致内存泄漏,但是弱引用比强引用多一层保障,在下次调用ThreadLocalMap的get、set、remove、rehash等方法都会主动清理key为null的Entry。
ThreadLocalMap hash冲突
ThreadLocalMap 中的set方法
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();
// 如果key存在,直接覆盖之前的值
if (k == key) {
e.value = value;
return;
}
// key是空,但是值不为空,说明ThreadLocal已经被回收,当前value需要清理
// 当前数组中的Entry是一个旧的(stale)元素
if (k == null) {
// 用新元素替换旧的,并且进行一些垃圾清理工作,防止内存泄漏
replaceStaleEntry(key, value, i);
return;
}
}
// 对应的key不存在,并且没有陈旧的元素,则创建一个新的Entry
tab[i] = new Entry(key, value);
int sz = ++size;
/**
* cleanSomeSlots用于清理e.get()==null的元素,这种key关联的对象已经被回收,
* 所以Entry(table[index])可以置为空,如果没有清理任何entry,并且当前使用负载因子
* 达到长度2/3,进行rehash(执行全表扫描清理工作)
* entry size个数达到(size >= threshold - threshold / 4)进行扩容,Entry扩容为原先的2倍
**/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
// 获取环形数组的下一个索引
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
private void rehash() {
xpungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
ThreadLoalMap使用线性探测法来解决哈希冲突,该方法一次探测下一个地址,直到有空的地址插入,如果整个空间都找不到空余的地址,则产生溢出。