ThreadLocal源码学习笔记

1.ThreadLocal的介绍

ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。
ThreadLocal的使用

// An highlighted block
static final ThreadLocal<T> sThreadLocal = new ThreadLocal<T>();
sThreadLocal.set();
sThreadLocal.get()

2.threadLocal原理

作为一个存储数据的类,关键点就在get和set方法。
ThreadLocal中的get和 set方法的源码

//set 方法
public void set(T value) {
      //获取当前线程
      Thread t = Thread.currentThread();
      //实际存储的数据结构类型
      ThreadLocalMap map = getMap(t);
      //如果存在map就直接set,没有则创建map并set
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value);
  }
  
//getMap方法
ThreadLocalMap getMap(Thread t) {
      //thred中维护了一个ThreadLocalMap
      return t.threadLocals;
 }
 
//createMap
void createMap(Thread t, T firstValue) {
      //实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
      t.threadLocals = new ThreadLocalMap(this, firstValue);
}

Thread源码,thread中持有一个ThreadLocalMap 对象

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

从上面代码可以看出每个线程持有一个ThreadLocalMap对象,ThreadLocalMap是ThreadLocal 中的一个内部类。每一个新的线程Thread存值时都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。
ThreadLocal中内部类ThreadLocalMap的源码

//Entry为ThreadLocalMap静态内部类,对ThreadLocal的若引用
//同时让ThreadLocal和储值形成key-value的关系
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

//ThreadLocalMap构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //内部成员数组,INITIAL_CAPACITY值为16的常量
        table = new Entry[INITIAL_CAPACITY];
        //位运算,结果与取模相同,计算出需要存放的位置
        //threadLocalHashCode比较有趣
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
}

通过上面的代码不难看出在实例化ThreadLocalMap时创建了一个长度为16的Entry数组。通过hashCode与length位运算确定出一个索引值i,这个i就是被存储在table数组中的位置。

前面讲过每个线程Thread持有一个ThreadLocalMap类型的实例threadLocals,结合此处的构造方法可以理解成每个线程Thread都持有一个Entry型的数组table,而一切的读取过程都是通过操作这个数组table完成的。

例如使用时

//在某一线程声明了三种类型的ThreadLocal
ThreadLocal<Student> threadLocalA = new ThreadLocal<Student>();
threadLocalA.set(new Student("java"))
ThreadLocal<Student> threadLocalB = new ThreadLocal<Student>();
threadLocalB.set(new Student("c"))
ThreadLocal<Student> threadLocalC = new ThreadLocal<Student>();
threadLocalC.set(new Student("php"))

结合上面threadLocal的set方法的源码,调用threadLocal的set方法存值时,执行的是map.set(this, value),所以是将值存进ThreadLocalMap中的数组中的。这里三个ThreadLocal对象存储值时,都在一个线程,所以存储的值是在同一个ThreadLocalMap中的同一个Entry数组中,只是存在不同位置。
关于存储在数组哪个位置,看ThreadLocalMap中的set方法源码

  //ThreadLocalMap中set方法。
  private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            //获取索引值,这个地方是比较特别的地方
            int i = key.threadLocalHashCode & (len-1);

            //遍历tab如果已经存在则更新值
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                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;
            //满足条件数组扩容x2
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

可以看到在ThreadLocalMap中的set方法与构造方法能看到以下代码片段。
int i = key.threadLocalHashCode & (len-1)
简而言之就是将threadLocalHashCode进行一个位运算(取模)得到索引i。
threadLocalHashCode是threadLocal中的成员变量

    //ThreadLocal中threadLocalHashCode相关代码.
    
    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        //自增
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

也就是说,每次new ThreadLocal对象时都会初始化一个threadLocalHashCode值,而这个东西是每次创建都会自增一个值增量为0x61c88647,所以不同ThreadLocal对象threadLocalHashCode值不同。

总结:
1.每个线程thread中都持有一个ThreadLocalMap对象;ThreadLocalMap中有一个Entry[]数组;数组的i位置存的tab[i] = new Entry(key, value),其中key是当前ThreadLocal实例对象,value就是存的值,i是ThreadLocal实例化时自增的threadLocalHashCode和数组长度计算得到的角标;
2.每个线程共用同一个ThreadLocalMap对象,即共用一个Entry[]数组,不同的ThreadLocal(例如上面的threadLocalA ,threadLocalB,threadLocalC )存值时存在同一数据不同位置

3.拓展:

3.1.关于索引 int i = key.threadLocalHashCode & (len-1);

这其实是一个取模计算,计算当前ThreadLocal实例在entry数组table中的下标,为了使数组中的元素尽量散开

hashCode = hashCode +HASH_INCREMENT每次new一个元素(threadLocal),hashCode 自增0x61c88647
元素散列位置(数组下标)i = hashCode & (length-1)

使用这个方法来检测该算法的优略

 	private static void hashCode(Integer length){
        int hashCode = 0; 
        for(int i=0;i<length;i++){
            hashCode = i*HASH_INCREMENT+HASH_INCREMENT;//每次递增HASH_INCREMENT
            System.out.print(hashCode & (length-1));//求散列下标,算法公式
            System.out.print(" ");
        }
        System.out.println();
 	}

   	public static void main(String[] args) {
        hashCode(16);//初始化16
        hashCode(32);//后续2倍扩容
        hashCode(64);
    }

7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 --》Entry[]初始化容量为16时,元素完美散列
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0–》Entry[]容量扩容2倍=32时,元素完美散列
7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0 --》Entry[]容量扩容2倍=64时,元素完美散列

3.2.为什么要使元素散列开,直接按照顺序 table[0],table[1] 存值不就行了?

为了提高效率,存值时如果table[i]位置被占了,要尽快的找到下一个能用的空位,例如上面的存储顺序table[7],table[14],如果table[7]位置被占了,就会去找table[8]位置,如果空缺,就使用table[8]。如果使用 table[0],table[1]这种方式,在数据多了之后就会导致大量遍历数组,效率低下。取值的时候也是一样,快速定位到 table[i]位置,没有就找隔壁位置。

3.3.table[i]位置被占怎么导致的,及快速找下一个空位

public static void main(String[] args) {

        new Thread("新线程") {
            public void run() {

                Student student1 = new Student();
                student1.setName("java");

                ThreadLocal<Student> threadLocal1 = new ThreadLocal<>();
                threadLocal1.set(student1);
                ThreadLocal<Student> threadLocal2 = new ThreadLocal<>();
                ThreadLocal<Student> threadLocal3 = new ThreadLocal<>();
                ThreadLocal<Student> threadLocal4 = new ThreadLocal<>();
                ThreadLocal<Student> threadLocal5 = new ThreadLocal<>();
                ThreadLocal<Student> threadLocal6 = new ThreadLocal<>();
                ThreadLocal<Student> threadLocal7 = new ThreadLocal<>();
                ThreadLocal<Student> threadLocal8 = new ThreadLocal<>();
                ThreadLocal<Student> threadLocal9 = new ThreadLocal<>();
                ThreadLocal<Student> threadLocal10 = new ThreadLocal<>();
                ThreadLocal<Student> threadLocal11 = new ThreadLocal<>();
                ThreadLocal<Student> threadLocal12 = new ThreadLocal<>();
                ThreadLocal<Student> threadLocal13 = new ThreadLocal<>();
                ThreadLocal<Student> threadLocal14 = new ThreadLocal<>();
                ThreadLocal<Student> threadLocal15 = new ThreadLocal<>();
                ThreadLocal<Student> threadLocal16 = new ThreadLocal<>();

                ThreadLocal<Student> threadLocal17 = new ThreadLocal<>();
                threadLocal17.set(student1);
            }
        }.start();

    }

ThreadLocalMap中的table数据初始化长度为16,第一次new ThreadLocal对象,并且set值,这时根据i = hashCode & (length-1)会生成索引i=10,然后每new一个ThreadLocal对象,hashCode 增加,table数组长度并没有扩容(用3/4时扩容),第17个ThreadLocal对象就会生成的索引和第一个样了。

如图断点调试
第一个threadLocal存值
第一个threadLocal存值
创建ThreaLocalMap 对象
创建ThreaLocalMap 对象
第一个存的位置i =10
第一个存的位置i =10
第17个threadLocal塞值
第17个threadLocal塞值
已有ThreadLocalMap 直接调用set方法
已有ThreadLocalMap 直接调用set方法
发现索引 i =10,和threandLocal1 的索引一样,这时就会往后遍历数组,索引 i+1,如果该位置有值但是key和本次的threandLocal17一样,就替换原值,如果该位置没值,就直接使用
发现索引 i =10,和threandLocal1 的索引一样,这时就会往后遍历数组,索引 i+1,如果该位置有值但是key和本次的threandLocal17一样,就替换原值,如果该位置没值,就直接使用
发现 i 变成了11
发现 i 变成了11,快速的找到了下一个空位

参考
https://www.jianshu.com/p/3c5d7f09dfbd
https://blog.csdn.net/qq_41652863/article/details/96132710

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值