安卓Loop机制剖析之ThreadLocal

45 篇文章 0 订阅
8 篇文章 0 订阅

ThreadLocal是什么

ThreadLocal是Thread的局部变量,用于编多线程程序,对解决多线程程序的并发问题有一定的启示作用,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路,使用这个工具类可以很简洁地编写出优美的多线程程序。

简单使用

解释其实还是很抽象的,下面展示一个demo,来看下threadLocal对象怎么使用的。

 public void threadLocal(){
    final ThreadLocal<String> t=new ThreadLocal<>();
    threadLocal.set("这是主线程threadLocal");
    new Thread(new Runnable() {
        @Override
        public void run() {
            t.set("这是子线程"+Thread.currentThread().getId()+"threadLocal");
            Log.e(TAG,t.get());
        }
    }).start();
    new Thread(new Runnable() {
        @Override
        public void run() {
            t.set("这是子线程"+Thread.currentThread().getId()+"threadLocal");
            Log.e(TAG,t.get());
        }
    }).start();
    Log.e(TAG,t.get());
}

打印结果如下:

15:35:32.942 4294-4294/com.example.zwzd_03.myapplication E/[MainActivity]: 这是主线程threadLocal
15:35:32.943 4294-4386/com.example.zwzd_03.myapplication E/[MainActivity]: 这是子线程1352threadLocal
15:35:32.944 4294-4388/com.example.zwzd_03.myapplication E/[MainActivity]: 这是子线程1353threadLocal

我们定义了一个ThreadLocal对象t,然后在三个不同的线程中对 t 赋值,然后在打印出它们值,打印出来的结果是各自线程中所赋值的值。threadLocal对象的作用就是,每个线程各自用来保存对象的容器,这个容器是线程相关的,各个线程相互隔离,比如有个业务场景是:让子线程进行一个耗时的计算操作,但是线程需要保持计算的过程数据。现在有n个子线程同时进行这样的操作,那么各个线程的计算数据各自保存,不会相互影响。这个类也就满足这样的场景,一批线程中规定的业务场景相同,但是需要保存各自业务场景的数据,这个时候使用ThreadLocal类来保存这些数据最适合不过了。

ThreadLocal如何实现

set方法

当然是从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);
    }

第一步:获取当前线程;
第二步:从t中获取ThreadLocalMap;
第三步:给map设置value 或者创建一个map,在赋值value;

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

可以看出来Thread类中有保存数据的容器类ThreadLocalMap,也就是threadLocals成员变量;

public class Thread implements Runnable {
	...省略
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
	...省略
}

然后就是setValue或者是创建map对象给thread;直接去看下ThreadLocalMap容器是怎么样的。

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

这个map容器是一个INITIAL_CAPACITY大小的Entry数组,看起来是不是很熟悉(和hashmap容器差不多的节点结构),其中key为ThreadLocal对象,value是泛型指定的类型对象,Entry代码如下:

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

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

Entry的Key作为参数传递给弱引用类构造函数,这样Key就会在没有强引用的情况下回收该对象,这样就可以防止Threadlocal对象造成了所在线程的内存泄漏问题。
接着往下看,

      int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
      table[i] = new Entry(firstKey, firstValue);
      size = 1;
      setThreshold(INITIAL_CAPACITY);

使用firstKey的hashcode和(INITIAL_CAPACITY - 1)做与操作,创建一个Entry赋值给table[i],最后设置阈值长度的2/3大小,大于就会进行扩容:

    private void setThreshold(int len) {
         threshold = len * 2 / 3;
     }

ThreadLocal.threadLocalHashCode 等于啥,我们来看下:

    private final int threadLocalHashCode = nextHashCode();
    private static AtomicInteger nextHashCode = new AtomicInteger();
    private static final int HASH_INCREMENT = 0x61c88647;
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

ThreadLocal的hashcode的计算:从0开始,每次创建一个ThreadLocal对象,那么就在nextHashCode 整数上累加HASH_INCREMENT,这样我们就知道了每个ThreadLocal对象他们的hashcode值都不一样,每创建一次hashcode就大一点。

知识点:知道为啥需要长度减一做与吗,因为在容器中长度都是2的指数长度,所以长度的二进制最后一位一定是0,不减一的情况下,与的结果一定是最后一位是0,那么就跳过了连续数组的偶数位位置,造成了空间的浪费,减一是为了让与的结果末位能够随机的1或0。

创建说完了,再来说下设置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) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

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

寻找索引位置还是key的hashcode和长度减一的与的结果,然后一个for循环寻找对应的索引位置i处是否有值,如果key和索引处的k是同一个对象的话,那么就替换value值,如何索引处K是空的,那么直接replaceStaleEntry设置k,v;如果for循环里面都不成立的话,那么说明有值的位置都不能替换,只能填充空的位置,填充完毕之后需要检查阈值并且扩容和重新hash定位。replaceStaleEntry方法有点绕,大概就是在定位的位置i向前遍历找key为空的索引直到遍历的index位置为空,然后基于index索引向后遍历key为空的位置去清理Entry,基于i向后遍历找到k和key相同的索引,找到的话交换索引和i位置的Entry。

get方法

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}

获取当前线程的map容器,容器为空初始化容器,不为空,map根据this对象获取Entry返回value。
看下map的getEntry函数是怎么操作的:

private Entry getEntry(ThreadLocal key) {
      int i = key.threadLocalHashCode & (table.length - 1);
      Entry e = table[i];
      if (e != null && e.get() == key)
          return e;
      else
          return getEntryAfterMiss(key, i, e);
}

根据hashcode去定位map中索引位置,如果Key相同那么直接返回Entry,如果不相同那么调用getEntryAfterMiss方法继续寻找正确的位置,继续:

private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

就是基于hashcode定位的索引i,一直向后遍历找到key相同的位置返回,如果找不到返回null,其中包含回收处理,如果索引处的Key为空的话,那么该处的值无效,弱引用已经没有强引用引用了,需要清理,防止内存泄漏。expungeStaleEntry大概就是根据索引i向后遍历,如果索引处k==null的话,那么进行回收;如果该key不为空,那么进行Entry的hashcode重新计算,一直找到nextIndex找到空位置进行放置。

总结

ThreadLocal对象的set/get方法都是操作的当前线程的ThreadLocalMap对象,所以ThreadLocal并不是存放数据的地方,它只是代理了线程ThreadLocalMap容器的set/get方法。画一个示意图就知道了:
在这里插入图片描述
ThreadLocal的get方法的key是当前的ThreadLocal.hashcode,而容器是所在线程的map,这两者决定了获取的对象是哪个。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值