先看使用用例, 下面使用ThreaLocal实现一个线程独立计数器的代码
public class ThreadLocalExample {
// 创建一个ThreadLocal变量,用于存储每个线程的计数器, 初始值设为0
private static final ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
// 创建多个线程并启动
for (int i = 0; i < 5; i++) {
new Thread(new CounterTask(i)).start();
}
}
// 内部静态类实现Runnable接口
private static class CounterTask implements Runnable {
private final int threadId;
//对线程名进行赋值
public CounterTask(int threadId) {
this.threadId = threadId;
}
@Override
public void run() {
// 每个线程增加自己的计数器值
for (int i = 0; i < 10; i++) {
int currentCount = threadLocalCounter.get();
threadLocalCounter.set(currentCount + 1);
System.out.println("Thread #" + threadId + " - Count: " + threadLocalCounter.get());
}
}
}
}
想弄懂ThreadLocal类, 首先我们先看Thread类
可以看到Thread类中有声明ThreadLocalMap类, 而ThreadLocalMap类又是ThreadLocal的静态内部类
根据map我们不难想到ThreadLocalMap在ThreadLocal中是用来存储数据的,而事实也的确如此,看set和get方法可以确认, ThreadLocal的get(), set()方法实际上是通过ThreadLocalMap来对Entry对象操作来实现的, 什么是Entry对象? 大家熟悉的Map对象是键值对的集合, Entry对象就是单个的键值对
上面讲完ThreadLocal的get()和set方法, 我们来进一步分析是怎么实现的, 先来看get方法
public T get() {
//currentThread为本地方法, 通过其他语言实现, 作用为获取当前线程对象
Thread t = Thread.currentThread();
//返回之前在Threa类中声明的ThreadLocalMap类, 引用名为TthreadLocals, 可以看本文第二张图的184行
ThreadLocalMap map = getMap(t);
if (map != null) {
/*这种写法想必大家很熟悉, 同样Entry为ThreaLocalMap的静态内部类,这个类下面再做详解,
只需要知道表示单个键值就行了这里 */
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//获取值, 并返回
T result = (T)e.value;
return result;
}
}
//如果没有设置值的话,默认值为设为null
return setInitialValue();
}
这里我们再来讲下Entry怎么工作的,Map底层的实现也依赖于Entry,可以帮你们进一步了解Map,话不多上贴源码
static class ThreadLocalMap {
///被忽略的代码
private Entry[] table;
//Entry静态内部类
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry getEntry(ThreadLocal<?> key) {
// 算出位置i, 根据线程对象和当前键值对的长度来计算的
int i = key.threadLocalHashCode & (table.length - 1);
//获取到key对应位置的单个键值对
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
//从i开始遍历后面的元素,找到对应键的元素, key相等就返回对应元素, 为null则进行探测式清理
return getEntryAfterMiss(key, i, e);
}
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;
//获取这个键在hash中对应的位置
int i = key.threadLocalHashCode & (len-1);
//从i开始找,直到找到对应的位置
for (Entry e = tab[i];
//e!null表示发生了哈希冲突
e != null;
//从ThreadLocal的对象散列值开始每次往后移一位
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如何要放的位置上的键和这个键相等说明是替换,进行值更新
if (k == key) {
e.value = value;
return;
}
//如果碰到过期的key, 优先使用过期的key,节省空间
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//如果到这里说明key对应的hash位置上没有数据,且Entry中的key都在被时殷弘,
tab[i] = new Entry(key, value);
int sz = ++size;
// 做一次启发式清理:
// 条件一:!cleanSomeSlots(i, sz) 成立,说明启发式清理工作未清理到任何数据..
// 条件二:sz >= threshold 成立,说明当前table内的entry已经达到扩容阈值了..会触发rehash操作。
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
//被忽略的代码
}
大致数据结构为一个 Thread 类可以有多个 ThreadLocal 类,一个 Thread 类拥有一个 ThreadLocalMap,一个ThreadLocalMap又存储了多个ThreadLocal, ThreadLocalMap的底层数据结构是数组;键值对Entry(ThreadLocal<?> k, Object v)(多个 ThreadLocal)都存储在 table 数组中,就像Map [{id:1},{name:"张三"},{age:20}]。
另外要注意的是Entry是继承了弱引用的, 而键确是强引用, 弱引用在gc时,不管内存空间够不够都会被回收, 而value不会被清理, 假如我们不做任何措施的话, vlue永远无法被回收, 从而导致内存泄漏
不过ThreadLocal也有应对这种情况的策略, 上面代码中的replaceStaleEntry方法就是使用新增的key代替原来的key,和value, 并且set(), get(). remove()等方法时都会清理key为null的记录
开放地址法,
而不是拉链寻址法, 即继续往后找空位置, 而不是往下找空位置原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。