Android消息机制之ThreadLocal的工作原理

news/2024/7/7 5:43:32 标签: ThreadLocal, handler, 消息机制, 消息队列

转载自:https://blog.csdn.net/singwhatiwanna/article/details/48350919
从开发的角度来说,Handler是Android消息机制的上层接口,这使得开发过程中只需要和Handler交互即可。Handler的使用过程很简单,通过它可以轻松的将一个任务切换到Handler所在的线程中去执行。很多人认为Handler的作用是更新UI,这说的的确没错,但是gengxUI仅仅是Handler的一个特殊的使用场景,具体来说是这样的:有时候需要在子线程中进行耗时的IO操作,可能读取文件或者访问网络,当耗时操作完成之后,可能需要在UI上做一些改变,由于Anderoid开发规范的限制,我们并不能在子线程中访问UI控件,否则就会触发程序异常,这个时候通过Handler就可以将更新Ui的操作切换到主线程中执行,因此,本质上来说,Handler并不是专门用于更新UI的,它只是常被大家用来更新UI。
Android的消息机制主要是指Handler的运行机制,Handler的运行需要底层的MessageQueue和Looper的支撑。MessageQueue是消息队列,通过单链表的数据结构来维护消息列表。Handler是消息辅助类,向消息池中发送各种消息事件和处理消息事件。Looper不断从消息队列中读取消息,按分发机制将消息分发给目标处理者。

Looper中还有一个特殊的概念,就是ThreadLocalThreadLocal并不是线程,他的作用是可以在每个线程中存储数据。 Handler创建的时候会采用当前线程的Looper来构造消息循环系统,那么handler内部如何获取到当前线程的Looper呢?这需要使用Threadlocal,Threadlocal可以在不同的线程之中互不干扰地存储并提供数据,通过Threadlocal可以轻松获取每个线程的Looper。需要注意的是,线程时默认没有Looper的,如果需要使用Handler就必须为线程创建Looper。而Ui线程被创建时就会初始化looper,这也是在主线程中默认可以使用handle的原因。

ThreadLocal

ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其他线程来说无法获取到数据。在日常开发中用到ThreadLocal的地方比较少,但是在某些特殊的场景下,通过ThreadLocal可以轻松的实现一些看起来很复杂的功能。这一点在android的源码中也有所体现,比如Looper、ActivityTHread以及AMS中都用到了ThreadLocal。具体到ThreadLocal的使用场景,这个不好统一的来描述,一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。比如对于Handler来说,它需要获取当前线程的Looper,很显然Looper的作用域就是线程并且不同线程具有不同的Looper,这个时候通过ThreadLocal就可以轻松实现Looper在线程中的存取,如果不采用ThreadLocal,那么系统就必须提供一个全局的哈希表供Handler执行线程的Looper,这样一来就必须提供一个类似于LooperManager的类了,但是系统并没有这么做而是选择了ThreadLocal,这就是ThreadLocal的好处。

ThreadLocal的另一使用场景是复杂场景下的对象传递,比如监听器的传递,有些时候一个线程中的任务过于复杂,这可能表现为函数调用栈比较深以及代码入口的多样性。这种情况下,我们又需要监听器能够贯穿整个过程的执行过程,这个时候可以怎么做呢?其实可以采用ThreadLocal,采用ThreadLocal可以让监听器作为线程内的全局对象而存在,在线程内部只要通过get方法就可以获取到监听器。而如果不采用ThreadLocal,那么我们能想到的可能是如下两种方法:第一种方法是将监听器通过参数的形式在函数调用栈中进行传递,第二种方法是将监听器作为静态变量供线程访问。上面两种方法都是由局限性的。第一种方法的问题是当函数调用栈很深的时候,通过函数参数来传递监听器这几乎是不可能接受的,这会让程序的设计看起来很糟糕。第二种方法是可以接受的,但这种状态是不具有可扩充性的,比如如果同时有两个线程在执行,那么就需要提供两个静态的监听器对象,如果有10个线程在并发执行呢?提供10个静态的监听器对象?这显然是不可思议的,而采用ThreadLocal每个监听器对象都在自己的线程内部存储,就不会有方法2这种问题。

实例

private ThreadLocal<Boolean>mBooleanThreadLocal = new ThreadLocal<Boolean>();

然后分别在主线程、子线程1和子线程2中设置和访问它的值,代码如下。

mBooleanThreadLocal.set(true);
Log.d(TAG, "[Thread#main]mBooleanThreadLocal=" + mBooleanThreadLocal.get());

new Thread("Thread#1") {
    @Override
    public void run() {
        mBooleanThreadLocal.set(false);
        Log.d(TAG, "[Thread#1]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
    };
}.start();

new Thread("Thread#2") {
    @Override
    public void run() {
        Log.d(TAG, "[Thread#2]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
    };
}.start();

在上面代码中,在主线程中设置mBooleanThreadLocal的值为true,在子线程1中设置mBooleanThreadLocal的值为false,在子线程2中不设置mBooleanThreadLocal的值,然后分别在3个线程中通过get方法区mBooleanThreadLocal的值,运行结果如下:

D/TestActivity(8676):[Thread#main]mBooleanThreadLocal=true

D/TestActivity(8676):[Thread#1]mBooleanThreadLocal=false

D/TestActivity(8676):[Thread#2]mBooleanThreadLocal=null

从上面日志可以看出,虽然在不同线程中访问的是同一个ThreadLocal对象,但是他们通过ThreadLocal来获取到的值却是不一样的,这就是ThreadLocal的奇妙之处。因为不同线程访问同一个THreadlocal的get方法,THreadLocal内部会从格子的线程中取出一个数组,然后再从数组中根据当前ThreadLocal的索引去查找出对应的value值。很显然,不同的线程中数组是不同的,这就是为什么通过ThreadLocal可以在不同的线程中维护一套数据的副本并且彼此互不干扰。

Threadlocal内部实现

public void set(T value) {
    Thread currentThread = Thread.currentThread();
    Values values = values(currentThread);
    if (values == null) {
        values = initializeValues(currentThread);
    }
    values.put(this, value);
}

在set方法中,首先要通过values方法来获取当前线程中的Threadlocal数据,在Thread类的内容中有一个专门的成员用于存储线程的Threadlocal的数据,如下所示:TreadLocal.Values localValues.因此获得当前线程的ThreadLocal数据变得异常简单了。如果localValues的值为null,那么就需要对其初始化,初始化后再将ThreadLocal的值进行存储。下面看下THreadLocal的值到底是怎么在localValues中进行存储的。在localValues内部有一个数组:private Object[] table,ThreadLocal的值就是存在这个table数组中,下面看下localValues是如何使用put方法将ThreadLocal的值存储到table数组中的,如下所示:

void put(ThreadLocal<?> key, Object value) {
    cleanUp();

    // Keep track of first tombstone. That's where we want to go back
    // and add an entry if necessary.
    int firstTombstone = -1;

    for (int index = key.hash & mask;; index = next(index)) {
        Object k = table[index];

        if (k == key.reference) {
            // Replace existing entry.
            table[index + 1] = value;
            return;
        }

        if (k == null) {
            if (firstTombstone == -1) {
                // Fill in null slot.
                table[index] = key.reference;
                table[index + 1] = value;
                size++;
                return;
            }

            // Go back and replace first tombstone.
            table[firstTombstone] = key.reference;
            table[firstTombstone + 1] = value;
            tombstones--;
            size++;
            return;
        }

        // Remember first tombstone.
        if (firstTombstone == -1 && k == TOMBSTONE) {
            firstTombstone = index;
        }
    }
}

上面的代码实现数据的存储过程,这里不去分析它的具体算法,但是我们可以得出一个存储规则,那就是ThreadLocal的值在table数组中的存储位置总是为ThreadLocal的reference字段所标识的对象的下一个位置,比如ThreadLocal的reference对象在table数组中的索引为index,那么ThreadLocal的值在table数组中的索引就是index+1,最终ThreadLocal的值将会被存储在table数组中,table[index+1] = value.

get()方法如下所示:

public T get() {
    // Optimized for the fast path.
    Thread currentThread = Thread.currentThread();
    Values values = values(currentThread);
    if (values != null) {
        Object[] table = values.table;
        int index = hash & values.mask;
        if (this.reference == table[index]) {
            return (T) table[index + 1];
        }
    } else {
        values = initializeValues(currentThread);
    }

    return (T) values.getAfterMiss(this);
}

可以发现,ThreadLocal的get方法的逻辑也比较清晰,它同样是取出当前线程的localValues对象,如果这个对象为null那么就返回初始值,初始值由ThreadLocal的initialValue方法来描述,默认情况下为null,当然也可以重写这个方法,它的默认实现如下所示:

/**
 * Provides the initial value of this variable for the current thread.
 * The default implementation returns {@code null}.
 *
 * @return the initial value of the variable.
 */
protected T initialValue() {
    return null;
}

如果localValues对象不为null,那就取出它的table数组并找出ThreadLocal的reference对象在table数组中的位置,然后table数组中的下一个位置所存储的数据就是ThreadLocal的值。

ThreadLocal的set和get方法可以看出,它们所操作的对象都是当前线程的localValues对象的table数组,因此在不同线程中范文同一个ThreadLocal的set和get方法,它们对ThreadLocal所作的读写操作仅限于各自线程的内部,这就是为什么ThreadLocal可以在多个线程中互不干扰地存储和修改数据。


http://www.niftyadmin.cn/n/794868.html

相关文章

Python 关键字参数 (4)

可变参数允许你传入0个或任意个参数&#xff0c;这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数&#xff0c;这些关键字参数在函数内部自动组装为一个dict。请看示例&#xff1a; def person(name, age, **kw):print(name:, …

【01】Java泛型基础

&#xff08;1&#xff09;一个人只要自己不放弃自己&#xff0c;整个世界也不会放弃你. &#xff08;2&#xff09;天生我才必有大用 &#xff08;3&#xff09;不能忍受学习之苦就一定要忍受生活之苦&#xff0c;这是多么痛苦而深刻的领悟. &#xff08;4&#xff09;做难事必…

Python Requests库

简介&#xff1a;通过 requests 类库的学习&#xff0c;理解 HTTP 基本原理&#xff0c;并能够纯熟地使用 requests 和 Github API 进行数据交互。除此之外&#xff0c;你还能够获得诸如 HTTP 认证&#xff0c;Oauth 授权等进阶知识和技能 1. 走进 Requests 库 1.1 Requests …

下载离线 Visual Studio 离线安装包

背景 新的办公室网络很差&#xff08;网速慢 不能翻墙&#xff09;&#xff0c;那边的同事表示没有办法下载 VS 解决方案 找一个网络好的环境&#xff0c;预先下载好完整版 的 VS 离线安装包&#xff0c;下完之后拿 U 盘 拷过去安装&#xff0c;我这边下的是 Visual Studio…

Java SE 063 反射机制大总结

&#xff08;1&#xff09;一个人只要自己不放弃自己&#xff0c;整个世界也不会放弃你. &#xff08;2&#xff09;天生我才必有大用 &#xff08;3&#xff09;不能忍受学习之苦就一定要忍受生活之苦&#xff0c;这是多么痛苦而深刻的领悟 &#xff08;4&#xff09;做难事必…

动态数据添加(DataGrid)

http://www.cnblogs.com/lovecherry/archive/2005/03/25/125526.aspx转载于:https://www.cnblogs.com/JoinZhang/archive/2005/07/29/202714.html

常见加密算法

加密算法分为对称加密、非对称加密和Hash算法。 对称加密 加密和解密使用相同的秘钥的加密算法。优点在于加解密的高速度和使用长秘钥时的难破解性。 常见的对称加密算法有DES、3DES、AES。 AES 高级加密标准(AES,Advanced Encryption Standard)为最常见的对称加密算法(微…

Makefile Project 中 MSBuild MSB3073 error 不能正确检测字符串 “error:

背景 在 VS 中创建 Visual C –> Cross Platform –> Android | Linux –> Makefile Project 后&#xff0c;只是在项目中有这样一段代码&#xff1a; LOGD("data length error: %d", datalen); 便宜的时候就会报错&#xff0c; 1>------ Build star…