ThreadLocal和InheritableThreadLocal

一、引子

最近在研究fps帧率时候了解到可以通过Choreographer刷新帧率的方法来统计界面刷新帧率,在当前应用确实可以,但是却不能监测其他app(比如QQ、微信、酷我音乐),为什么呢?难道说Choreographer不是全局变量。

Choreographer.getInstance()
        .postFrameCallback(new Choreographer.FrameCallback() {
            @Override
            public void doFrame(long l) {
                if (LogMonitor.getInstance().isMonitor()) {
                    LogMonitor.getInstance().removeMonitor();
                }
                LogMonitor.getInstance().startMonitor();
                Choreographer.getInstance().postFrameCallback(this);
            }
        });

点进去Choreographer的getInstance方法发现用到了ThreadLocal,ThreadLocal是线程私有的,所以这里Choreographer不是全局的,只能监测当前应用的帧率。

 

//Choreographer.java
/**
 * Gets the choreographer for the calling thread.  Must be called from
 * a thread that already has a {@link android.os.Looper} associated with it.
 *
 * @return The choreographer for this thread.
 * @throws IllegalStateException if the thread does not have a looper.
 */
public static Choreographer getInstance() {
    return sThreadInstance.get(); //ThreadLocal的get方法
}
……
// Thread local storage for the choreographer.
private static final ThreadLocal<Choreographer> sThreadInstance =
        new ThreadLocal<Choreographer>() {
    @Override
    protected Choreographer initialValue() {
        Looper looper = Looper.myLooper();
        if (looper == null) {
            throw new IllegalStateException("The current thread must have a looper!");
        }
        Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
        if (looper == Looper.getMainLooper()) {
            mMainInstance = choreographer;
        }
        return choreographer;
    }
};

二、ThreadLocal

ThreadLocal不是android特有的,实际上ThreadLocal是java中的类,可以在Thread中使用ThreadLocal来存储当前线程的一些变量。ThreadLocal提供线程局部变量,不同线程创建的ThreadLocal具有独立的值,相互之间不影响。线程A中创建并赋值的ThreadLocal在线程B中是获取不到的(或者获取到为null),看个例子把

private void testThreadLocal() {
    ThreadLocal<String> threadLocal0 = new ThreadLocal<>();
    threadLocal0.set("main_thread");
    Thread t = new Thread(){
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        ThreadLocal<String> threadLocal2 = new ThreadLocal<>();

        @Override
        public void run() {
            super.run();
            threadLocal.set("child thread,1");
            threadLocal2.set("child thread,2");
            Log.d(TAG, "testThreadLocal,threadLocal="+threadLocal.get()+",threadLocal2="+threadLocal2.get()+",threadLocal0="+threadLocal0.get());
        }
    };
    t.start();
    Log.d(TAG, "testThreadLocal,threadLocal0="+threadLocal0.get());
}

运行后log打印如下:

: testThreadLocal,threadLocal0=main_thread
: testThreadLocal,threadLocal=child thread,1,threadLocal2=child thread,2,threadLocal0=nul

 

threadLocal0 是在主线程创建的,在子线程 t 中去获取threadLocal0 获取到的是null,threadLocal 和threadLocal2 是在子线程创建的,可以在子线程正常获取到。为什么可以线程独立呢?ok,看源码!!!

2.1 构造函数

构造函数没什么内容

//ThreadLocal.java
public ThreadLocal() {
}

2.2 set

set方法先获取当前ThreadLocal所处的当前线程,然后获取当前线程对应的ThreadLocalMap,这就解释了为什么ThreadLocal是线程的私有局部变量,B线程无法获取到A线程的ThreadLocal值。从getMap方法可以看出每个线程都保存有一个ThreadLocal.ThreadLocalMap类型的threadLocals变量,ThreadLocalMap是ThreadLocal的静态内部类。 threadLocals变量初始值为null,如果为null则将当前线程和ThreadLocal值作为参数调用createMap方法,否则将当前ThreadLocal和ThreadLocal值作为参数调用ThreadLocalMap的set方法。

//ThreadLocal.java
public void set(T value) {
    Thread t = Thread.currentThread();//获取当前线程
    ThreadLocalMap map = getMap(t); //得到当前线程的ThreadLocalMap
    if (map != null)
        map.set(this, value); 
    else
        createMap(t, value);
}
……

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
//Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;

 

2.2.1 createMap

createMap方法对threadLocals进行初始化,ThreadLocalMap初始化时接收当前ThreadLocal以及ThreadLocal值作为参数。

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

画面切到ThreadLocalMap类的构建函数,ThreadLocalMap内部通过数组来存储ThreadLocal对象,初始数组大小是16,为了避免内存泄漏,这里创建Entry对象时采取弱引用。

//ThreadLocal$ThreadLocalMap
private Entry[] table; //数组
private static final int INITIAL_CAPACITY = 16;
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;
    }
}

这里需要知道一个知识点,一个整数对2的n次方求模的结果和一个整数对2的n次方减一进行按位&的结果是一样的,也就是是成立的,并且按位&操作的时间效率还略优于取模操作%。

(a % (int)Math.pow(2, n)) == (a & ((int)Math.pow(2, n)-1))

在计算ThreadLocal插入到数组table的位置时使用了每次nextHashCode 增加HASH_INCREMENT(0x61c88647)来分散位置,看官方介绍通过这种方式可以减少碰撞概率。同时,threadLocalHashCode 是final类型,初始化赋值后就不再改变,这就保证每个ThreadLocal插入位置是不变的。nextHashCode 是静态变量,在多个ThreadLocal实例中是共享的,这就使得不同的ThreadLocal的threadLocalHashCode 不一样,减少了由于插入位置一样而碰撞的概率。

//ThreadLocal.java

private final int threadLocalHashCode = nextHashCode();//final类型,一次赋值后面不再改变
/**
 * The next hash code to be given out. Updated atomically. Starts at
 * zero.
 */
private static AtomicInteger nextHashCode =
    new AtomicInteger(); //静态变量,

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

然后将当前数组table的实际大小size置为1,接着调用setThreshold方法,和hashmap中的负载因子类似,这里也是为了减少hash碰撞,为了不出现下标碰撞,这里将阈值设为数组大小的2/3,也就是只要数组当前数组table实际大小增到16*2/3=10个的时候就扩容。

//ThreadLocal.java
/**
 * Set the resize threshold to maintain at worst a 2/3 load factor.
 */
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

2.2.2 map.set

如果当前线程之前已经创建过ThreadLocalMap,那么就以当前ThreadLocal和ThreadLocal值为参数调用ThreadLocalMap的set方法。和上面的createMap类似,还是先根据key.threadLocalHashCode和(len-1)进行类似的求模操作获取当前ThreadLocal插入位置,然后遍历当前数组table中的元素如果存在和当前ThreadLocal相同的元素,证明当前ThreadLocal已经在当前数组table中插入过,那么此时不需要新增数组元素,只需要更新下ThreadLocal对应的值就行。如果k == null代表之前的元素已被删除,这个卡槽处于闲置状态,可以将ThreadLocal插入到闲置的卡槽中。 如果for循环结束后还是没有匹配到当前ThreadLocal,那么当前ThreadLocal就是个新家伙,就new一个Entry并将ThreadLocal和ThreadLocal值放入其中,然后将新建的元素插入到数组table的指定位置。

 

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

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table; //数组浅拷贝一份,之前操作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;//ThreadLocal已经存在,替换下值就可以
            return;
        }

        if (k == null) { //如果有元素被删除,就插入到这个废弃的卡槽
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value); //没有合适的卡槽元素,需要新增
    int sz = ++size; //实际size加1
    if (!cleanSomeSlots(i, sz) && sz >= threshold) //判断是否需要扩容
        rehash(); //扩容
}

map.set方法有新增和更新两个作用,如果执行到int sz = ++size这一步就代表新增,那么就要判断ThreadLocalMap是否需要扩容了。当没有脏数据并且当前数组实际大小大于阈值就调用rehash方法。rehash()先去删除陈旧元素然后调用resize()方法以2的倍数扩容,从这里可以看出ThreadLocalMap的大小是2的指数。

//ThreadLocal$ThreadLocalMap
private void rehash() {
    expungeStaleEntries();//清除陈旧元素

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize(); //实际扩容
}
/**
 * Double the capacity of the table.
 */
private void resize() {
    Entry[] oldTab = table; //拷贝一份原来的数组数据
    int oldLen = oldTab.length;
    int newLen = oldLen * 2; //以2的倍数扩容
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e; //新数组赋值
                count++;
            }
        }
    }

    setThreshold(newLen); //设置阈值
    size = count;
    table = newTab;
}

2.3 get

ThreadLocal对象的get方法先获取当前线程,然后获取当前线程对应的ThreadLocalMap,getMap方法在上一节set中已经讲过,不再重复。

//ThreadLocal.java
public T get() {
    Thread t = Thread.currentThread(); //获取程序执行的线程
    ThreadLocalMap map = getMap(t); //获取ThreadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

将ThreadLocal作为参数调用了ThreadLocalMap的getEntry方法,根据ThreadLocal的threadLocalHashCode和ThreadLocalMap的长度获取预期下标,取出指定下标的元素与传入参数ThreadLocal进行比较,如果是同一个对象说明找到了返回该对象,否则执行遗失处理函数getEntryAfterMiss。

//ThreadLocal$ThreadLocalMap
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); //不相等可能有遗失,处理
}

getEntryAfterMiss使用线性探测的方法依次取出指定下标以及后面的元素,比较后面的元素是否和传入的ThreadLocal相等,如果后面元素匹配上了则返回实例;如果发现key为null则调用expungeStaleEntry方法将对应卡槽中的key和value都置为null,expungeStaleEntry还会遍历卡槽坐标后面的元素如果发现null则将key和value都置为null,方便后续垃圾回收;如果没匹配上ThreadLocal则返回null。

//ThreadLocal$ThreadLocalMap
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); //有null数据则将key和value都置为null,方便后续垃圾回收
        else
            i = nextIndex(i, len); //循环取出后面的元素,比较后面的元素是否和传入的ThreadLocal相等
        e = tab[i];
    }
    return null; //没匹配上则返回null
}

map.getEntry(this)分析完了,如果找到了对应的ThreadLocal,那么将其value返回;如果没找到则调用setInitialValue方法看下是否有必要执行初始化创建ThreadLocalMap或者为该ThreadLocal赋值null的动作。

//ThreadLocal.java
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;
}

2.4 remove

remove方法先获取当前线程对应的ThreadLocalMap ,然后将当前ThreadLocal对象作为参数执行对ThreadLocalMap执行remove操作

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

ThreadLocal的remove方法从预期下表开始向后遍历ThreadLocalMap中的元素如果找到匹配的ThreadLocal则先调用Entry 的clear方法,然后调用expungeStaleEntry对Entry 数组中指定位置的key和value置空,并对指定下标后面的元素进行检查,如发现则将对应的key和value也置空。

//ThreadLocal$ThreadLocalMap
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;
        }
    }
}

三、InheritableThreadLocal

上面说到的ThreadLocal用于线程内部保存数据,那么如果想一个线程访问另外一个线程的ThreadLocal变量该如何操作呢?这里就引入了InheritableThreadLocal,顾名思义,子线程可以访问父线程的独有变量。

private void testInheritableThreadLocal() {
    InheritableThreadLocal<String> threadLocal0 = new InheritableThreadLocal<>();
    threadLocal0.set("shan0");
    Thread t = new Thread(){
        @Override
        public void run() {
            super.run();
            Log.d(TAG, "testInheritableThreadLocal,child thread, threadLocal0="+threadLocal0.get());
        }
    };
    t.start();
    Log.d(TAG, "testInheritableThreadLocal,threadLocal0="+threadLocal0.get());
}

log打印如下,可以看到,和上面的testThreadLocal方法打印不同,testInheritableThreadLocal方法的子线程可以访问到父线程的threadLocal0 变量

: testInheritableThreadLocal,threadLocal0=shan0
: testInheritableThreadLocal,child thread, threadLocal0=shan0

那么改下代码,如果子线程对主线程创建的变量修改后在主线程取值会怎么样呢?

private void testInheritableThreadLocal() {
    InheritableThreadLocal<String> threadLocal0 = new InheritableThreadLocal<>();
    threadLocal0.set("shan0");

    Thread t = new Thread(){
        @Override
        public void run() {
            super.run();
            Log.d(TAG, "testInheritableThreadLocal,child thread, threadLocal0="+threadLocal0.get());
            threadLocal0.set("shan_child");
            Log.d(TAG, "testInheritableThreadLocal,child thread, modify threadLocal0="+threadLocal0.get());
        }
    };
    t.start();
    SystemClock.sleep(2000);
    Log.d(TAG, "testInheritableThreadLocal,father threadLocal0="+threadLocal0.get());
}

log打印如下,可以看到虽然子线程可以取到父线程的值,但是却不能修改(或者修改后父线程的变量取出并没有变),为什么呢?

2021-06-12 16:42:48.348 28529-28567/com.zhanxun.myapplication.car.debug D/MainActivity2Tag: testInheritableThreadLocal,child thread, threadLocal0=shan0
2021-06-12 16:42:48.348 28529-28567/com.zhanxun.myapplication.car.debug D/MainActivity2Tag: testInheritableThreadLocal,child thread, modify threadLocal0=shan_child
2021-06-12 16:42:50.349 28529-28529/com.zhanxun.myapplication.car.debug D/MainActivity2Tag: testInheritableThreadLocal,father threadLocal0=shan0

父线程的inheritableThreadLocals调用set赋值会调用createMap方法,createMap方法在InheritableThreadLocal类中被重写,创建ThreadLocalMap时将t.inheritableThreadLocals进行赋值。

//InheritableThreadLocal.java
ThreadLocalMap getMap(Thread t) {
   return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
    t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}

通过new Thread方法创建子线程时会判断inheritThreadLocals 以及parent.inheritableThreadLocals != null是否成立,inheritThreadLocals 是init传过来的为true,parent.inheritableThreadLocals已经在父线程set元素时赋值了,所以parent.inheritableThreadLocals不为null,这样子线程ThreadLocalMap持有parent.inheritableThreadLocals的一个深拷贝,这样初值就有了,同时,子线程修改变量不会影响父进程变量。

//Thread.java
public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize) {
    init(g, target, name, stackSize, null, true); //最后一个参数inheritThreadLocals为true,允许继承
}
   private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        ……
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);//挨个复制父线程的inheritableThreadLocals
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }
//ThreadLocal.java
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value); //深拷贝
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值