ThreadLocal 细节和设计

关键词: 空间换时间、弱引用、开放地址法(线性探测,二次探测,双重探测)


简介

ThreadLocal 是一个线程的本地变量,也就意味着这个变量是线程独有的,是不能与其他线程共享的,这样就可以避免资源竞争带来的多线程的问题,这种解决多线程的安全问题和lock是有本质的区别的。(这里的lock 指通过synchronized 或者Lock 等实现的锁)

Lock

  • 资源是多个线程共享的,所以访问的时候需要加锁
  • 通过时间换空间的做法

ThreadLocal

  • 每个线程都有一个副本,是不需要加锁的
  • 通过空间换时间的做法

使用

ThreadLocal 的使用是非常简单的,看下面的代码

public class Test {
    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        // 设置值
        threadLocal.set("hello world");
        // 获取值
        System.out.println(threadLocal.get());            
    }        
}

源码分析

分析源码之前先画一下ThreadLocal ,ThreadLocalMap 和Thread 的关系

ThreadLocal与Thread关系

java.lang.ThreadLocal#set 方法

public class ThreadLocal<T> {
    
    public void set(T value) {
        Thread t = Thread.currentThread();
        // 获取线程绑定的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            // 第一次设置值的时候创建
            createMap(t, value);
    }
    
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    ...
    
}

createMap 方法只是在第一次设置值的时候创建一个ThreadLocalMap 赋值给Thread 对象的threadLocals 属性进行绑定,以后就可以直接通过这个属性获取到值了。从这里可以看出,为什么说ThreadLocal 是线程本地变量来的了。

值真正是放在ThreadLocalMap 中存取的,ThreadLocalMap 内部类有一个Entry 类,key是ThreadLocal 对象,value 就是你要存放的值,上面的代码value 存放的就是hello word。ThreadLocalMap 和HashMap的功能类似,但是实现上却有很大的不同:

HashMap

  • 数据结构是数组+链表
  • 通过链地址法解决hash冲突
  • Entry内部类的引用都是强引用

ThreadLocalMap

  • 数据结构仅仅是数组
  • 通过开放地址法来解决hash 冲突的问题
  • Entry内部类中的key是弱引用,value是强引用

链地址法

这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。列如对于关键字集合{12,67,56,16,25,37, 22,29,15,47,48,34},我们用前面同样的12为除数,进行除留余数法:

在这里插入图片描述

链地址法优缺点:

  1. 处理冲突简单,且无堆积现象,平均查找长度短
  2. 链表中的结点是动态申请的,适合构造表不能确定长度的情况。
  3. 删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
  4. 指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间。

开放地址法

这种方法的基本思想是一旦发生了冲突,就去寻找下一个空的散列地址(这非常重要,源码都是根据这个特性,必须理解这里才能往下走),只要散列表足够大,空的散列地址总能找到,并将记录存入。

比如说,我们的关键字集合为{12,33,4,5,15,25},表长为10。 我们用散列函数f(key) = key mod l0。 当计算前S个数{12,33,4,5}时,都是没有冲突的散列地址,直接存入(蓝色代表为空的,可以存放数据):

在这里插入图片描述

计算key = 15时,发现f(15) = 5,此时就与5所在的位置冲突。于是我们应用上面的公式f(15) = (f(15)+1) mod 10 =6。于是将15存入下标为6的位置。这其实就是房子被人买了于是买下一间的作法:

在这里插入图片描述

开放地址法优缺点:

  1. 容易产生堆积问题,不适于大规模的数据存储
  2. 散列函数的设计对冲突会有很大的影响,插入时可能会出现多次冲突的现象。
  3. 删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂。

魔数0x61c88647

  • 生成hash code间隙为这个魔数,可以让生成出来的值或者说ThreadLocal的ID较为均匀地分布在2的幂大小的数组中。
public class ThreadLocal {
 
    /**
     * 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);
    }
}
  • 可以看出,它是在上一个被构造出的ThreadLocal的ID/threadLocalHashCode的基础上加上一个魔数0x61c88647的。
  • 这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。
  • 斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。换句话说 (1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是1640531527也就是0x61c88647 。
  • 通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。
  • ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。为了优化效率。

ThreadLocalMap 采用开放地址法原因

  • ThreadLocal 往往存放的数据量不会特别大(而且key 是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低

java.lang.ThreadLocal.ThreadLocalMap#set 方法

private void set(ThreadLocal<?> key, Object value) {   

    Entry[] tab = table;
    int len = tab.length;
    // 计算数组的下标
    int i = key.threadLocalHashCode & (len-1);
    // 循环的结束条件是 e!= null
    // 先通过hash找到数组下标,然后寻找相等的ThreadLocal对象
    // 找不到就往下一个index找,有两种可能会退出这个循环
    // 1. 找到了相同ThreadLocal对象
    // 2. 一直往数组下一个下标查询,直到下一个下标对应的是null 跳出
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        // 如果找到直接设置value值,并返回
        if (k == key) {
            e.value = value;
            return;
        }

        // k==null && e!=null 说明key被垃圾回收了,这里涉及到弱引用
        if (k == null) {
            // 被回收的话就需要替换掉过期的值,把新的值放在这里返回
            replaceStaleEntry(key, value, i);
            return;
        }
    }

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

ThreadLocal内存泄漏?

  • key-弱引用、value-强引用 当jvm内存不足时,会回收key,如果此时value还有引用就不会被回收。所以有内存泄漏的危险。

如何避免ThreadLoacl内存泄漏

  • get、set方法都可能触发清理方法,所以在正常使用来讲,是不会发生内存泄漏。为了避免发生内存泄漏,养成好的习惯在使用完后调用remove。

  • 退一步来讲,线程结束时,也就没有强引用指向ThreadLocalMap了,这样ThreadLocalMap里的元素Entry(key、value)会全部被回收掉。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值