ThreadLocal主要是为了解决线程安全性问题的
非线程安全举例
public class ThreadLocalDemo {
// 非线程安全的
private static final SimpleDateFormat sdf= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static Date parse(String strDate) throws ParseException {
return sdf.parse(strDate);
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 20; i++) {
executorService.execute(() -> {
try {
System.out.println(parse("2021-05-30 20:21:20"));
} catch (ParseException e) {
e.printStackTrace();
}
});
}
}
}
以上代码,构造了一个SimpleDateFormat对象,然后在main线程中,开启了20个线程执行时间的格式化,其输出结构部分如下:
可以看到,程序有部分线程打印出了结果,但结果也都不一样,并且也有部分报了异常。
分析:这是由于SimpleDateFormat在这里设置的是一个全局变量,多个线程共用的时候,必然涉及到共享资源的抢占,其parse方法内部对字符串的处理的操作就是非原子性的,因此就会出现真正执行的时候,拿到的最终字符串无法确定,导致以上报错。
思考: 如何修改? 使用ThreadLocal将DateFormat设置为线程安全的,保证每个线程的操作都是原子的。
ThreadLocal应用
public class ThreadLocalDemo {
private static final ThreadLocal<DateFormat> dateFormatThreadLocal = new ThreadLocal<>();
public static Date parse(String strDate) throws ParseException {
DateFormat dateFormat = dateFormatThreadLocal.get();
if (dateFormat == null ) {
dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
dateFormatThreadLocal.set(dateFormat);
}
return dateFormat.parse(strDate);
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 20; i++) {
executorService.execute(() -> {
try {
System.out.println(parse("2024-04-12 15:12:20"));
} catch (ParseException e) {
e.printStackTrace();
}
});
}
}
}
结果输出
ThreadLocal常用方法
set()
在当前线程范围内,设置一个值存储在ThreadLocal中,这个值仅对当前线程可见
相当于在当前线程范围内建立了副本
get()
从当前线程范围内取出set()方法设置的值
remove()
移除当前线程范围内的值
ThreadLocal原理猜想
1. 能够实现线程的隔离,当前保存的数据,只会存储在当前的线程范围内->线程内私有的
2.有一个存储结构
3.key->保存当前线程
ThreadLocal源码分析
1.初始化ThreadLocalMap
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
作为一个静态内部类,在类加载时,就会加载该线程的一个ThreadLocalMap.Entry
注意:在这里Entry采用的是一个弱引用对象,为什么要采用弱引用对象呢?这是由于ThreadMap是和线程绑在一起的,如果这个线程没有被销毁,而我们又已经不会在使用ThreadLocal引用了,那么key-value的键值对就会一直在map中存在,这对于程序来说,就出现了内存泄漏。为了避免这种情况,只要将Key设置为弱引用,那么当发生GC的时候,就会自动的把弱引用给清理掉了。
2. set主逻辑源码
public void set(T value) {
// 1. 获得当前线程t
Thread t = Thread.currentThread();
// 2. 取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 2.1 如果map不为空, 则设置当前ThreadLocal变量的value值
map.set(this, value);
} else {
// 2.2 若map为空,则创建一个ThreadLocalMap
createMap(t, value);
}
}
1.2 当前线程的map不为空时,如何进行set值
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 2.1.1 根据当前key获得索引值
int i = key.threadLocalHashCode & (len-1);
// 2.1.2 循环entry数组
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 2.1.3 entry不为空 获得当前entry数组元素的key
ThreadLocal<?> k = e.get();
// 2.1.4 若相等,则赋值value
if (k == key) {
e.value = value;
return;
}
// 2.1.5若当前entry中为空 则进行替换空余的数组
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 2.1.6 这里主要是对Entry数组的一个扩容知识
rehash();
}
分析:当前entry为空(key值可能被GC回收了),那么该条数据就可能为脏数据,脏Entry,只有value有值 key为null 就执行replaceStaleEntry方法(注:2.1.6在这里不再展开,主要是Entry数组的一个扩容)
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// a.当前key的索引
int slotToExpunge = staleSlot;
// b. 向前查询脏Entry
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// c. 向后查找可覆盖的Entry
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
// d.进行交换 避免Entry中数据重复
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// e.从前往后清理脏Entry
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// f. 没有找到可覆盖的Entry,则清理当前索引的value重新赋值
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// h.清理查询到的脏Entry
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
该段代码其实会分为4种情况
a. 向前查找有脏Entry 向后查找到可覆盖的Entry(这里是由于存储的时候会有哈希冲突,因此向后可能会有相同的key值)
b.向前有脏Entry向后未找到可覆盖的Entry(则直接在当前索引位置直接赋值新的Entry)
c.向前没有脏Entry向后找到可覆盖的Entry
d.向前没有脏Entry向后未找到可覆盖的Entry
分析:上述set值,一定程度上避免了Entry数组的内存泄漏,因为可以向前检索到脏Entry并进行清理,但是如果向前查找提前停了下来,那么前面仍还有脏Entry未扫描到,那么仍会有部分内存泄漏。真正全部清理需要每次使用后调用remove方法
1.3 当前线程的Map为空时,进行创建并set值
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 2.2.1 初始化一个16长度大小的数组
table = new Entry[INITIAL_CAPACITY];
// 2.2.2 设置下标索引 采用的是线性探测法
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 2.2.3 将Entry设置到该数组索引处
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 2.2.4 设置阈值为10
setThreshold(INITIAL_CAPACITY);
}
设置索引的地方主要是采用线性探测法来解决哈希冲突,找到一篇不错的博客,感兴趣可以参考博客:ThreadLocalMap线性探测法解决hash冲突_thread t = thread.currentthread(); threadlocalmap -CSDN博客
3. get主逻辑源码
public T get() {
Thread t = Thread.currentThread();
// 1.首先获得当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 2.拿到Entry如何Entry不为空的情况下 直接返回值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 3. 如果弱引用key被回收了,则会重新创建当前线程的Entry,并赋值
return setInitialValue();
}
4.remove主逻辑源码
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) {
// 遍历当前Entry数组,全部清理掉
e.clear();
expungeStaleEntry(i);
return;
}
}
}
ThreadLocal总结
1.ThreadLocal主要是为了线程安全,避免多线程的资源共享,线程间的资源互相隔离
2..ThreadLocal的注意点: ThreadLocal可能会造成内存泄漏,因此在每次使用完后,调用remove进行清理
3.为什么ThreadLocal的key值是弱应用,而value值是强引用? 在ThreadLocalMap初始化时已经说明了key值为什么要采用弱引用,那么value值为什么不能设置为弱引用呢。假设Entry的key所引用的ThreadLocal对象还被其他的引用对象强引用着,那么这个ThreadLocal对象就不会被GC回收,但如果value是弱引用且不被其他引用对象引用着,那么GC的时候就会被回收掉了。那线程通过ThreadLocal获取value的时候就会获得null,ThreadLocal显然就是用来关联value的,value才是我们要保存的值,如果value都没了,还用ThreadLocal干嘛。所以value不能是弱引用