ThreadLocal解析

作用

ThreadLocal类用来提供线程内部的局部变量。一般ThreadLocal实例都是private static类型的。他会为每个线程提供一个独立的变量副本,解决了变量并发访问的冲突问题。ThreadLocal提供给各个线程的变量都是独立的,不会相互影响,是线程隔离的。

结构

现在的ThreadLocal是在每个子线程中维护一个TheadLocalMap,key保存的是TheadLocal,value是线程中需要用到的副本变量。然而早先的ThreadLocal是在ThreadLocal下维护TheadLocalMap,然后map中存的key是Thead,value是线程中需要用到的副本变量。

新的设计的好处:

1.每个Map存储的Entry数量变少。(因为新版的Entry数量取决于ThreadLocal的数量,而老版的Entry数量取决于Thread的数量,一般情况下ThreadLocal数量小于Thread数量)

2.当线程销毁的时候,ThreadLocalMap也会被回收,可以减少内存使用

核心方法

  • protected T initialValue()        返回当前线程局部变量的初始值
  • public void set(T value)          设置当前线程绑定的局部变量
  • public T get()                          获取当前线程绑定的局部变量
  • public void remove()              移除当前线程绑定的局部变量

set:获取当前线程,通过当前线程获取一个map,如果map为空,则为该线程创建一个map,然后我们给map赋值,key为TheadLocal的引用,value为赋给该线程的局部变量。

get:获取当前线程,通过当前线程获取一个map,如果map不为空,则以ThreadLocal的引用作为key在map中获取对应的Entry e,如果e不为空,则get返回e.value。如果map为空或者Entry为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为key,value创建一个Map,get返回value。

remove:获取当前线程,通过当前线程获取一个map,如果获取的map不为空,则移除当前ThreadLocal对象对应的Entry。

initialValue:返回该线程局部变量的初始值。当我们使用ThreadLocal,未调用set方法先调用了get方法时会执行initialValue为线程创建一个map,因为map不为空了,所以之后再调用get就不会再执行initialValue。initialValue默认返回null,如果我们想要修改初始值,可以重写此方法。

ThreadLocalMap内部结构

TheadLocalMap内部有一个table是一个Entry类型的数组,数组的初始容量是INITIAL_CAPACITY,也有扩容阈值。Entry的key只能是ThreadLocal对象,这个Entry是继承自WeakReference<ThreadLocal<?>>,那也就是说key(ThreadLocal)是弱引用,这样可以将ThreadLocal对象的生命周期和线程的生命周期解绑。

ThreadLocalMap的hash相关

  private final int threadLocalHashCode = nextHashCode();

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

  private static AtomicInteger nextHashCode = new AtomicInteger();

  private static final int HASH_INCREMENT = 0x61c88647;

注:上面的AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,适合高并发情况下的使用。

这里获取到当前值会加上HASH_INCREMENT,这个HASH_INCREMENT的值和斐波那契数列(黄金分割数)有关,其目的是为了让哈希码能均匀的分布在2的n次方的数组里,可以减少hash冲突。

TheadLocalMap中通过 hashcode& (size - 1)  确定Entry在数组中的位置,这个方式在size为2的n次方时等价于 hashCode % size,但是却更加高效。而且2的n次方减1的二进制每一位都是1,这也能减少hash冲突的发生,因此TheadLocalMap的容量是2的整次幂。

ThreadLocalMap中解决Hash冲突采用的是线性探测法。比如计算key的hash是5但是table[5]上已经有值,并且key与当前key不同,则会去table[6]上查看,如果还是被占用则去table[7]...,如果超出数组则从table[0]到table[1]....,依次往下找,直到发现一个table上为空或者table上key与当前key相等,将值放入。

ThreadLocal的内存泄漏

TheadLocalMap中的Entry下的key是弱引用,当gc来的时候key会变成null,但是(当前线程->Map->Entry)的这条强引用链还在,此时value和Entry是不会被回收的,然后key却成了null,那么这个value将无法被使用,从而导致内存泄漏。:

避免内存泄漏的两种方式:

  1. 使用完ThreadLocal,调用它的remove方法删除对应的Entry
  2. 使用完ThreadLocal,当前线程也随之结束

既然map的key使用弱引用无法解决内存泄漏为什么还要使用呢?

因为ThreadLocalMap的set/getEntry方法中,会对key是否为null进行判断,如果为null则会将value也置为null。这么一来,当用完ThreadLocal,而当前线程还在运行下,就会在我们忘记调用remove方法情况下多一层保障。

ThreadLocal和synchronized的区别

synchronized的同步是以时间换空间,只提供一个变量,然后不同线程排队访问。而ThreadLocal采用 空间换时间的方式。为每一个线程提供了一份变量的副本,各个线程操作这个变量互不影响。synchronized注重同步访问资源,而ThreadLocal是使每个线程的数据相互隔离。相比之下,ThreadLocal在高并发下有更好的性能。

使用举例

    public static void test() {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Person p = PersionManager.getPerson();
                System.out.println("a1="+p.getName());
                p.setName("小鱼");
                System.out.println("a2="+p.getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("a3="+p.getName());
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                Person p = PersionManager.getPerson();
                System.out.println("b1="+p.getName());
                p.setName("小李");
                System.out.println("b2="+p.getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("b3="+p.getName());
            }
        });
        t2.start();
    }


class PersionManager{
    static ThreadLocal<Person> threadLocal = new ThreadLocal<>();
    public static Person getPerson(){
        Person person = threadLocal.get();
        if(person == null){
            System.out.println("p null");
            person= new Person("小明", "男");
            threadLocal.set(person);
        }else{
            System.out.println("p not null");
        }
        return person;
    }
}

class Person {
    private String name;
    private String sex;

    public Person(String name, String sex) {
        this.name = name;
        this.sex = sex;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
}
上面的案例输出为:
p null
p null
b1=小明
b2=小李
a1=小明
a2=小鱼
b3=小李
a3=小鱼

由上面输出可知,if(person==null)进入了两次,说明ThreadLocal为每个子线程都创建了一个Person。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值