Android全面解析之Handler机制(二):ThreadLocal

1,848 阅读7分钟

文章相关内容已授权『郭霖』公众号发布

前言

很高兴遇见你~ 欢迎阅读我的文章。

本文是系列文章的第二部分,主要内容是介绍Handler的内部模式结构与详解ThreadLocal

Handler系列文章共分为6个部分:

  1. 第一部分:系列开篇,从0开始认识Handler;
  2. 第二部分:介绍Handler内部模式,详解关键类ThreadLocal;
  3. 第三部分:解析Handler内部关键类:Message、MeesageQueue、Looper;
  4. 第四部分:解析Handler内部关键类:Handler,同时介绍HandlerThread;
  5. 第五部分:总结Handler,从源码设计角度思考Handler;
  6. 第六部分:Handler常见问题解答;

本文为系列文章的第二部分。读者可前往笔者主页选择感兴趣的部分阅读。

那么,我们开始吧。

正文

Handler内部模式结构

经过前面的介绍对于Hadnler机制已经有了一定的认知,但可能对他内部的模式还不太清楚。这一部分先讲解Handler的大概内部模式,目的是为下面的详解做铺垫,为做整体概念感知。先上图:

Handler机制内部有三大关键角色:Handler,Looper,MessageQueue。其中MessageQueue是Looper内部的一个对象,MessageQueue和Looper每个线程有且只有一个,而Handler是可以有很多个的。他们的工作流程是:

  1. 用户使用线程的Looper构建Handler之后,通过Handler的send和post方法发送消息
  2. 消息会加入到MessageQueue中,等待Looper获取处理
  3. Looper会不断地从MessageQueue中获取Message然后交付给对应的Handler处理

这就是大名鼎鼎的Handler机制内部模式了,说难,其实也是很简单。

ThreadLocal

概述

ThreadLocal是Java中一个用于线程内部存储数据的工具类。

ThreadLocal是用来存储数据的,但是每个线程只能访问到各自线程的数据。我们一般的用法是:

ThreadLocal<String> stringLocal = new ThreadLocal<>();
stringLocal.set("java");
String s = stringLocal.get();

不同的线程之间访问到的数据是不一样的:

public static void main(String[] args){
    ThreadLocal<String> stringLocal = new ThreadLocal<>();
 stringLocal.set("java");
    
    System.out.println(stringLocal.get());
    new Thread(){
        System.out.println(stringLocal.get());
    }
}

结果:
java
null

线程只能访问到自己线程存储的数据。

ThreadLocal的作用

ThreadLocal的特性适用于同样的数据类型,不同的线程有不同的备份情况,如我们这篇文章一直在讲的Looper。每个线程都有一个对象,但是不同线程的Looper是不一样的,这个时候就特别适合使用ThreadLocal来存储数据,这也是为什么这里要讲ThreadLocal的原因

ThreadLocal内部结构

ThreadLocal的内部机制结构如下:

每个Thread,也就是每个线程内部维护有一个ThreadLocalMap,ThreadLocalMap内部存储多个Entry。Entry可以理解为键值对,他的本质是一个弱引用,内部有一个object类型的内部变量,如下:

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

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

Entry是ThreadLocalMap的一个静态内部类,这样每个Entry里面就维护了一个ThreadLocal和ThreadLocal泛型对象。每个线程的内部维护有一个Entry数组,并通过hash算法使得读取数据的速度达到O(1)。由于不同的线程对应的Thread对象不同,所以对应的ThreadLocalMap肯定也不同,这样只有获取到Thread对象才能获取到其内部的数据,数据就被隔离在不同的线程内部了。

ThreadLocal工作流程

那ThreadLocal是怎么实现把数据存储在不同线程中的?先从他的set方法入手:

TheadLocal.class
    
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

逻辑不是很复杂,首先获取当前线程的Thread对象,然后再获取Thread的ThreadLocalMap对象,如果该map对象不存在则创建一个并调用他的set方法把数据存储起来。我们继续看ThreadLocalMap的set方法:

ThreadLocalMap.class

private void set(ThreadLocal<?> key, Object value) {
    // 每个ThreadLocalMap内部都有一个Entry数组
    Entry[] tab = table;
    int len = tab.length;
    // 获取新的ThreadLocal在Entry数组中的下标
    int i = key.threadLocalHashCode & (len-1);
    // 判断当前位置是否发生了Hash冲突
    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;
        }
    }
    // 若当前位置没有其他元素则直接把新的Entry对象放入
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 判断是否需要对数组进行扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

这里的逻辑和HashMap是很像的,我们可以直接使用HashMap的思维来理解ThreadLocalMap:ThreadLocalMap的key是ThreadLocal,value是ThreadLocal对应的泛型。他的存储步骤如下:

  1. 根据自身的threadLocalHashCode与数组的长度进行相与得到下标
  2. 如果此下标为空,则直接插入
  3. 如果此下标已经有元素,则判断两者的ThreadLocal是否相同,相同则更新value后返回,否则找下一个下标
  4. 直到找到合适的位置把entry对象插入
  5. 最后判断是否需要对entry数组进行扩容

是不是和HashMap非常像?和HashMap的不同是:hash算法不一样,以及这里使用的是开发地址法,而HashMap使用的是链表法。ThreadLocalMap牺牲一定的空间来换取更快的速度。具体的Hash算法这里就不再深入了,有兴趣的读者可以阅读这篇文章ThreadLocal传送门

然后继续看ThreadLocal的get方法:

ThreadLocal.class

public T get() {
    // 获取当前线程的ThreadLocalMap
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 根据ThreadLocal获取Entry对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果没找到也会执行初始化工作
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 把获取到的对象进行返回
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

前面讲到ThreadLocalMap其实非常像一个HashMap,他的get方法也是一样的。使用ThreadLocal作为key获取到对应的Entry,再把value返回即可。如果map尚未初始化则会执行初始化操作。下面继续看下ThreadLocalMap的get方法:

ThreadLocalMap.class

private Entry getEntry(ThreadLocal<?> key) {
    // 根据hash算法找到下标
    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);
}

利用ThreadLocal的threadLocalHashCode得到下标,然后根据下标找到数据。没找到则根据算法寻找下个下标。

内存泄露问题

我们会发现Entry中,ThreadLocal是一个弱引用,而value则是强引用。如果外部没有对ThreadLocal的任何引用,那么ThreadLocal就会被回收,此时其对应的value也就变得没有意义了,但是却无法被回收,这就造成了内存泄露。怎么解决?在ThreadLocal回收的时候记得调用其remove方法把entry移除,防止内存泄露。

ThreadLocal总结

ThreadLocal适合用于在不同线程作用域的数据备份

ThreadLocal机制通过在每个线程维护一个ThreadLocalMap,其key为ThreadLocal,value为ThreadLocal对应的泛型对象,这样每个ThreadLocal就可以作为key将不同的value存储在不同Thread的Map中,当获取数据的时候,同个ThreadLocal就可以在不同线程的Map中得到不同的数据,如下图:

ThreadLocalMap类似于一个改版的HashMap,内部也是使用数组和Hash算法来存储数据,使得存储和读取的速度非常快。

同时使用ThreadLocal需要注意内存泄露问题,当ThreadLocal不再使用的时候,需要通过remove方法把value移除。

最后

本文的重点是ThreadLocal。这是个很重要的类,是接下来要介绍的Looper关键类的线程存储方式。下一部分将开始介绍Handler机制的内部关键类。

希望文章对你有帮助。

全文到此,原创不易,觉得有帮助可以点赞收藏评论转发。 笔者才疏学浅,有任何想法欢迎评论区交流指正。 如需转载请私信交流。

另外欢迎光临笔者的个人博客:传送门