探究ThreadLocal
下面有一个很重要的HASH_INCREMENT,他的值是0x61c88647
public class ThreadLocal<T> {
/**
*ThreadLocals依赖于附加到每个线程的每线程线性探针哈希映射 (thread.threadLocals和inheritableThreadLocals)。ThreadLocal对象充当键,通过threadLocalHashCode搜索。这是一个自定义哈希代码 (仅在ThreadLocalMaps中有用),它消除了在相同线程 *使用连续构造的ThreadLocals的常见情况下的冲突,同时在不太常见的情况下保持良好的行为。
*/
private final int threadLocalHashCode = nextHashCode();
/**
* 连续生成的哈希码之间的差异-将隐式顺序线程本地id转换为两次幂大小的表的近乎最佳扩展的乘法哈希值。
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* 连续生成的哈希码之间的差异-将隐式顺序线程本地id转换为两次幂大小的表的近乎最佳扩展的乘法哈希值。
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* 返回下一个哈希代码。
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//忽略
}
0x61c88647:
0x是十六进制。
将61c88647放入进制转换器中:
二进制:1100001110010001000011001000111
十进制:1640531527
这个数字表示黄金比例乘以2的31次方 ((sqrt(5)-1) * (2^31))。结果是一个黄金数,即2654435769或-1640531527。
此外,在ThreadLocalMap的哈希处理方式中,我们可以看到HASH_INCREMENT与使用黄金比率的斐波那契哈希有关。标准的java.util.HashMap使用链表来解决哈希冲突。而ThreadLocalMap则只是寻找下一个可用的空间并将元素插入其中。它通过位掩码来找到第一个可用的空间,因此只有较低的几位是有效的。如果第一个空间已满,它会将元素放入下一个可用的空间。HASH_INCREMENT将键在稀疏的哈希表中分散排列,以减小找到相邻值的可能性。
因此,使用HASH_INCREMENT可以帮助减少ThreadLocalMap中哈希冲突的可能性,保证元素在哈希表中分布均匀。这样可以提高ThreadLocal的性能和效率。
其中使用了Entry[]
Entry对象是静态内部类
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
使用了WeakReference,这是弱引用。
Java中 4种引用的级别和强度由高到低依次为:强引用 -> 软引用 -> 弱引用 -> 虚引用
当垃圾回收器回收时,某些对象会被回收,某些不会被回收。垃圾回收器会从根对象 Object来标记存活的对象,然后将某些不可达的对象和一些引用的对象进行回收。
表格说明如下:
引用类型 | 被垃圾回收时间 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | JVM停止运行时终止 |
软引用 | 当内存不足时 | 对象缓存 | 内存不足时终止 |
弱引用 | 正常垃圾回收时 | 对象缓存 | 垃圾回收后终止 |
虚引用 | 正常垃圾回收时 | 跟踪对象的垃圾回收 | 垃圾回收后终止 |
为什么使用Entry数组
是因为多线程的情况,要保证每个线程的副本值都有各自的值。
他用的是线性探测法。就是线程不多。不存在经常扩容的情况。
ThreadLocal 往往存放的数据量不会特别大(而且key 是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低
HashMap和ConcurrentHashMap1.8实现,不再展开。
开放地址法:容易产生堆积,不适合大规模的数据存储。删除其中一个冲突的元素时,需要移动元素。
链地址法:平均查找长度短,指针额外空间开销。适合大规模数据。
开放地址法
即使key产生hash冲突,也不会形成链表,而是将所有元素都存入哈希表里。发生hash冲突时,就以当前地址为基准,进行再寻址的方法去寻址下一个地址,直到找到一个为空的地址为止。实现方式有:
1.线性探测法:发生hash冲突时,顺序查找下一个位置,直到找到一个空位置(固定步长1探测)
2.线性补偿探测法:在发生hash冲突时,在表的左右位置进行按一定步长跳跃式探测(固定步长n探测)
3.伪随机探测:在发生hash冲突时,根据公式生成一个随机数,作为此次探测空位置的步长(随机步长n探测),从而可以避免堆聚
缺点:容易产生堆积问题;不适于大规模的数据存储;散列函数的设计对冲突会有很大的影响;插入时可能会出现多次冲突的现象,删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂;结点规模很大时会浪费很多空间
拉链法
jdk1.8 中HashMap,ConcurrentHashMap都是采用这个方法,使用链表来保存发生hash冲突的key,即不同的key有一样的hash值,将这些发生冲突的 value 组成一个单向链表。
优点:
①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短
②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况
③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间
④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可
缺点:
指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度
再哈希法
发生hash冲突时,使用第二个,第三个,第四个哈希函数来计算地址,直到无冲突时,比较耗时。
建立公共溢出区
为所有发生hash冲突的关键字记录一个公共的溢出区来存放。在查找的时候,先与哈希表的相应位置比较,如果查找成功,则返回。否则去公共溢出区按顺序查找。在冲突数据少时性能好,冲突数据多的时候耗时。