ThreadLocal和InheritableThreadLocal
Posted 爱炒饭
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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++;
以上是关于ThreadLocal和InheritableThreadLocal的主要内容,如果未能解决你的问题,请参考以下文章