ThreadLocal你了解么?
什么是ThreadLocal
其实对于ThreadLocal并不陌生,在多线程的场景下,应该是都有使用的, ThreadLocal实际上一种线程隔离机制,也是为了保证在多线程环境下对于共享变量的访问的安全性。线程私有的,那么也就存在线程共享。
1、因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
2、既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
3、ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收
ThreadLocal使用场景
个人认为技术的选择一定是基于某个业务场景下的。脱离了业务为背景的技术都是空谈的,都是不缺实际的。因此也就没有所谓的技术过时一说,只是说某个技术不适用了当前的这个业务场景了。所以说选择什么样的技术,一大部分是取决于业务场景的。就那个ThreadLocal来说,首先必要的就是淡当然要了解该技术的原理了。
1、每个线程需要有自己单独的实例
2、实例需要在多个方法中共享,但不希望被多线程共享
用法
入门才是关键,对于一个新的知识,先入门才能更好研究(在心里找到平衡),只有先通过使用,结合其特点,在来研究为啥会有这样的效果?
说白了就是,每个单独的线程独自管理自己线程中的内容
package com.yypdemo.example.ThreadLocalDemo;
/**
* @author lamb-yyp
* @version 1.0
* @date 2020/9/13 13:28
*/
public class Demo1 {
static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
private static void setThreadLocal(){
threadLocal.set(100);
}
public static void main(String[] args) {
// Demo1.setThreadLocal();
Thread[] threads = new Thread[5];
for(int i=0;i< 5; i++){
Thread thread = new Thread(()->{
Demo1.setThreadLocal(); //初始化 (或者写在静态类中)
int num = threadLocal.get(); // 线程独享
threadLocal.set(num+1); System.out.println(Thread.currentThread().getName()+" - "+ threadLocal.get());
});
threads[i] = thread;
thread.setName("线程"+i);
}
for(int i=0;i< 5;i++){
threads[i].start();
}
}
}
执行结果
基本原理
说到原理,我们还是根据用法上来作为突破口即(set,get方法)
set方法 (会判断是否有初始化化数组)
初始化()
数组初始化(第一次进来,惰性加载)
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { // INITIAL_CAPACITY = 16
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 通过hash函数,计算出下标i
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 计算出扩容因子16*0.75 = 12
setThreshold(INITIAL_CAPACITY);
}
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be 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)]) { //从i 往后遍历,遍历到最后一个Entry(纯粹的线性探索,相对比较耗时)
ThreadLocal<?> k = e.get();
if (k == key) { // 找到相同的,覆盖值
e.value = value;
return;
}
if (k == null) { // 如果为null ,用新值覆盖
//并且清除为key = null的旧数据(弱应用)
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
// 达到阀值,就需要扩容了
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
弱引用
private void replaceStaleEntry(ThreadLocal<?> key, Object value, // 值
int staleSlot //无用的数值下标
) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
// 找到最近的 一个无效的Slot
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
// 从i往后遍历,找到替换
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
//与无效的sloat进行交换
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
如果最早的一个无效的slot和当前的staleSlot相等,则从i作为清理的起点
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
// 如果没有,将新的ksy-value 插入数组
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
扩容
/**
* Re-pack and/or re-size the table. First scan the entire
* table removing stale entries. If this doesn't sufficiently
* shrink the size of the table, double the table size.
*/
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
/**
* Expunge all stale entries in the table.
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
// 遍历 将无效的 剔除掉
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
//向后遍历 循环
expungeStaleEntry(j);
}
}
/**
* Double the capacity of the table.
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2; // 扩容为原来的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();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
// 计算新的扩容因子
setThreshold(newLen);
size = count;
table = newTab;
}
什么是线性探测?
用来解决hash冲突的一种策略.就是根 「据初始 key 的hashcode值确定元素在 table 数组中的位置,如果发现这个位置上已经有其他 key 值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置」 。 ThreadLocalMap 解决 Hash 冲突的方式就是简单的步长加 1 或减 1 ,寻找下一个相邻的位置
1、写入 , 找到发生冲突最近的空闲单元
2、查找, 从发生冲突的位置,往后查找
// 斐波那契数列 (为了使不同 hash 值发生碰撞的概率更小,尽可能促使元素在哈希表中均匀地散列在表中)
private static final int HASH_INCREMENT = 0x61c88647;
public static void main(String[] args) {
// Demo1.setThreadLocal();
hashTableIndex(16);
}
public static void hashTableIndex(int size){
// 生成hashcode间隙为这个魔数,可以让生成出来的值或者说ThreadLocal的ID较为均匀地分布在2的幂大小的数组中。
int hashCode=0;
for(int i=0;i<size;i++){
hashCode=i*HASH_INCREMENT+HASH_INCREMENT;
System.out.print((hashCode&(size-1))+" ");
}
System.out.println("");
}
//运行 结果
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0
为啥计算下标都是需要2的次幂?
有位大神写的比较好,我就不赘述了,引用下:为什么是2的次幂
最后的总结
大伙应该都发现了,由于用到的线性探索,存在大量的循环遍历,这样是相对来说是很消耗性能的。
ThreadLocal的内存泄露
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是GCRoot可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存
建议
1、使用ThreadLocal,建议用static修饰 static ThreadLocal headerLocal = new ThreadLocal();
2、使用完ThreadLocal后,及时执行remove操作,避免出现内存溢出情况。