Android系统分析之ThreadLocal

1 ThreadLocal操作示例

1.1 例子

public class MainActivity extends AppCompatActivity {
   private static final String TAG = "ThreadLoacalTest";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);

        multiThreadOneThreadLocal();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        oneThreadMultiThreadLocal();
    }

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

    /**
     * 测试多个线程使用同一个ThreadLocal
     */
    private void multiThreadOneThreadLocal() {
        Log.d(TAG, "[multiThreadOneThreadLocal]:");
        //main
        mBooleanThreadLocal.set(true);
        printMultiThreadOneThreadLocal();

        new Thread("Thread#1"){
            @Override
            public void run() {
                mBooleanThreadLocal.set(false);
                printMultiThreadOneThreadLocal();
            }
        }.start();

        new Thread("Thread#2"){
            @Override
            public void run() {
                //不设置mBooleanThreadLocal
                printMultiThreadOneThreadLocal();
            }
        }.start();
    }

    private void printMultiThreadOneThreadLocal() {
        Log.d(TAG, "[Thread#" + Thread.currentThread().getName() + "#" + Thread.currentThread().getId() + "]:mBooleanThreadLocal=" + mBooleanThreadLocal.get() );
    }


    private ThreadLocal<String> mStringThreadLocal = new ThreadLocal<>();
    private ThreadLocal<Integer> mIntegerThreadLocal = new ThreadLocal<>();
    private ThreadLocal<Double> mDoubleThreadLocal = new ThreadLocal<>();

    /**
     * 测试一个线程使用多个ThreadLocal
     */
    private void oneThreadMultiThreadLocal() {
        //main
        mStringThreadLocal.set("String");
        mIntegerThreadLocal.set(1);
        mDoubleThreadLocal.set(1.0);

        Log.d(TAG, "[oneThreadMultiThreadLocal]:");
        Log.d(TAG, "[Thread#main]:mStringThreadLocal=" + mStringThreadLocal.get() );
        Log.d(TAG, "[Thread#main]:mIntegerThreadLocal=" + mIntegerThreadLocal.get() );
        Log.d(TAG, "[Thread#main]:mDoubleThreadLocal=" + mDoubleThreadLocal.get() );
    }
}

1.2 结果

//测试多个线程使用同一个ThreadLocal
ThreadLoacalTest: [multiThreadOneThreadLocal]:
ThreadLoacalTest: [Thread#main#1]:mBooleanThreadLocal=true
ThreadLoacalTest: [Thread#Thread#1#162]:mBooleanThreadLocal=false
ThreadLoacalTest: [Thread#Thread#2#163]:mBooleanThreadLocal=null
//测试一个线程使用多个ThreadLocal
ThreadLoacalTest: [oneThreadMultiThreadLocal]:
ThreadLoacalTest: [Thread#main]:mStringThreadLocal=String
ThreadLoacalTest: [Thread#main]:mIntegerThreadLocal=1
ThreadLoacalTest: [Thread#main]:mDoubleThreadLocal=1.0

1.3 结论

  在3个不同的线程中,分别对同一个 ThreadLocal对象进行了操作,但结果互不干扰。
  一个线程使用多个ThreadLocal,取值时互不影响

2 四个问题

2.1 什么是ThreadLocal?

  ThreadLocal类是负责向线程内进行数据存储/读取操作的类。如果定义了一个ThreadLocal,每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的。

2.2 基本实现思路是怎样的?

  Thread类有一个类型为ThreadLocal.ThreadLocalMap的变量threadLocals,在ThreadLocal中定义的ThreadLocalMap变量指向的是Thread类的threadLocals,也就是说每个线程有一个自己的ThreadLocalMap,实现数据存储在各自线程中。  
  ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。
  每个线程在往某个ThreadLocal里set(value)的时候,都会往当前的线程的ThreadLocalMap里存;get()内部其实会先去获取当前的线程对象,然后以当前线程为参数,获取一个ThreadLocalMap对象,在map里以当前ThreadLocal作为key去取值,从而实现了线程隔离。

2.3 如何做到一个线程使用多个ThreadLocal,却可以返回各自ThreadLocal对应的数据呢?

  同一个Thread中,不同ThreadLocal虽然使用同一个ThreadLocalMap的变量threadLocals,但key不一样,取值时也就不会相互影响。

2.2 ThreadLocal和Synchonized区别?

  共同点:都用于解决多线程并发访问。
  Synchronized用于线程间的数据共享(使变量或代码块在某一时该只能被一个线程访问),是一种以延长访问时间来换取线程安全性的策略;
  ThreadLocal则用于线程间的数据隔离(为每一个线程都提供了变量的副本),是一种以空间来换取线程安全性的策略。

3 set(T value)

3.1 通俗的理解

(1)先分配角色

ThreadLocal对象就是:孙大圣
Thread对象就是:土地公
ThreadLocal.ThreadLocalMap threadLocals对象就是:土地公的记事本
Entry[] table就是:记事本里面的页,通过页数可以进行查找。

(1)表演的时刻到了
  孙大圣(ThreadLocal )在女儿国领地内,忽然想起一点事情,但又怕忘了,得找地方记下来,比如他师傅又被抓了。他喊一声“土地”(Thread t = Thread.currentThread()),当地土地公(Thread t )就出现了,孙大圣的脾气比较急,直接一把抓过土地公的记事本(ThreadLocalMap map = getMap(t)),往记事本中写下这件事情(map.set(this, value))。
  在记事本中,他是怎样记录的呢?以孙大圣的行事风格, 他翻开记事本,找到最近的一个空页,记下内容(tab[i] = new Entry(key, value))。

3.2 set的源码

    /**
     * ThreadLocal类
     * 设置此线程局部变量的当前线程的副本到指定值。
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
/**
 * ThreadLocal.ThreadLocalMap内部类
 * ThreadLocal中定义的ThreadLocalMap map变量指向的是Thread类的threadLocals
 */
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
}
/**
 * Thread类
 * 线程的ThreadLocalMap变量由ThreadLocal类来维护,最终设置的值存储在线程中的ThreadLocalMap实例变量中。
 */
ThreadLocal.ThreadLocalMap threadLocals = null;

(1)取得当前的线程。
(2)获取线程里面的ThreadLocal.ThreadLocalMap。
(3)看这个ThreadLocal.ThreadLocalMap是否存在,存在就设置一个值,不存在就给线程创建一个ThreadLocal.ThreadLocalMap。

3.3 创建一个ThreadLocal.ThreadLocalMap分支源码分析

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// 初始容量,必须为2的幂
private static final int INITIAL_CAPACITY = 16;
// Entry表,大小必须为2的幂
private Entry[] table;
// 表里entry的个数
private int size = 0;
// 重新分配表大小的阈值,默认为0
private int threshold; 
/**
 * Entry便是ThreadLocalMap里定义的节点,它继承了WeakReference类,
 * 定义了一个类型为Object的value,用于存放塞到ThreadLocal里的值。
 */
static class Entry extends WeakReference<java.lang.ThreadLocal<?>> {
    // 往ThreadLocal里实际塞入的值
    Object value;
    Entry(java.lang.ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

/**
 * 构造一个包含firstKey和firstValue的map。
 * ThreadLocalMap是惰性构造的,所以只有当至少要往里面放一个元素的时候才会构建它。
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 初始化table数组
    table = new Entry[INITIAL_CAPACITY];
    // 求一个ThreadLocal实例的哈希值(用firstKey的threadLocalHashCode与初始大小16取模得到哈希值)
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 初始化该节点
    table[i] = new Entry(firstKey, firstValue);
    // 设置节点表大小为1
    size = 1;
    // 设定扩容阈值
    setThreshold(INITIAL_CAPACITY);
}
/*
 * 生成hashcode间隙为这个魔数,可以让生成出来的值或者说ThreadLocal的ID较为均匀地分布在2的幂大小的数组中。
 */
private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
/**
 * 设置resize阈值以维持最坏2/3的装载因子
 */
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

(1)这个Map中并没有next节点,它虽然叫做Map,但其实存储的方式不是链表法而是开地址法。看到设置table中的位置的时候,都把一个static的nextHashCode累加一下,这意味着,set的同一个value,可能在每个ThreadLocal.ThreadLocalMap中的table中的位置都不一样,不过这没关系。

3.4 设置的分支map.set(this, value)源码分析

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

    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)]) {
        ThreadLocal<?> k = e.get();
        // 找到对应的entry。同一个ThreadLocal,然后数据就覆盖,返回
        if (k == key) {
            e.value = value;
            return;
        }
        // 替换失效的entry
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) {
        rehash();
    }
}
/**
 * 环形意义的下一个索引
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}
/**
 * 环形意义的上一个索引
 */
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

理一下逻辑,设置的时候做了几步:
1、先对ThreadLocal里面的threadLocalHashCode取模获取到一个table中的位置

2、这个位置上如果有数据,获取这个位置上的ThreadLocal:
(1)判断一下位置上的ThreadLocal和我本身这个ThreadLocal是不是一个ThreadLocal,是的话数据就覆盖,返回

扫描二维码关注公众号,回复: 4740528 查看本文章

(2)不是同一个ThreadLocal,再判断一下位置上的ThreadLocal是是不是空的,这个解释一下。Entry是ThreadLocal弱引用,有可能这个ThreadLocal被垃圾回收了,这时候把新设置的value替换到当前位置上,返回

(3)上面都没有返回,给模加1,看看模加1后的table位置上是不是空的,是空的再加1,判断位置上是不是空的……一直到找到一个table上的位置不是空的为止,往这里面塞一个value。换句话说,当table的位置上有数据的时候,ThreadLocal采取的是办法是找最近的一个空的位置设置数据。

3、为什么Entry要弱引用?
(1)因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点;

(2)弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。

4 get()

4.1 通俗的理解

  孙大圣(ThreadLocal )回了趟花果山,又一个筋斗云回到女儿国领地内。他想起还有事情没完成,喊一声“土地”(Thread t = Thread.currentThread()),当地土地公(Thread t)就出现了,抓过土地公的记事本(ThreadLocalMap map = getMap(t)),翻开记事本找到上次记下的内容的那一页(ThreadLocalMap.Entry e = map.getEntry(this)),内容不为空就读出来立刻去干活,找不到内容时重新再写入。

4.2 get()源码分析

public T get() {
    // 1、获取当前的线程
    Thread t = Thread.currentThread();
    // 2、以当前线程为参数,获取一个ThreadLocalMap对象map
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 3、map不为空,则以当前ThreadLocal对象实例作为key值,去map中取值,有找到直接返回
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    // 4、map为空或者在map中取不到值,返回设置的初始值
    return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
    // 根据key这个ThreadLocal的ID来获取索引,也即哈希值
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 对应的entry存在且未失效且弱引用指向的ThreadLocal就是key,则命中返回
    if (e != null && e.get() == key) {
        return e;
    } else {
        // 因为用的是线性探测,所以往后找还是有可能能够找到目标Entry的。
        return getEntryAfterMiss(key, i, e);
    }
}
/*
 * 调用getEntry未直接命中的时候调用此方法
 */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    // 基于线性探测法不断向后探测直到遇到空entry。
    while (e != null) {
        ThreadLocal<?> k = e.get();
        // 找到目标
        if (k == key) {
            return e;
        }
        if (k == null) {
            // 该entry对应的ThreadLocal已经被回收,调用expungeStaleEntry来清理无效的entry
            expungeStaleEntry(i);
        } else {
            // 环形意义下往后面走
            i = nextIndex(i, len);
        }
        e = tab[i];
    }
    return null;
}

4 remove()

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
/**
 * 从map中删除ThreadLocal
 */
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;
        }
    }
}

(1)取得当前线程的ThreadLocal.ThreadLocalMap,如果有ThreadLocal.ThreadLocalMap,直接在table中找key,如果找到了,把弱引用断了做一次段清理。

5 应用场景

  《开发艺术探索》中所说的:一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用 ThreadLocal。

5.1 场景1:Looper.myLooper()

  比如对应Handler 来说,它需要获取当前线程的Looper,很显然Looper 的作用域就是线程并且不同线程具有不同的Looper,这个时候通过ThreadLocal就可以轻松实现Looper在线程中的存取。如果不采用ThreadLocal,那么系统就必须提供一个全局的哈希表供Handler查找指定线程的Looper,这样一来就必须提供一个类似于 LooperManager的类了,但是系统并没有这么做而是选择了ThreadLocal,这就是ThreadLocal的好处。

  问题:在不同线程中调用 Looper.myLooper() 为什么可以返回各自线程的 Looper 对象呢?明明我们没有传入任何线程信息,内部是如何找到当前线程对应的 Looper 对象呢?
  (1)因为Looper.myLooper()内部其实是调用了ThreadLocal的get()方法,ThreadLocal内部会自己去获取当前线程的成员变量threadLocals,该变量作用是线程自己的数据存储容器,作用域自然也就仅限线程而已,以此来实现可以自动根据不同线程返回各自线程的Looper对象。
  (2)毕竟,数据本来就只是存在各自线程中,自然互不影响,ThreadLocal 只是内部自动先去获取当前线程对象,再去取对象的数据存储容器,最后取值返回而已。
  (3)但取值之前要先存值,而在Looper类中,对ThreadLocal的set()方法调用只有一个地方: prepare(),该方法只有主线程系统已经帮忙调用了。这其实也就是说,主线程的Looper消息循环机制是默认开启的,其他线程默认关闭,如果想要使用,则需要自己手动调用,不调用的话,线程的Looper对象一直为空。

5.2 场景2:复杂逻辑下的对象传递

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

6 ThreadLocal的原理简总结

  ThreadLocal不需要key,因为线程里面自己的ThreadLocal.ThreadLocalMap不是通过链表法实现的,而是通过开地址法实现的。
  每次set的时候往线程里面的ThreadLocal.ThreadLocalMap中的table数组某一个位置塞一个值,这个位置由ThreadLocal中的threadLocaltHashCode取模得到,如果位置上有数据了,就往后找一个没有数据的位置。
  每次get的时候也一样,根据ThreadLocal中的threadLocalHashCode取模,取得线程中的ThreadLocal.ThreadLocalMap中的table的一个位置,看一下有没有数据,没有就往下一个位置找。
  既然ThreadLocal没有key,那么一个ThreadLocal只能塞一种特定数据。如果想要往线程里面的ThreadLocal.ThreadLocalMap里的table不同位置塞数据 ,比方说想塞三种String、一个Integer、两个Double、一个Date,请定义多个ThreadLocal,ThreadLocal支持泛型”public class ThreadLocal”。

7 学习链接

Java多线程9:ThreadLocal源码剖析

ThreadLocal源码解读

带你了解源码中的 ThreadLocal

Android ThreadLocal就是孙大圣

从ThreadLocal的实现看散列算法

猜你喜欢

转载自blog.csdn.net/chenliguan/article/details/81266669