源码 | 完全理解ThreadLocal---Java并发编程系列(一)

先备知识

Java中不一样的引用类型

在Java中根据垃圾回收的方式不一样,引用按照对象生命周期的长短分为四种,由高到低分别为强引用、软引用、弱引用和虚引用。java

强引用

Java中默认的引用类型,一个对象若是具备强引用那么就没有资格被垃圾回收。 数组

VfCm5t.png

软引用

一个对象若是只具备软引用,当JVM内存充足的时候和强引用并没有区别,那么当JVM内存不足的时候,这个对象就会被垃圾回收。软引用能够和一个引用队列(ReferenceQueue)联合使用。若是软引用所引用对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。 缓存

VfC1Kg.png

弱引用

若是一个对象只具备弱引用(即不具备强引用,软引用,虚引用),那么这个对象会被垃圾回收器标记回收。弱引用能够和一个引用队列(ReferenceQueue)联合使用,若是弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。弱引用指向的对象能够经过弱引用的get方法得到,由于弱引用不能阻挡垃圾回收器对其回收,因此当弱引用指向的对象被GC的时候get方法会返回null。 微信

VfCaGV.png

虚引用

虚引用不会影响对象的生命周期,惟一用处就是能在对象被GC时收到系统通知,JAVA中用PhantomReference来实现虚引用。虚引用指向的对象十分脆弱,咱们不能够经过get方法来获得其指向的对象。dom

ThreadLocal简介

ThreadLocal是什么?

首先ThreadLocal是一个用于线程建立本身线程本地变量的类。这个变量相对于本线程是全局的,相对于其余线程是隔离的,也就是说在不一样的线程之间独立存在,一个线程没法访问和修改其余线程的线程本地变量。即使两个线程执行同一段代码,在代码中都使用了ThreadLocal来建立线程本地变量,那它们也只能看到并访问本身线程的本地变量,并不能相互看到对方的线程本地变量。ide

如何使用?

Talk is cheap,show me the code。函数

咱们先经过一个实际的例子看一下ThreadLocal的使用。性能

假设有一个商城,客户下发一个订单,商城会分红多个步骤来处理这个订单(查库存,配货等),商城为每一个订单分配一个惟一标识OrderID,而且在订单的各个处理步骤中都应该被随时读取。咱们对于每一个客户的订单处理new一个线程来表示,实际处理的步骤省略掉只是打印OrderID。大数据

public class OrderIdHolder {
    public static final ThreadLocal<String> CURRENT_ORDERID = new ThreadLocal();

    static String getCurrentOrderId() {
        return CURRENT_ORDERID.get();
    }

    static void setCurrentOrderId(String Id) {
        CURRENT_ORDERID.set(Id);
    }

    static void remove() {
        CURRENT_ORDERID.remove();
    }
}

public class OrderProcessingThread extends Thread {
    Random random = new Random();

    OrderProcessingThread(String name) {
       super(name);
    }

    @Override
    public void run() {
        OrderIdHolder.setCurrentOrderId(getName() +" " + random.nextInt(100));
        /*注意这里咱们并无显式的传递OrderId*/
        BusinessService businessService = new BusinessService();
        businessService.checkInventory();
        businessService.ship();
        OrderIdHolder.remove();
    }

    public static void main(String args[]) {
        Thread threadOne = new OrderProcessingThread("ThreadA");
        threadOne.start();

        Thread threadTwo = new OrderProcessingThread("ThreadB");
        threadTwo.start();
    }
}

public class BusinessService {
    public void checkInventory() {
        System.out.println("checkInventory " + OrderIdHolder.getCurrentOrderId());
    }

    public void ship() {
        System.out.println("ship " + OrderIdHolder.getCurrentOrderId());
    }
}
复制代码

结果:this

checkInventory ThreadB 18
checkInventory ThreadA 42
ship ThreadA 42
ship ThreadB 18
复制代码

如上所示,虽然咱们并无显式的将OrderId传递到checkInventory和ship方法内,可是同一个订单处理(同一个线程)的两个方法得到的OrderId均相同,可是不一样的订单处理(不一样线程)的OrderId是不一样的。能够看到ThreadLocal变量是每一个线程“独自持有一份儿的”,两个线程实际上是有两份儿不同的ThreadLocal变量。

能够解决什么问题?

从上面的例子能够体会到,当一个实例,不被容许在多个线程间共享,可是对于每一个线程来讲不一样的类与方法都须要共享并常常访问这个实例的时候,应该使用ThreadLocal

ThreadLocal核心源代码解析

从对开发者暴露的set方法入手

你可能会有疑问,咱们存储变量时明明是只有一个CURRENT_ORDERID(ThreadLocal),为何每一个线程会本身有一份儿线程本地变量呢?下面咱们一块儿揭开ThreadLocal的神秘面纱。

咱们直接从ThreadLocal使用时的核心方法set入手。

public class ThreadLocal<T> {
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    ThreadLocalMap getMap(Thread t) {
      return t.threadLocals;
    }
}

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
复制代码

调用set方法时,先是取得了当前的线程,而后调用getMap方法,取得了一个Map,从这里能够看出ThreadLocal自己并不存储变量的值,数据实际存放在Thread内的一个Map里面,也就是说数据实际都是存放在各个线程自己的,使用者调用ThreadLocal的set()方法其实最终都是对这个Map进行操做的。ThreadLocal只是为咱们操做这个Map提供了一个便捷入口。能够看到ThreadLocalMap的初始值是null,当第一次经过ThreadLocal的set方法或get方法试图访问操做这个Map时会调用createMap(t, value)进行初始化工做。

public class ThreadLocal<T> {
    ……
    private final int threadLocalHashCode = nextHashCode();
    private static final int HASH_INCREMENT = 0x61c88647;
    private static AtomicInteger nextHashCode = new AtomicInteger();
    private Entry[] table;
    private int size = 0;
    private int threshold;

    private static int nextHashCode() {
       return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    ……
    static class ThreadLocalMap {
      private Entry[] table;
      private int size = 0;
      private int threshold;
      private static final int INITIAL_CAPACITY = 16;

      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;
       }
    }
}
复制代码

上面的代码中能够看出ThreadLocalMap其实就是一个依赖数组实现,定制化的HashTable。ThreadLocal对象实例做为Key用于定位数据实际在Entry数组中的下标,下标的值为ThreadLocal对象的threadLocalHashCode通过位运算取模获得(不太清楚原理的同窗请参考https://blog.csdn.net/actionzh/article/details/78976082)。在下标处放入相应数据后,把当前Entry数组已存放数据的个数(size)设置为1,并把Threshold设置为当前容量的2/3,这个值在进行扩容时会做为判断条件使用。 除了第一次访问操做Map调用ThreadLocalMap的createMap(t, value)初始化ThreadLocalMap(其实是ThreadLocalMap底层的Entry数组),之后都会调用ThreadLocalMap的set方法存放数据。

static class ThreadLocalMap {
      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)]) {
                // 注释1:Entry的get方法
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
      }

      static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
     }
}
复制代码

上面代码中的注释1处咱们看到调用了Entry对象的get方法,返回的类型是ThreadLocal,那么这里是什么意思呢?咱们先来看一下Entry这个类。能够看到继承了WeakReference这个类,那么能够看到Entry的key是一个指向了ThreadLocal对象的弱引用。若是指向的对象被GC掉了(前面说过,弱引用是不会影响其指向对象的GC的),那么Entry对象的get方法就会返回null,能够由此来判断其指向的ThreadLocal对象是否已经无用被用户“弃用”。

上面这段代码整体逻辑比较简单,先根据ThreadLocal对象计算出以此为key的Entry应该放置在Entry数组中的Index,若是这个Index处没有Entry,直接放置,若是已经放置了Entry也即slot不为空,那么就说明两个Entry的key映射到了一个地方,也就是散列表产生了冲突,此时采用线性探测法解决冲突来探测空的slot。探测的过程当中,若是查找到了目标key的Entry,直接替换value为咱们的目标value便可。比较重点的地方是线性探测的过程当中若是遇到了位置i此slot处的Entry的key指向的ThreadLocal已经被GC掉了,那么就将i与待插入的Entry做为参数传递给replaceStaleEntry方法,并执行而后直接return。

replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot)
复制代码

replaceStaleEntry方法具体都作了什么呢?replaceStaleEntry会将做为其参数传递来的Entry存放在Entry数组的staleSlot处,并会清除夹在staleSlot先后两个null之间的一连串Entry中全部key为null(即指向的ThreadLocal已经被GC)的Entry。听起来有些绕,为了下降描述的复杂度,引入两个名词。

  • run --- 在Entry数组中夹在两个null之间的一连串Entry。
  • Stale Entry --- Entry的key指向的ThreadLocal已经被GC,这个时候ThreadLocal已经不存在了,那么这个ThreadLocal对应存放的数据Entry已经没有意义天然要被GC,因此形象地来讲就是Stale Entry。

如今从新表述一下replaceStaleEntry的做用:将参数传递来的Entry存放在Entry数组的staleSlot(函数第三个参数)处,并清除Entry数组中staleSlot所在run中全部的Stale Entry。 代码读到这里咱们能够大体画出ThreadLocal的总体原理图了:

VfCIqH.png

丢掉冗余,才能高效---清道夫replaceStaleEntry

下面咱们就来看一下这个replaceStaleEntry的具体实现。

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
            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();
                if (k == key) {
                    e.value = value;
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    // 第三阶段
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            if (slotToExpunge != staleSlot)
                // 第三阶段
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }
复制代码

根据ThreadLocalMap的set方法中replaceStaleEntry调用的状况,这三个参数分别表明要插入Entry的key,value以及线性探测法解决哈希冲突扫描的过程当中遇到的第一个StaleEntry的Index。 进入方法内部查看具体的实现,能够发现replaceStaleEntry自己只负责了将Entry放到staleSlot处,实际上当前run的staleEntry清理操做交给了expungeStaleEntry方法,replaceStaleEntry内只是为expungeStaleEntry方法的调用作了准备工做。 简单来讲此方法主要作了两件事:

  • 将待插入的Entry放到StaleEntry处 从staleSlot处开始向后扫描staleSlot当前所在run,若是发现具备目标key的Entry将value设置成将要插入的Entry的value并将此Entry与staleSlot处的staleEntry交换,若是没有发现,那么直接new一个Entry放到staleSlot处。
  • 肯定进行StaleEntry清扫工做的起始Index->slotToExpunge,并将其做为参数传递给expungeStaleEntry方法进行StaleEntry清扫 全面扫描staleSlot所在run除了staleSlot以外(由于无论怎样,调用expungeStaleEntry前,staleSlot处都会填充Entry再也不是staleEntry了)的第一个staleEntry的Index->slotToExpunge,并传递给expungeStaleEntry方法进行当前run的从slotToExpunge开始对staleEntry的大清扫。若是没有找到除了staleSlot处的Entry以外的staleEntry那么就不进行清扫工做。 下面咱们经过几个例子,直观地看一下大体的处理流程(下面的例子并不是要穷举全部状况,旨在帮助读者经过关键例子体会代码的执行逻辑)。 为了后续图片表述的无歧义性,这里再约定几个画图规则。
    VfCORf.png
  • 上图表示Entry数组
  • 每一个格子(Slot)里面: NULL:表示空Slot。 空心圆:StaleEntry。 黑色实心圆:表示不为NULL也不是StaleEntry的正常Entry。 黑色实心圆有Key标志:表示不为NULL也不是StaleEntry的正常Entry,而且Key与所要插入的Entry的Key相同。 replaceStaleEntry的过程大体能够表示为以下三个阶段(下面的内容请结合上面的代码里面的注释一块儿看):

replaceStaleEntry方法执行前初始状态

状况1:

VfPJyD.png
状况2:
VfP2wj.png
状况3:
VfP4f0.png
状况4:
VfPT6U.png
第一阶段 状况1: 在run中从staleSlot处出发向前扫描,若是发现staleEntry那么将扫描过程当中排在最前面的staleEntry的Index赋值给slotToExpunge。
Vfi9XD.png
状况2,3,4: 在run中从staleSlot处出发向前扫描,没有发现staleEntry,不作任何事情。

第二阶段

接下来在run中从staleSlot处出发向后扫描,扫描过程当中对于每个slot内的数据:

1.先判断是不是具备目标key的Entry,若是是,将其value设置成将要插入的Entry的value并与staleSlot处的staleEntry交换(注意交换后当前slot处的Entry就是staleEntry了)。交换后判断slotToExpunge == staleSlot成立则说明此run内当前Slot位置前并没有staleEntry。当前slot的Index就是当前run中的第一个staleEntry的Index,也即后续清扫工做的起始Index,将其赋值给slotToExpunge。若是slotToExpunge!=staleSlot说明此时slotToExpunge的值已是当前run中第一个staleEntry的Index了,那就不对slotToExpunge值作更改。slotToExpunge被肯定后,中止继续向后扫描,进入到第三阶段将slotToExpunge传递给清理函数进行staleEntry的清扫工做,而后return。

2.若是当前slot内的数据不是具备目标key的Entry,判断当前slot内的Entry若是知足是staleEntry而且slotToExpunge == staleSlot,那么就表明当前Entry的Index是当前run除了staleSlot外的第一个staleEntry的Index,也即后续清扫工做的起始Index,将其赋值给slotToExpunge。 在第二阶段的末尾,若是扫描过程当中没有扫描到具备目标key的Entry,那么直接将要插入的Entry放到staleSlot处,若是此时slotToExpunge!=staleSlot, 说明当前run中有staleEntry而且slotToExpunge是第一个staleEntry,也即后续清扫工做的起始Index,那么进入第三阶段将slotToExpunge参数传递给清理函数进行staleEntry的清扫工做。若是slotToExpunge == staleSlot则证实当前run没有须要清理的staleEntry就不进入第三阶段。

总结一下:第二阶段的任务就是a.在第一阶段从staleSlot处向前扫描的基础上,向后扫描最终肯定进行StaleEntry清扫工做的起始Index->slotToExpunge。 b.将待插入的Entry放到StaleEntry处。c.根据slotToExpung的值与staleSlot的值的相等关系来判断是否进入第三阶段。 :slotToExpunge值的更改,都是判断slotToExpunge==staleSlot成立后才进行的,由于初始值slotToExpunge就是等于staleSlot的,这样能够保证slotToExpunge的值只有在碰见当前run内除了staleSlot处外第一个staleEntry的时候才会更改,保证了slotToExpunge的值是当前run中的第一个staleEntry的Index,也即后续清扫工做的起始Index。

状况1: 按照上述步骤,状况1的第二阶段如图所示:(注意一点,扫描到具备目标key的Entry后,这个阶段的三个任务都已经完成,将不继续向后扫描,直接进入第三阶段)。

VfiAAA.png
状况2: slotToExpunge会变成交换前的目标key所在位置,此时slotToExpunge为当前run的第一个StaleEntry的Index,以下图所示:(一样扫描到具备目标key的Entry后,不继续向后扫描,直接进入第三阶段。)
Vfim1f.png
状况3: 在当前run从satleSlot向后扫描,slotToExpunge置为当前run除了staleSlot处外第一个StaleEntry的Index,并将待插入的Entry放到staleSlot位置。经判断staleSlot不等于slotToExpunge,表示当前run有staleEntry须要被清理,将其传递给expungeStaleEntry清扫方法,进行第三阶段staleEntry大清扫工做。如图所示:
Vfi3As.png
状况4: 没有扫描到具备目标key的Entry以及staleEntry,将待插入的Entry放到staleSlot位置。判断一下staleSlot等于slotToExpunge,意味着当前的run并无staleEntry须要清理(staleSlot处已经放置Entry),说明当前run很干净不用清扫,不作任何操做,不进入第三阶段。如图所示:
VfiGhq.png
第三阶段

状况1,状况2,状况3: 执行expungeStaleEntry清扫方法,进行staleEntry大清扫工做。 状况4: 未进入第三阶段。

清扫工做的具体执行者---清道夫expungeStaleEntry

前面说过,replaceStaleEntry把须要插入的数据放到了staleSlot处后,只是作了调用expungeStaleEntry前的准备工做,即扫描到了须要清理部分的最开始的位置,并看成参数staleSlot传递给了expungeStaleEntry方法,真正进行清扫工做的是expungeStaleEntry。

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            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;
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }
复制代码

此方法清理了从staleSlot开始当前run内全部的staleEntry,让指向其的引用指向null,使其变成不可达对象,以便JVM垃圾回收。从staleSlot处开始清理,当前Entry若是是staleEntry就清理掉,若是是正常的Entry,但不在根据它的key计算的本来应在的Index处,说明这个Entry当时插入的时候产生了哈希冲突,目前所在位置的Index是线性探测后找到的位置。目前所在位置前通过清扫工做后可能会整理出不少空Slot(可用位置),将当前Entry前移整理到[Index(Cal),Index(Cur) ]这个闭区间内的第一个空Slot处,这样能够提升下次调用get方法查找此Entry的效率。此方法的返回值是整理后的run的末尾Slot的Index。

注:Index(Cal)表明根据它的key计算的本来应在的Index,Index(Cur)表明其目前所在的根据线性探测后找到的位置Index。

未雨绸缪,再尝试清除一些冗余---启发式清理cleanSomeSlots

在replaceStaleEntry方法中,expungeStaleEntry和cleanSomeSlots都是成对出现的,expungeStaleEntry会将返回的当前run末尾的slot传递给cleanSomeSlots,cleanSomeSlots会尝试向后扫描logn次,若是发现了stale Entry那么将n置为table的长度len,作一次连续段的清理(expungeStaleEntry)(这里n是用来进行scan control的,初始值就为table的长度len,重置为table的长度len,意味着循环不会退出,会继续扫描下去,直到连续扫描(logn)+1次都没有碰见staleEntry)。若是至少有一个stale Entry被成功清理了,那么cleanSomeSlots就返回true不然就返回false。

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 = len;
              removed = true;
              i = expungeStaleEntry(i);
          }
      } while ( (n >>>= 1) != 0);
      return removed;
}
复制代码

这个方法除了会在replaceStaleEntry中expungeStaleEntry清理完成后调用,也会在set方法中当一个新元素添加后调用。

static class ThreadLocalMap {
 private int size = 0;
 private int threshold;
 private void setThreshold(int len) {
    threshold = len * 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 set(ThreadLocal<?> key, Object value) {
   ......
   tab[i] = new Entry(key, value);
   int sz = ++size;
   if (!cleanSomeSlots(i, sz) && sz >= threshold)
                 rehash();
 }
}
复制代码

添加新元素后,会再作一次启发式清理cleanSomeSlots,此时若是没有stale Entry被清理掉,而且size达到了threshold临界值,那么就有容量不够的风险,rehash会再次进行清理扩容。为何cleanSomeSlots清理成功就不须要进行sz >= threshold的判断了呢? 首先咱们来证实一件事情, 咱们把ThreadLocalMap的set方法里面的

int sz = ++size;
复制代码

代码块记为increment_{size},

if (!cleanSomeSlots(i, sz) && sz >= threshold)
              rehash();
复制代码

代码块记为rehash_{conditional}

如今咱们要证实,程序第n次运行完increment_{size}时,size<=threshold总成立(n为全体正整数)。证实过程以下所示:

1.n=1时,size=0+1=1,threshold=16*2/3=10,size<=threshold成立。

2.若n=k时,程序第k次运行完increment_{size}时,size<=threshold成立。 则n=k+1时,根据set代码的逻辑可知,第k次increment_{size}执行完成到k+1次increment_{size}执行完成之间,必定会执行一次rehash_{conditional}

Vfi0HJ.png
综上所述,k+1时也成立

3.因此结论可证 有了这个结论后,很明显就能得知,cleanSomeSlots执行前size<=threshold,若是cleanSomeSlots返回true那么size必定是小于threshold的,因此就不用判断sz >= threshold这个条件了,直接就能够认定不须要rehash。

最后再来看看对开发者暴露的get方法

get方法比较简单,咱们来简单的分析一下:

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();
}

private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
}

protected T initialValue() {
    return null;
}

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);
}

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;
}
复制代码

能够看到get方法也是取得了当前线程内的map,而后使用ThreadLocal对象做为key查找相应的Entry,并返回Entry的value值。 总体逻辑比较简单,有两点须要注意一下:

  1. getEntry计算index后没有直接找到Entry的话会进行线性探测来找具备相应key的Entry,在线性探测的过程当中若是遇见staleEntry那么就顺便调用expungeStaleEntry进行清理,而后继续向后在当前run中查找,若是查找到了就返回Entry,不然就返回null。
  2. 若是没有找到相应的Entry(因为map没初始化或者map初始化了但就是没找到),而且是第一次调用get方法的话,即若是线程先于set(T) 方法第一次调用get方法,那么就会调用setInitialValue方法new一个Entry并肯定一个initialValue(默认是null)放到map里。这个方法最多会被调用一次。能够经过为ThreadLocal建立子类的方式重写initialValue方法的方式改变initialValue,通常来讲使用匿名内部类。以下所示:
ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer> (){
    @Override
    protected Integer initialValue() {
      return new Integer(1);
    }
};
复制代码

再思考的深刻一些

为何线性探测过程当中,有可能具备目标key的Entry必定在当前的run里面呢?

从ThreadLocalMap的set方法代码中咱们能够看出来, 其搜索可能具备目标key的Entry,范围只是局限在根据它的ThreadLocal对象实例计算出的Index值(即其本来应该放置的Index处)处所在的run当中。 首先咱们来思考一下,Entry数组的多个run是如何造成的? 只有两种状况

  1. 插入造成
    VfiggK.png
  2. 由一个run经过清除staleEntry大扫除后造成
    VfiWuD.png
    或是
    Vfi5Ed.png
    接下来咱们来分析一下,具备目标key的Entry在这两种状况下与根据它的ThreadLocal对象实例计算出的本来应在的Index值是否依旧保持在一个run内。 对于状况1这种run造成过程来讲,必定是具备目标key的Entry插入本来计算出的Index处的时候,发现位置已经被其余Entry占用了,进行线性探测找到null slot插入,这种状况下必定是保持在一个run内的。 对于状况2这种run造成过程来讲,在大扫除以前具备目标key的Entry与本来应在的Index处必定是在一个run中的,在大扫除过程当中它们两个之间可能会产生多个null slot,这个时候具备目标key的Entry必定会前移到离本来应在的Index处最近的null slot处(极端状况可能就是本来应在的Index处)。这样来看,大扫除完成后具备目标key的Entry与本来应在的Index处必定是一连串连续的不为NULL也不是staleEntry的正常Entry,因此必定是保持在一个run内的。 综上所述,线性探测过程当中,有可能具备目标key的Entry必定在当前的run里面。

为何ThreadLocal处理哈希冲突要使用线性探测法?

  1. 线性探测法采用数组实现,能够有效地利用CPU缓存行来加速查询速度。
  2. 线性探测法有一个很明显的问题就是在数组的空间愈来愈满的时候,性能会急速降低最坏甚至会到O(n),因此咱们须要随时保持,数组有大量空闲的空间,这样的话在大数据量下就比较浪费内存,不然性能就会不好。可是对于ThreadLocal的使用场景来讲, 咱们在一个程序中并不会使用不少个ThreadLocal,数据量并不大,因此这个问题就被避免了。 综上所述对于ThreadLocal的使用场景来讲,采用线性探测法来处理哈希冲突比较适合。

声明: 本文由CodePlus_小小的我原创,采用 CC BY 3.0 CN协议进行许可。 可自由转载、引用,但需署名做者且注明文章出处。如转载至微信公众号,请在文末添加做者公众号二维码。

Vfmpef.jpg
相关文章
相关标签/搜索