深入理解ThreadLocal

ThreadLocal是什么?

ThreadLocal实例为每一个访问它的线程(即当前线程)都关联了一个该线程的线程特有对象

线程特有对象(TSO,Thread Specific Object):各个线程创建各自的实例,一个实例只能被一个线程访问的对象就被称为线程特有对象,相对应的线程就被称为该线程特有对象的持有线程

ThreadLocal的使用

static  ThreadLocal<Integer> local = new ThreadLocal<Integer>() {
       //ThreadLocal<T>内部的初始化方法
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        Thread[] t= new Thread[5];
        for (int i = 0; i < 5; i++) {
            t[i]=new Thread(()->{
                int num=local.get();
                local.set(num+=5);
                System.out.println(Thread.currentThread().getName()+" num="+num);
            });
            t[i].start();
        }
    }

执行结果:
在这里插入图片描述

ThreadLocal源码分析

首先,我们看一下ThreadLocal的所有方法:
在这里插入图片描述
其中红色方框标注的是比较常用的方法。

set(T)方法

咱们先看set(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);
    }

通过Thread.currentThread();获取当前线程,然后通过当前线程去获取当前线程的ThreadLocalMap ,也可以理解为ThreadLocalMap 就是线程的特有对象。
再看看ThreadLocalMap 是如何创建的?

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
}

这个this就是ThreadLocal实例,再接着往下看

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);
}
static class Entry extends WeakReference<ThreadLocal<?>> {
 /** The value associated with this ThreadLocal. */
    Object value;

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

ThreadLocalMap有一个Entry对象数组,而且这个entry是一个(K,V)结构。通过ThreadLocal实例的hashcode值&(数组容量值(INITIAL_CAPACITY) - 1)获得下标。
我们画个图理解一下:
在这里插入图片描述
接下来,我们分析一下当ThreadLocalMap不为空的时候,set()方法是如何赋值的

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)]) {
         ThreadLocal<?> k = e.get();

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

         if (k == null) {
         	//替换脏的Entry
             replaceStaleEntry(key, value, i);
             return;
         }
     }

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

通过ThreadLocal<?> key拿到Entry 数组的下标,然后通过for循环去线性探测,避免hash碰撞冲突

线性探测:属于开放寻址发。而hashMap的底层是使用了链寻址法,即将相同hash值对象组成一个链表

通过下标获得entry对象,然后循环当前下标之后的的entry直到entry为null。通过当前entry拿到k:

  1. 当k == key,直接赋值 e.value = value;
  2. 当k == null,因为entry 是不为空的,所以k为空,value是有值的。这说明ThreadLocal被回收了,因为这里k是弱引用

接下来,我们来分析一下它是如何处理这些脏对象的

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;

            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

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

    // If key not found, put new entry in stale slot
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

这里有两个循环,第一个循环是以当前下标开始,向前循环,直到entry == null,找到最前的一个脏对象(key==null&&value!=null)的下标,并将下标赋值给slotToExpunge
第二个循环是以当前下标开始,向后循环,也就是线性探测,直到entry == null,获取当前entry的k:

  1. 当k == key时,替换value的值e.value = value,然后把脏对象替换当前entry对象
    tab[i] = tab[staleSlot],即当前对象成了脏对象。

    这是为了防止当用一个ThreadLocal进来放数据的时候放到了以前脏数据的位置,从而导致了同一个ThreadLocal有两个entry的问题

    接着判断slotToExpunge 有没有改变。如果没有改变,则将当前下标赋值给slotToExpunge,最后将脏数据清理掉,即value=null,最后执行完毕,退出当前方法;

  2. 当k==null && slotToExpunge == staleSlot,也就是当前entry前后的entry对象都是null,将当前下标赋值给slotToExpunge

最后,就剩下没找到相同的key,则将当前entry清空,重新生成一个entry,插入到当前下标下,同时清理其他脏对象

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

get方法就简单的多了,通过当前线程Thread.currentThread(),得到该线程的ThreadLocalMap

  1. 如果ThreadLocalMap 不为空,通过ThreadLocal去拿取entry的value。
  2. 如果ThreadLocalMap 为空,则将初始话一个ThreadLocal,并将ThreadLocal的初始值插入进去,最后返回。

remove方法

private void remove(ThreadLocal<?> key) {
    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)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

找到所有相同的ThreadLocal,然后将entry清空

ThreadLocal使用场景

  • 需要使用非线程安全对象,但又不希望因此引入锁
  • 使用线程安全对象,但希望避免其作用的锁的开销和其它问题
  • 隐式参数传递,即一个类的方法调用另一个类的方法时,前者向后者传递数据可以借助ThreadLocal而不必通过方法参数传递
  • 特定于线程的单例模式。如果我们希望对于某个类每个线程有且仅有该类的一个实例,就可以使用线程特有对象

总结

  • 每个Thread中只能保存一个对应ThreadLocal的一个值,保存在变量ThreadLocalMap<ThreadLocal,V>;
  • 通过使用线性探测的方式解决了hash冲突问题
  • 实体(entry)继承了·WeakReference·能在不被GCRoot标记时,直接被GC回收,即其坐在entry的key会被置为null,相应的entry也就成了无效条目,从而导致内存泄漏。但是,当ThreadLocalMap有新的ThreadLocal到线程特有对象的映射关系被创建(相当于有新的entry被添加到ThreadLocalMap)的时候,ThreadLocalMap会将无效条目清空,打破了无效条目对于线程特有对象的强引用,从而使相应线程特有对象能够被垃圾回收。但是这样处理有一个缺点------如果一个线程在相当长的时间里一直处于非运行状态,那么该线程的ThreadLocalMap可能就不会有任何变化,因此相应的ThreadLocalMap的无效条目也不会被清理,导致线程特有对象无法被垃圾回收,从而导致了伪内存泄漏
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值