最近突然想到ThreadLocal虽然能够为每个线程提供一个变量的副本,实现线程之间变量操作的隔离性、互不影响。但是它却不能保证状态变量的线程安全性,也就是说如果ThreadLocal为每个线程保存的变量原本就是线程不安全的,那么在多线程环境下,对此变量的操作依然存在并发安全问题。并且ThreadLocal并不能实现父子线程之间变量的传递【它的子类InheritableThreadLocal能够实现父子线程间的变量传递】。那么为什么ThreadLocal不能保证以上两点。接下来,就对ThreadLocal的实现深入了解下。
如何使用
源码中给的例子如下:
此例为每个线程生成一个唯一标识ID,线程标志ID在第一次调用get()方法时,被初始化。然后再以后的调用过程中,依然保持不变。所以ThreadLocal建议的使用方法为:定义一个全局的静态不可变的对象,如果需要初始值,便通过匿名内部类重写其initialValue()方法。
public class ThreadId {
// 定义一个原子递增的数据序列
private static final AtomicInteger nextId = new AtomicInteger(0);
// 为每个线程生成一个唯一ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// 返回当前线程的唯一ID
public static int get() {
return threadId.get();
}
}
线程副本存储机制
接下来讲解源码之前,有必要简要阐述下ThreadLocal如何为每个线程存储线程隔离的副本以及他与线程的关系又是如何的。
查看Thread的源码,你会发现他有两个成员变量
/* 此成员变量存储和线程有关的ThreadLocal值,数据的存储结构为散列Map。
不过这个Map并不是我们常用的HashMap,而是ThreadLocal类自己定义的一个
散列表。Map内部维护一个Entry数组,Entry就是key、value组合,Key就是构建的ThreadLocal变量,Value就是需要保存的变量,每个线程都会存储这样一个数据结构。下面会详细讲解。
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* 此成员变量存储和线程有关的InheritableThreadLocal值.
* InheritableThreadLocal类是ThreadLocal的子类,它能实现父子线程之间
* 的值传递,而ThreadLocal不能.下面会详细讲解。
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
线程Thread便是通过这两个成员变量,实现了线程间的变量隔离存储,线程间对ThreadLocal变量的操作互不影响。它们之间的关系如下
(图一)
成员变量及魔数0x61c88647
private final int threadLocalHashCode = nextHashCode();
/**
* 获取一个原子递增的序列,起始值为0
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* 此魔数极有可能是32位机器有符号数的黄金分割数.
* 为何如此选择此数、可以参考黄金分割率、斐波那契散列法有关的资料
* 源码中注解的大致意思:用此魔数作为步长与连续生成的哈希码之间的差异-
* 在长度为2^N大小的哈希表中,
* 将顺序生成的thread-local的ID转化为近似最优扩展的乘法哈希值
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* 以魔数0x61c88647为步长取得ThreadLocal的HashCode值.
* 此方法会使ThreadLocal均匀分布在稀疏哈希表中
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
关于此魔数是如何得出的,以下提供的思路,仅提供参考,并不保证可靠性。
笔者查阅了算法导论这本书【①】,在乘法散列法一节中可窥见一丝端倪。
乘法散列法的公式为h(k)=「m(kA mod 1)」
此公式包含两个步骤:
①用关键字乘上常数A(0<A<1),并提取kA的小数部分,即kA-「kA」
②用m乘以这个值,再向下取整
乘法散列法的一个优点就是对m的选择不是特别关键,一般选择它为2的某个幂次(m=2^p,p为某个整数),这是因为我们可以在大多数计算机上,按下面所示方法较容易地实现散列函数。假设某计算机的字长为w位,而k正好可用一个单字表示。限制A为形如s/2^w的一个分数。其中s是一个取自0<s<2^w的整数,参见下图,先用w位整数s=A*2^w乘上k,其结果是一个2w位的值,r1.2^w+r0,这里r1为乘积的最高位字,r0为乘积的最低位字。所求的p位散列值中,包含了r0的p个最高有效位。
虽然这个方法对于任何的A值都适用,但对某些值效果更好。最佳的选择与待散列的数据特征有关。Knuth[211]认为A≈(√5-1)/2是个比较理想的值。
所以s=2^32*(√5-1)/2≈2654435769
如果把它转为32位的有符号整数则为1640531527,这个数的十六进制数正是0x61c88647
public class ThreadHashTest {
public static void main(String[] args) {
long l1 = (long) ((1L << 31) * (Math.sqrt(5) - 1));
System.out.println("as 32 bit unsigned: " + l1);
int i1 = (int) l1;
System.out.println("as 32 bit signed: " + i1);
System.out.println("MAGIC = " + 0x61c88647);
}
}
方法
初始化
/**
*返回当前线程的thread-local变量初始值。当线程第一次使用{@link #get}方法
*访问变量时,将调用此方法,除非线程先前调用了{@link #set}方法,
*在这种情况下,{@code initialValue}方法将不会被线程调用。
*通常,每个线程最多调用一次此方法,但如果调用{@link #remove}后,
*再次调用{@link #get}则此方法会继续执行。
*此方法默认返回null,如果你希望初始化时,赋予ThreadLocal不同的值,
*则需要重写此方法,通常,示例中那样,使用匿名内部类
*/
protected T initialValue() {
return null;
}
/**
* JDK8新增方法
* 创建一个线程局部变量。 通过调用{@code Supplier}上的{@code get}方法
* 确定变量的初始值
* @since 1.8
*/
public static <S> ThreadLocal<S> withInitial(
Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
/**
*你可以使用{@code withInitial}方法,如下初始化ThreadLocal变量
*/
private static final ThreadLocal<Integer> threadId
=ThreadLocal.withInitial(new Supplier<Integer>() {
public Integer get() {
return nextId.getAndIncrement();
}
});
下面的方法,则是ThreadlLocal实现的核心了
设置当前线程的thread-local变量
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的成员变量ThreadLocalMap
//此map维护着由一组ThreadLocal作为key、给定变量值为value的Entry数组
ThreadLocalMap map = getMap(t);
//如果map不为空,则赋值【详情见下面set()方法】
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
set()方法
/**
* 此方法由静态内部类ThreadLocalMap定义,大致流程如下:
* 根据ThreadLocal的hashcode值,取得数组下标。以此下标开始往后寻找,
* 1)如果此key已经存在,则直接替换value。
* 2)如果key被垃圾回收,则继续往后寻找是否存在相同的key,如果有则替换它,
* 并交换两者的位置、以保证哈希表的顺序性。
* 在此过程中,则尽可能的一次性清除无效槽位
* 则以给定的key、value替换它。
* 3)如果没有找到key相同的槽位,则新建槽位。
* 如果新建槽位后,表的size超过负载因子,则重新rehash(清除整个表的无效槽位)
* 或者将整个表扩容为2倍长度
* @param key 为ThreadLocal变量
* @param value 给定值
*/
private void set(ThreadLocal<?> key, Object value) {
//ThreadLocalMap维护的entry数组
Entry[] tab = table;
int len = tab.length;
//threadLocal的hashcode值和数组长度-1做与运算
//为什么用此算法求数组下标?见【注释1】
int i = key.threadLocalHashCode & (len-1);
//此处循环有三个原因
//1:查找key是否已经存在,如果存在则直接替换value
//2:如果哈希存在碰撞,则继续向后寻找未使用的槽位【即碰到null,则停止】
//这是解决哈希碰撞常用的方式-开放寻址法-线性探查
//3:因为ThreadLocalMap的key是继承自弱引用的ThreadLocal,
//如果key被GC回收了、则entry也应该被清除,继而保证散列表的顺序
//为什么ThreadLocalMap的key被设置为弱引用,何时会被GC回收,见【注释2】
for (Entry e = tab[i];
e != null;//碰到null,则停止
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果key相同,则直接替换value
if (k == key) {
e.value = value;
return;
}
//key为空,则以指定key和value替换它,然后再清除key为空的槽位
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//如果没有匹配到此key所在的槽位,则创建
tab[i] = new Entry(key, value);
int sz = ++size;
//如果没有找到无效槽位并且数组中元素个数超过负载(16*2/3),则重新
//扩容详解,见下
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/**
* 数组下标i+1,如果超过数组长度,则重新从0开始。
* 这正是开放寻址方法-线性探查方式,寻找槽位的序列顺序
* 为什么i增长到数组长度后,要从0开始?见【注释3】线性探查序列
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
*以指定的key和value替换在set操作期间遇见到的无效槽位
*即key被垃圾回收了的Entry
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//当碰到key被清空的Entry时,此时需要尽可能的清除整个entry。
//当然,最好的策略应该是继续往前寻找被GC了key,记住它的位置。
//待后续一次性清除。但是因为由魔数计算出来的hashcode是间歇性跳跃的。
//例如0,3,6,9。
//所以并不能保证一次清除所有,只能保证在遇到空槽之前尽量清除
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
//找到关键字所在的槽位或者后面紧跟的空槽
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果找到了此ThreadLocal所在的槽位,以给定的value替换旧value,
//然后把此槽位与key为空的槽位置换。以保证槽位的顺序。
//并清除key为空的槽位
if (k == key) {
e.value = value;
//把此槽位与key为空的槽位置换
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//如果最早出现key为空的槽位仍然是参数中的staleSlot,
//也就是说在staleSlot槽位之前,未发现陈旧的槽位
//由于上面交换了槽位,所以需要刷新陈旧槽位的位置
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//如果最早出现key为空的槽位仍然是参数中的staleSlot,
//也就是说在staleSlot槽位之前,未发现陈旧的槽位
// 则重置陈旧槽位所在的位置
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 如果没有匹配到此关键字,则重新设置
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果发现其它无效条目,请将其清除
//清除详解,见下面方法
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
/**
*删除无效槽位
*此处staleSlot即为最早出现key为空的entry的下标
*/
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;
//一直往下探查,如果碰到key为空的entry,则清除
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果key为空,则清除
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//如果因为顺序调整或陈旧条目清除而造成哈希重新分散。
//因此为了保证数组的顺序性,则为此key所在entry重新分配槽位
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
//返回遇到的空槽的位置
return i;
}
/**
*此方法会尽可能的清除所有无效槽位
*当插入新元素或无效槽位被清除时,被执行
*当没有无效条目被发现时,它执行数组长度的对数扫描次数(log2(n))
*当持续发现无效条目时,他执行log2(table.length)-1对数扫描次数
*这也会造成O(n)的时间复杂度
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
//当发现无效槽位时,扫描起始对数的真数被重置为N
//因此极有可能会清除所有无效槽位
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);//无符号右移1位
return removed;
}
重新散列或扩容
/**
* 重新散列调整表格的大小,首先探查整个表、清除无效的槽位。
* 如果这不足以缩小哈希表中元素的大小,则进行扩容
*/
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)
//清除无效槽位,同时对size做减法,详解见上文
expungeStaleEntry(j);
}
}
/**
* 将表容量加倍.
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
//创建double长度的新表
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;
}
获取当前线程的thread-local变量
/**
* 返回当前线程的thread-local变量,如果ThreadLocal变量变量没有被赋值,
*那将会调用initialValue()方法,获取初始值
*/
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程维护的ThreadLocalMap哈希表
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;
}
}
//如果未找到对应的变量,则执行初始化initialValue()方法,并返回初始值
return setInitialValue();
}
/**
* 根据key值,在ThreadLocalMap维护的数组内,寻找变量
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//如果key相同,则直接返回变量。否则继续寻找,直到遇到空槽位
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
/**
* 当ThreadLocal变量的直接散列值未找到变量时,则执行此方法
*
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//从当前元素开始,继续向下探查,直到找到key对应的变量,或者遇到空槽位返回null
//在探查的过程中,会清理无效槽位
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的initialValue()方法
*然后指定set()方法,将初始值设置到当前线程的ThreadLocalMap变量里
* @return the initial value
*/
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//如果map存在,则直接设置。
//如果不存在,则先创建,再设置
//负载因子就在createMap()中设置
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
/**
*数组长度INITIAL_CAPACITY=16
*当数组的size大于16*2/3时,就扩容
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
移除当前线程的thread-local变量
/**
* 移除当前线程thread-local变量,如果随后便调用get()方法,
* 则会执行初始化initialValue()方法,除非由set()设置当前线程变量
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* 首先调用clear()清除弱引用ThreadLocal变量,
* 然后调用expungeStaleEntry(i)清除无效槽位,将Entry置为null
*/
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) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
为什么不能保证并发安全性
通过以上源码、可以看出,Thread中ThreadLocal.ThreadLocalMap threadLocals变量,只是维护由ThreadLocal变量作为key,任意变量作为value的Entry数组。并未对Value做任何并发安全性操作。所以如果Value本事就是状态对象,即使用ThreadLocal进行set后,使其变成线程私有变量,那么它仍然存在并发安全隐患。
为什么InheritableThreadLocal能够从父线程传递到子线程
InheritableThreadLocal继承自ThreadLocal,重写了下面三个方法
/**
*将父线程Entry的value复制到子线程(浅copy),
*可重写
*/
protected T childValue(T parentValue) {
return parentValue;
}
/**
* 返回Thread的成员变量inheritableThreadLocals
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
*已给定的key、value初始化ThreadLocalMap
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
当新建线程,执行init()方法时,会执行下面一段代码
Thread parent = currentThread();
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
再来看下ThreadLocal.createInheritedMap()方法
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
/**
*将父线程的Entry[]数组copy到子线程,如果开发者没有重写
*InheritableThreadLocal的childValue()方法,默认执行浅copy
*/
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
//默认执行浅copy,只复制引用
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
接下来,我们验证下父子线程间的值传递是否为浅复制
public class ThreadLocalTest {
private static final InheritableThreadLocal<User> threadUser =
new InheritableThreadLocal<User>();
public static void main(String[] args) throws InterruptedException {
threadUser.set(new User("parent"));
System.out.println(Thread.currentThread().getName()+":"+threadUser.get().getName());
new Thread(new Runnable() {
public void run() {
threadUser.get().setName("child");
System.out.println(Thread.currentThread().getName()+":"+threadUser.get().getName());
}
}).start();
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+":"+threadUser.get().getName());
}
}
程序执行结果
main:parent
Thread-0:child
main:child
从结果得知父子线程之间的值传递确实为浅复制。子线程对变量的操作,会影响父线程。
如果你想改变此默认行为,可重写InheritableThreadLocal的childValue()方法。
注释1【②】
把hashcode值与数组长度做与运算,这正解释了数组长度为什么要取2的整数幂,因为这样数组长度减一正好相当于一个低位掩码。“与”操作的结果就是散列值的高位全部归零,只保留低位值,【这样取得数组下标总是在0~数组长度-1的区间内,因此这样导致的后果,难免会带来哈希碰撞。后面再讲ThreadLocal是如何处理碰撞的】用来做数组下标访问。以初始长度16为例,16-1=15。2进制表示是 00001111。和某hashcode值【假如AtomicIntege一直递增到100,暂时不考虑负载因子、扩容】做“与”操作如下,结果就是截取了最低的四位值。
01100100
& 00001111
---------------------
00000100 //高位全部归零,只保留末四位,最终结果为十进制4
注释2
就像图一描述的那样,每个线程内部都独立维护着一个ThreadLocalMap对象。ThreadLocal对象作为key,私有对象作为value。也就是说只要是关联了此ThreadLocal对象的线程都有一个指向此ThreadLocal对象的引用。如下图
假设如果某一时刻,程序主动清除了ThreadLocal对象,而关联此对象的线程又是线程池中的核心线程,永远不会销毁。此时,线程中对ThreadLocal对象的引用迟迟不能释放。那么ThreadLocal对象就不会被GC回收。这样就会有造成内存泄漏的可能性。那么ThreadLocalMap是如何解决这个问题的呢?ThreadLocalMap继承了WeakReference,它把对ThreadLocal引用置为弱引用。弱引用的好处就在于当程序中没有此对象的强引用时,当GC执行的时候,JVM就会回收此对象的内存,减少了内存泄漏的可能性。GC执行后,只是ThreadLocalMap内Entry的key(ThreadLocal)被回收,而value所占用的内存还没有被释放。当程序再次调用其它ThreadLocal对象的get()或者set()方法时,就会清除key为空的Entry对象,此时Entry对象彻底被回收。
注释3【③】
在开放寻址法中,所有的元素都存放在散列表里。也就是说每个表项或包含动态集合的一个元素,或包含NIL。当查找某个元素时,要系统地检查所有的表项,直到找到所需的元素,或者最终表明该元素不在表中,此过程便称为探查。插入一个元素,也是要连续地检查散列表,直到找到一个空槽来放置待插入的关键字为止。检查的顺序不一定是0,1,…,m-1(这种顺序下的查找时间为Θ(n)),而是要依赖待插入的关键字。
在下面的伪代码中,假设散列表T中的元素为无卫星数据的关键字,关键字k等同于包含关键字k的元素。每个槽包含一个关键字,或包含NIL(如果该槽为空)。
插入:
HASH-INSERT(T,k)
i=0
repeat
j=h(k,i)
if T[j]==NIL
T[j]=k
return j
else i=i+1
until i==m
查找
HASH-SEARCH(T,k)
i=0
repeat
j=h(k,i)
if T[j]==k
return j
i=i+1
until T[j]==NIL or i==m
return NIL
有三种技术常用来计算开放讯执法中的探查序列:线性探查、二次探查和双重探查。
ThreadLocalMap采用的便是线性探查。
给定一个普通的散列函数h’:U->{0,1,…,m-1},称之为辅助散列函数,线性探查方法采用的散列函数为:h(k,i)=(h’(k)+i) mod m,i=0,1,…,m-1
给定一个关键字k,首先探查槽T[h’(k)],即由辅助散列函数所给出的槽位。再探查槽T[h’(k)+1],以此类推,直至槽T[m-1]。然后,又绕到槽T[0],T[1],…,直到最后探查到槽T[h’(k)-1]。在线性探查方法中,初始探查位置决定了整个序列,故只有m中不同的探查序列。线性探查法较容易实现,但它存在着一个问题,称为一次群集。随着连续被占用的槽不断增加,平均查找时间也随之不断增加。群集现象很容易出现,这是因为当一个空槽前有i个满的槽时,该空槽为下一个将被占用的概率是(i+1)/m。连续被占用的空槽就会变得越来越长,因而平均查找时间也会越来越大。
【参考】
①《算法导论》数据结构11.3.2乘法散列法
②关于hashMap的一些按位与计算的问题https://www.zhihu.com/question/28562088
③《算法导论》数据结构11.4开放寻址法