文章目录
1.ThreadLocal简介
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题,如下图所示
2.ThreadLocal的简单使用
package test;
public class ThreadLocalTest {
static ThreadLocal<String> localVar = new ThreadLocal<>();
static void print(String str) {
//打印当前线程中本地内存中本地变量的值
System.out.println(str + " :" + localVar.get());
//清除本地内存中的本地变量
localVar.remove();
}
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程1中本地变量的值
localVar.set("localVar1");
//调用打印方法
print("thread1");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程1中本地变量的值
localVar.set("localVar2");
//调用打印方法
print("thread2");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
});
t1.start();
t2.start();
}
}
3.ThreadLocal的内部设计
3.1 早期的设计
3.2 现在的设计
3.3 优化后的好处
1:Map的数量的entry减少啦: 因为之前的map的entry 是有多少个Thread决定的,而JDK1.8之后,map的entry的key 是由ThreadLocal作为key,从前面的代码,我们可知道,上面我们的声明了2个线程,但是只用了1个ThreadLocal,实际代码中ThreadLocal的数量要小于线程的数量的
2:当Thread销毁的试试,ThreadLocal也会随之销毁: 因为Thread是作为ThreadLocal的一个整体,多个ThreadLocal组成为了TheadlocalMap作为Thread的一个变量,如果对象都没有啦,对象的变量也会消失
4.ThreadLocal的核心源码
4.1 set方法
public void set(T value) {
//(1)获取当前线程(调用者线程)
Thread t = Thread.currentThread();
//(2)根据当前线程,得到当前县城对应的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//以map不为null当前的ThreadLocal为key,变量为value为进行赋值
map.set(this, value);
//(4)如果map为null,说明首次添加,需要首先创建出对应的map,就是new个map,然后进行上面set的操作
else
createMap(t, value);
}
//getMap
ThreadLocalMap getMap(Thread t) {
/**得到当前线程的ThreadLocalMap**/
return t.threadLocals;
}
//threadLocals属性的讲解
ThreadLocal.ThreadLocalMap threadLocals = null;
//createMap代码
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
总结:
4.2 get方法
public T get() {
//1:获取当前的线程
Thread t = Thread.currentThread();
//2:获取线程维护的map
ThreadLocalMap map = getMap(t);
if (map != null) {
//3:如果存在,以当前的对象为key,得到ThreadLocal对应的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
//4:如果entry不为空,获取对应的值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//5:
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
4.3 Remove方法
public void remove() {
//获取当前线程维护的ThreadLocal
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//如果存在,把对应key对应的entry移除掉
m.remove(this);
}
4.4 setInitialValue方法
protected T initialValue() {
return null;
}
默认是null。但是我们可以重写此方法,进行初始值的赋值
4.ThreadLocalMap源码解析
4.1整体结构
4.2 成员变量
4.3 存储结构
5:弱引用和内存泄漏
更为详细的请参考这位大佬的博客
https://www.cnblogs.com/javaee6/p/4763190.html
注意:如果弱引用被一个强引用所引用,他仍然保持弱引用的特性
5.1 如果使用强引用会内存泄漏吗?
1:上面ThreadLocalRef he CurrentThreadRef 分别代码ThreadLocal和CurrentThread的引用,他们分别存储在栈里面
5.2 如果使用弱引用会内存泄漏吗?
5.3 出现内存泄漏的真实原因
5.4 JDK为什么最后选择弱引用呢?
6.ThreadLocalMap 没有Map中的链表,如何解决hash冲突?
6.1 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
//计算索引
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
//entry存储的个数,因为是构造函数,所以是1
size = 1;
setThreshold(INITIAL_CAPACITY);
}
===========================threadLocalHashCode 相关代码======================================
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
//HASH_INCREMENT 是为了让hash均匀的分布,尽量避免
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
==========================INITIAL_CAPACITY - 1)============================================
学过HashMap的都知道,容量被设置为2的n次方,当进行-1的时候,此时2进制的最后一位都是1,此时相当于提高了效率,比如此时1和0进行4预算,此时如果为1的话,后面基本上就不用看啦
6.2 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对应的key存在,覆盖之前的值
if (k == key) {
e.value = value;
return;
}
//如果不存在,说明被回收啦,当前数据中entry是一个陈旧的元素
if (k == null) {
//用新元素代替旧元素,这个方法进行了不少清理垃圾的动作,防止内存泄漏
replaceStaleEntry(key, value, i);
return;
}
}
//如果key不存在,并且没有找到陈旧的元素,则在空位置上进行新创建新的entry
tab[i] = new Entry(key, value);
int sz = ++size;
//清除e.get() = null的元素
//这种数据关联key关联的对象,已经被回收啦,所以entry(table (index)可以设置为null
//如果没有清楚任何的entry,并且当前的使用量达到了负载银子,那么进行rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
总结:
和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。
所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能