三、散列算法
-
ThreadLocal的Hash值计算
-
ThreadLocal里的hash code偏移量是固定值
private static final int HASH_INCREMENT = 0x61c88647;
-
0x61c88647 转为 十进制就是0.618,0.618就是hash值的黄金分割点。计算公式也就是 √5 -1 / 2.
-
ThreadLocal使用的就是 斐波那契散列法 + 拉链法存储数据到数组结构中。
-
四、源码部分
-
初始化,创建对象时很简单,只需要设置泛型,就会自动得到一个对应的hash值下标。
private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }
-
设置元素
-
四种情况:
- 待插入下标。是空位置直接插入
- 待插入下标,不为空,key相同,直接更新
- 待插入下标,不为空,key不相同,拉链法寻址。
- 不为空,key不相同,碰到过期key。
-
源码:
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(); if (k == key) { // 查看key是否相等 相等直接更新值 e.value = value; return; } if (k == null) { // key为空,直接赋值 replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
-
扩容机制
if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();
-
首先,先进行清理操作,把过期元素清理掉,看空间是否充足
-
之后,判断大小,如果数组中的元素大于 len * 2 / 3 就需要扩容了,threshold = len * 2 / 3
-
rehash()
private void rehash() { expungeStaleEntries(); // 探测过期元素后判断是否满足扩容条件 if (size >= threshold - threshold / 4) resize(); } 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) s expungeStaleEntry(j); } }
-
resize()
private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 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; // 不存在了则进行置空,指控后被垃圾处理器回收释放内存 } 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不同,需要拉链式寻找
- 没有直接定位到,key不同,拉链式寻找,遇到GC清理元素,需要探测式清理,再寻找元素。
-
源码
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } // 被调用的getEntry private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); } // 被调用的 gentEntyrtAgterMiss private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); // 清理并前移后续的元素 else i = nextIndex(i, len); e = tab[i]; } return null; }
-
探测式清理,执行较耗时,在使用ThreadLocal.remove()操作,避免弱引用发生GC后,导致的内存泄露的问题。
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
六、总结
-
ThreadLocal的本质通过类名我们就可以得知,他是多线程本地的变量共享,但实际上它的作用并不能完全满足线程安全。假如存储的是引用类型的变量,线程安全的结论就不成立了,因为它内部存储的还是引用类型,最终的hash地址的指向依旧是同一个,假如多个线程使用了当前ThreadLocal变量里的值,并修改了内部的属性值,所有线程内的数据都会同步修改,最终的处理方法其实只有以下示例方式。
public static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new InheritableThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } };
此处依旧是每次线程调用时都会执行一个新的对象,但优点就是方便,节省内存空间。
-
ThreadLocal的数据结构
- 底层数据存储结构依旧是数组,通过斐波那契散列法+拉链法进行了数据的存储。
-
ThreadLocal是可以满足线程安全的吗?
- 如果是类似String类型的不可变数据,实际上是可以满足线程安全的说法的
- 假如是引用类型,就无法满足线程安全的说法,因为ThreadLocal中实际的存储依旧是指向的同一个内存地址。
-
在使用ThreadLocal时要注意什么
- 一定要在finaly中手动的remove掉ThreadLocal中存储的线程副本,避免执行自扫描GC的操作,避免引起内存泄漏
-
ThreadLocal的应用场景
- 链路追踪
- 对线程不安全的类进行包裹使用。