ThreadLocal

1.什么是ThreadLocal

线程内部的数据存储类,通过它可以在指定的线程中存储数据,对数据存储后,只有在线程中才可以获取到存储的数据,对于其他线程来说是无法获取到数据。

  • 每个线程线程内部都有一个Map。
     ThreadLocal.ThreadLocalMap threadLocals = null;
  • Map里面存储线程本地对象(键)和线程的变量副本(值)
  • 但是,线程内部的Map是由ThreadLocal中维护的,由ThreadLocal的负责向Map获取和设置线程的变量值。

所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

2.ThreadLocal核心方法及源码

public T get() //用于获取当前线程的副本变量值。
public void set(T value) //用于保存当前线程的副本变量值。
public void remove() //删除当前前程的副本变量值。

2.1 get方法

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); //得到当前线程的map(threadLocals)
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);//这里this代表的是当前ThreadLocal
        if (e != null) {
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}


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

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

步骤:

  1. 获取当前线程的ThreadLocalMap对象threadLocals
  2. 从map中获取线程存储的KV Entry节点
  3. 从入口节点获取存储的值副本值返回
  4. map为空的话返回初始值null,即线程变量副本为空。

2.2 set方法

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



void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
  1. 获取当前线程的成员变量map 

  2. map非空,则重新将ThreadLocal和新的价值副本放入到map中

  3. map空,则对线程的成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal的和值副本放入map中。

2.3 remove方法

public void remove() {
    ThreadLocal.ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}


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


private void remove(ThreadLocal<?> key) {
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}
  1. 获取当前线程的成员变量map 

  2. map非空,则删除在map中键值

3. ThreadLocalMap

ThreadLocalMap是ThreadLocal中的内部类,是一个特别的map没有实现map接口,用独立的方式实现了map的功能,其内部的入口也独立实现。

在ThreadLocalMap中,也是用输入来保存KV结构数据的。但是进入中关键只能是ThreadLocal的对象,这点被输入的构造方法已经限定死了。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

继承自WeakReference的(弱引用,生命周期只能存活到下次GC前,在《垃圾回收机制(GC)》一文中有说明),

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value
永远无法回收,造成内存泄露。

ThreadLocalMap设计时的对上面问题的对策,getEntry函数的流程大概为:

  1. 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (table.length-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
  2. 如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry。否则,如果key值为null,则擦除该位置的Entry,并继续向下一个位置查询。在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。仔细研究代码可以发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。
      但是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的getEntry函数或者set函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。

4.解决哈希冲突

和HashMap中的最大的不同在于,ThreadLocalMap结构非常简单,没有下一个引用,也就是说ThreadLocalMap中解决哈希冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始密钥的哈希码值确定元素在表数组中的位置,如果发现这个位置上已经有其他关键值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

ThreadLocalMap解决散列冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。


private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}



private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

显然ThreadLocalMap采用线性探测的方式解决哈希冲突的效率很低,如果有大量不同的ThreadLocal的对象放入地图中时发送冲突,或者发生二次冲突,则效率很低。所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到地图中的重点都是相同的ThreadLocal的,如果一个线程要保存多个变量,就需要创建多个ThreadLocal中,多个ThreadLocal中放入地图中时会极大的增加哈希冲突的可能。



 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值