该类提供了线程本地变量。该变量不同于普通的副本,因为访问这个变量(通过 get 或 set 方法)的每个线程都是自己独立初始化该变量的。
ThreadLocal实例通常是Thread类中典型的静态私有属性,由于关联线程的状态(比如,user ID 或 Transaction ID)
每个线程都持有其局部变量副本的隐式引用(即,每个线程都持有一个该变量的副本,因此,每个线程间的变量是独立的),在其整个生命周期中,只要该引用可访问。一旦线程消亡,它的所有局部变量都会被GC(除非存在其他对该副本的引用)
protected T initialValue() {
return null;
}
返回当前线程局部变量的初始化值。这个方法将在 get 方法第一次调用时被调用,除非线程再次之前调用了 set 方法,那么该方法将不会被调用。通常,该方法只会被调用一次,但如果在后续的操作中,在调用了remove方法后调用get方法,那么该方法(initialValue)将会被调用多次(因为,此时线程的局部变量需要重新被初始化)。
也就是说,该方法主要就是用于初始化线程的局部变量的,如果变量已经通过其他方式初始化了(如,set方法),那么该方法就不会被调用。
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();
}
返回当前线程该局部变量的值。如果当前线程该变量不存在,那么会调用“initialValue”方法进行变量的初始化,并返回初始化后该变量的值。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
将当前线程的局部变量设置为指定的值。大部分的子类无需重写该方法,只需要重写“initialValue”方法来设置局部变量的值。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
删除当前线程该局部变量值。如果当前线程在后续又调用了 get 方法,那么该局部变量的值会通过调用“initialValue”方法被重新初始化,除非再次期间“set”方法被调用了。这就是导致“initialValue”方法在当前线程中被调用多次的原因。
ThreadLocalMap 是一个自定义的 hash map,它仅适用于维护线程的局部变量。该类的所有操作都没有暴露在 ThreadLocal 类之外。为了帮助大对象和长生命周期对象的使用,ThreadLocalMap 的 Key 使用 WeakReferences 类型。然后,因为不使用引用队列,那么仅保证在 hash map 表空间不足的情况下,才会将无用的对象(stale entries)从 map 中移除。
ThreadLocalMap 是一个“懒”构造(即,延迟构造)。它会在至少有一个 Entry 需要被输入时,才会进行构造。
ThreadLocalMap 是使用一个 Entry 类来表示一个线程局部变量(Key)和这个变量的值(Value)。 其中 Key 是 WeakReference 的子类,而 WeakReference 所维护的引用通常就是 ThreadLocal。
注意:当 Entry#get() 返回 null 的时候,说明对应的 key 不存在了,即,这个对象已不再被任何对象引用了。因此,该 Entry 能被从当前的 hash map 中清除。这样的对象下 ThreadLocalMap 的操作中称为“stale entries”
WeakReference 是Java引用类型的一种。用来描述非必须的对象,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
理想情况下,不同的键都能转化为不同的索引值。当然,这只是理想情况,所以我们需要面对两个或者多个键都会散列到相同的索引值的情况。
因此,一个 Hash 算法主要有2个部分:Hash函数、解决碰撞的方法
ThreadLocalMap 使用的 Hash 函数是“斐波那契(Fibonacci)哈希法”;而解决碰撞的方法是“线性探测法”
在说“斐波那契(Fibonacci)哈希法”前,我们先来说说“乘法哈希法”
hash(key) = floor( M/W * ( a * key mod W) )
其中 floor 表示对表达式进行下取整注意:
其实,“乘法哈希”的思想就是:提取关键字 key 中间 k 位数字。
a ≈ W/φ
,1/φ ≈ (√5-1)/2 = 0.618 033 988
时情况。而,1/φ ≈ (√5-1)/2 = 0.618 033 988
,可称为黄金分割点。Q:那,为什么“斐波那契(Fibonacci)哈希法”能够更好的将关键字 key 进行散列了? A:Why is 'a ≈ W/φ' special? It has to do with what happens to consecutive keys when they are hashed using the multiplicative method. As shown in Figure ‘Fibonacci Hashing’ , consecutive keys are spread out quite nicely. In fact, when we use 'a ≈ W/φ' to hash consecutive keys, the hash value for each subsequent key falls in between the two widest spaced hash values already computed. Furthermore, it is a property of the golden ratio, φ , that each subsequent hash value divides the interval into which it falls according to the golden ratio!undefined
显然,短小的键簇才能保证较高的效率。随着插入的键越来越多,这个要求很难满足,较长的键簇也会越来越多。另外因为(基于均匀性假设)数组的每个位置都有相同的可能性被插入一个新键,长键簇被选中的可能被短键簇更大,同时因为新键的Hash值无论落在簇中的任何位置都会使簇的长度加 1(甚至更多,如果这个簇和相邻的簇之间只有一个空元素相隔的话)
假设J(均匀哈希假设)。我们使用的Hash函数能够均匀并独立地将所有的键散布于 0 到 M-1 之间。 讨论。我们在实现Hash函数时随意指定了很多参数,这显然无法实现一个能够在数学意义上均匀并独立地散布所有键的Hash函数。事实上,深入的理论研究报告告诉我们想要找到一个计算简单但又拥有一致性和均匀性的Hash函数是不太可能的。在实际应用中,和使用 Math.random() 生成随机数一样,大多数程序员都会满足于随机数生成器类的Hash函数。很少人会去检验独立性,而这个性质一般都不会满足。
命题 M :在一张大小为 M 并含有 N = α * M 个键的基于线性探测的哈希表中,基于假设 J ,命中和未命中的查找所需的探测次数分别为:
特别是当 α 约为 1/2 时,查找命中所需要的探测次数约为 3/2,未命中所需要的约为 5/2。当 α 趋于 1 时,这些估计值的精确度会下降,但不需要担心这些情况,因为我们会保证哈希表的使用率小于 1/2。
当哈希表快满的时候查找所需的探测次数是巨大的(α 越趋近于1,由公式可知探测的次数也越来越大),但当使用率 α 小于 1/2 时探测的预计次数只在 1.5 到 2.5 之间。
当执行“添加"操作后,hash map 元素(可能包括“失效”还未清除的元素)的长度超过表长度的 2/3 时,就会触发 rehash()操作。而rehash()操作,则会先对这个 hash map 中的失效元素进行清除,若清除后hash map中元素个数,依旧大等于表长度的 1/2 (size >= threshold - threshold / 4,即,size >= tab.length/2),则进行 hash map resize操作,将表长度扩大为原先的 2 倍。并将所有有效元素,重新插入扩建后的 hash map中。