0%

Android-Handle---消息分发机制(ThreadLocal)

创建handler的背景

Android应用启动App的线程定义为UI线程(主线程),由于为了保证系统流畅性,用户页面不能被阻塞,除UI线程外其他线程不能做更新UI的操作,但是由于Java多线程通信又都是堵塞方法,所以就得设计一套工作线程和主线程可以兼容的方法,目的无非2个,工作线程和UI线程的多线程通信,UI线程不能被堵塞问题。

Handler 就是为了创建Android系统中UI线程和工作线程的多线程通信机制

Handler 不是独立线程,因为它的引用会在别的线程作为发送端,也就是Handler 本身就是多线程共享引用,Handler需要一个独立在线程内部的切私有的类帮助它接受信息 – Looper。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
->Looper

public static void loop() {
for (;;) {
//1、取消息
Message msg = queue.next(); // might block
...
//2、消息处理前回调
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
...

//3、消息开始处理
msg.target.dispatchMessage(msg);// 分发处理消息
...

//4、消息处理完回调
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
}
...
}

由于loop循环存在,所以主线程可以长时间运行。如果想要在主线程执行某个任务,唯一的办法就是通过主线程Handler post一个任务到消息队列里去,然后loop循环中拿到这个msg,交给这个msg的target处理,这个target是Handler.

导致卡顿的原因可能有两个地方

注释1的queue.next()阻塞,–
注释3的dispatchMessage耗时太久。 –

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Message next() {
for (;;) {
//1、nextPollTimeoutMillis 不为0则阻塞
nativePollOnce(ptr, nextPollTimeoutMillis);

synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
// 2、先判断当前第一条消息是不是同步屏障消息,
if (msg != null && msg.target == null) {
//3、遇到同步屏障消息,就跳过去取后面的异步消息来处理,同步消息相当于被设立了屏障
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}

//4、正常的消息处理,判断是否有延时
if (msg != null) {
if (now < msg.when) {
//3.1
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
//5、如果没有取到异步消息,那么下次循环就走到1那里去了,nativePollOnce为-1,会一直阻塞
// No more messages.
nextPollTimeoutMillis = -1;
}
}

next方法的大致流程是这样的:

MessageQueue是一个链表数据结构,判断MessageQueue的头部(第一个消息)是不是一个同步屏障消息,所谓同步屏障消息,就是给同步消息加一层屏障,让同步消息不被处理,只会处理异步消息;

如果遇到同步屏障消息,就会跳过MessageQueue中的同步消息,只获取里面的异步消息来处理。如果里面没有异步消息,那就会走到注释5,nextPollTimeoutMillis设置为-1,下次循环调用注释1的nativePollOnce就会阻塞;

如果looper能正常获取到消息,不管是异步消息或者同步消息,处理流程都是一样的,在注释4,先判断是否带延时,如果是,nextPollTimeoutMillis就会被赋值,然后下次循环调用注释1的nativePollOnce就会阻塞一段时间。如果不是delay消息,就直接返回这个msg,给handler处理;

从上面分析可以看出,next方法是不断从MessageQueue里取出消息,有消息就处理,没有消息就调用nativePollOnce阻塞,nativePollOnce 底层是Linux的epoll机制,这里涉及到一个Linux IO 多路复用的知识点

作者:蓝师傅
链接:https://juejin.cn/post/6973564044351373326
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Class Name DES MORE
Message 消息 使用了享元设计模式 链表的数据结构 详情
MessageQuene 消息队列 单链表的数据结构 优先级的队列 — 根据时间先后顺序排队的单链表
Handler message的处理者 即线程间传递的对象,传递的信息包含在其中 Handler的构造函 ,在构造函数中初始化了一个Looper 和 MessageQueue。
ThreadLocal 数据结构是键值对 只有在指定的线程可以获取到存储的数据 获取线程唯一的变量 Theadlocal value 线程内部的数据存储类,使用场景:当某些数据是以线程为作用域,并且不同线程具有不同的数据副本
Looper 循环器 code_looper 类Looper的prepare的函数,即是对Looper进行了初始化,将Looper对象引用保存在sThreadLocal中,先保证了Looper和Threadlocal-1V1关系,由于sThreadLocal获取的值是通过获取当前线程获取线程唯一的变量,这样就保证了一个线程只有一个looper

参考文献

技术小黑屋
简书-InheritableThreadLocal

引言

1
事物的创造从未有应该或不应该,但被创造出来后如何被人利用却成了最大的问题。

我觉得这个话应用到编程上也是一样的,把自身关注点放到事物是什么背景下创造出来的,了解事物的出生,发展,迭代,衰亡,站在巨人的肩膀上看问题。

深刻理解事物的本质和特性,这也是导致变化的原因,本质影响结果的走向,特性影响过程的变化。
无法对现象作出最终解释的理论都是无用的,因为其不能反映客观事实!

ThreadLocal 简述

ThreadLocal是一个本地线程副本变量工具类,主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰。

ThreadLocal就是提供给每个线程操作变量的工具类,做到了线程之间的变量隔离目的。

TheadLocal 原理及应用场景

  • ThreadLocal主要有2大特性
  • 全局性 线程内可访问
  • 唯一性 使用ThreadLocal维护变量时,每个线程都会获得该线程独享一份变量副本。

这里借鉴下大神的图
code_looper


1.每个Thread线程内部都有一个ThreadLocalMap。
2.Map里面存储线程本地对象ThreadLocal(key)和线程的变量副本(value)。
3.Thread内部的Map是由ThreadLocal维护,ThreadLocal负责向map获取和设置线程的变量值。
4.一个Thread可以有多个ThreadLocal。

Thread
当我们初始化一个线程的时候,其内部干去创建了一个ThreadLocalMap的Map容器待用

1
2
3
4
5
6
class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */

ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocal

1
2
3
4
5
6
7
8
9
10
11
Sets the current thread's copy of this thread-local variable to the specified value. 
Most subclasses will have no need to override this method, relying solely on the {@link #initialValue} method to set the values of thread-locals.

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
1
2
3
4
5
Get the map associated with a ThreadLocal 

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

到这里其实可以发现真正起到作用的其实这个ThreadLocal内部类ThreadLocalMap
由于ThreadLocalMap的数据结构是Map键值对(K,V),这里的K是本地线程threadLocals,V是线程的变量副本(value)。

但是这样还是不够,我还是没弄明白ThreadLocal是怎么把变量复制到Thread的ThreadLocalMap中的?

直到我去google到这篇文章 😯 😄 😏

当我们初始化一个线程的时候其内部干去创建了一个ThreadLocalMap的Map容器待用

1
2
3
4
5
6
7
class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */

ThreadLocal.ThreadLocalMap threadLocals = null;
}

当ThreadLocalMap被创建加载的时候ThreadLocalMap静态内部类Entry也随之加载,完成初始化动作。
ThreadLocalMap内部类Entry

1
2
3
4
5
6
7
8
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

可能到这里大家多少有些困惑,我们重新整理下流程
当我们在Thread内部调用set方法时:
ThreadLocal
1.会去获取调用当前方法的线程Thread。
2.然后顺其自然的拿到当前线程内部的ThreadLocalMap容器。
3.最后就把变量副本给丢进去。
ThreadLocal(就认为是个维护线程内部变量的工具!)只是在Set的时候去操作了Thread内部的·ThreadLocalMap将变量拷贝到了Thread内部的Map容器中,Key就是当前的ThreadLocal,Value就是变量的副本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
}

由于涉及的类很多,我们这里把架构图拆分下
threadLocal架构图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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();
}

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;
}
获取当前线程的ThreadLocalMap对象
从map中根据this(当前的threadlocal对象)获取线程存储的Entry节点。
从Entry节点获取存储的对应Value副本值返回。
map为空的话返回初始值null,即线程变量副本为null。

Android十万个为什么

Key的弱引用问题

1
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.

为了处理非常大和生命周期非常长的线程(usages),哈希表使用弱引用作为 key。

ThreadLocal在没有外部对象强引用时如Thread,发生GC时弱引用Key会被回收,而Value是强引用不会回收,如果创建ThreadLocal的线程一直持续运行如线程池中的线程,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

key 如果使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

通常ThreadLocalMap的生命周期跟Thread(注意线程池中的Thread)一样长,如果没有手动删除对应key(线程使用结束归还给线程池了,其中的KV不再被使用但又不会GC回收,可认为是内存泄漏),一定会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal会被GC回收,不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除,Java8已经做了上面的代码优化。

总结
每个ThreadLocal只能保存一个变量副本,如果想要一个线程能够保存多个副本以上,就需要创建多个ThreadLocal。
ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

应用场景

再看看内部的剖析图
ThreadLocal结果图

ThreadLocal底层原理是线程内部维护了ThreadLocalMap,至于怎么加载ThreadLocalMap,上面已经有详细的解释了,这里我们不做太多的解释,我们把 Threadlocal 对象作为 key,要存储的的数据作为 value ,这里Threadlocal的对象作为线程局部变量的入口。
再扩展一下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static class ThreadLocalMap {

// Map中的Entry对象,弱引用类型,key是ThreadLocal对象,value是线程局部变量
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

// 初始化容量16,必须是2的幂次方
private static final int INITIAL_CAPACITY = 16;

// 存储数据的数组,可扩容,长度必须是2的幂次方
private Entry[] table;

// table数组的大小
private int size = 0;

// table数组的阈值,达到则扩容
private int threshold; // Default to 0
}


再看demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ThreadLocalTest {
private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

public static void main(String[] args) {
// 设置线程局部变量
THREAD_LOCAL.set("我是你爹");
// 使用线程局部变量
peelChenpi();
// 删除线程局部变量
THREAD_LOCAL.remove();
// 使用线程局部变量
peelChenpi();
}

public static void peelChenpi() {
System.out.println(THREAD_LOCAL.get());
}
}

输出结果

1
2
我是你爹
null

我们需要从已知的开始反推。
为什么 ThreadLocalMap 内部存储机构是维护一个数组呢?因为一个线程是可以通过多个不同的 ThreadLocal 对象来设置多个线程局部变量的,这些局部变量都是存储在自己线程的同一个 ThreadLocalMap 对象中。通过不同的 ThreadLocal 对象可以取得当前线程的不同局部变量值。

作者:熬夜不加班
链接:https://www.jianshu.com/p/4c3e54656f4a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

ThreadLocal线程安全问题

ThreadLocal 类中的所有方法都是没有加锁的,因为 ThreadLocal 最终操作的都是对当前线程的 ThreadLocalMap 对象进行操作,既然线程处理自己的局部变量,就肯定不会有线程安全问题

思考之外

Thread 的 threadLocals 变量是默认访问权限的,只能被同个包下的类访问,所以我们是不能直接使用 Thread 的 threadLocals 变量的,这也就是为什么能控制不同线程只能获取自己的数据,达到了线程隔离。Threadlocal 类是访问它的入口。

ThreadLocal内存泄露

线程的生命周期都比较长,加上现在普遍使用的线程池,会让线程的生命更加长。
不remove,当然不会释放。这和Key,到底是不是弱引用,关系不大。
严格来说,ThreadLocal没有内存泄漏问题。有的话,那就是你忘记执行remove方法。这是不正确使用引起的。

InheritableThreadLocal