Android多线程之Java 8中ThreadLocal内部实现机制详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/chenbaige/article/details/79526390

前言ThreadLocal是线程内部的存储类,通过它可以实现在每个线程中存储自己的私有数据。即数据存储以后,只能在指定的线程中获取这个存储的对象,而其它线程则不能获取到当前线程存储的这个对象。ThreadLocal有一个典型的应用场景,即我们在前文中说到的Android线程间通信中的Looper。每一个线程都有一个私有的Looper对象去处理当前线程的消息队列,有不清楚的同学,可以去上篇文章查看。话不多说,今天我们主要探讨的是ThreadLocal实现线程存储私有数据的工作原理。

  • 上面我们提到,通过ThreadLocal能实现在线程中存储的私有数据,下面我们来看一个典型的应用案例,我们在UI线程中执行如下代码:
 private void testThreadLocal() {
        final ThreadLocal<String> nameLocal = new ThreadLocal<>();
        nameLocal.set("我是UI主线程存储的数据");
        new Thread(new Runnable() {
            @Override
            public void run() {
                nameLocal.set("我是子线程存储的数据");
                //打印出当前线程和其存储的数据
                System.out.println(Thread.currentThread() + ":" + nameLocal.get());
            }
        }).start();
        //打印出当前线程和其存储的数据
        System.out.println(Thread.currentThread() + ":" + nameLocal.get());
    }

上面代码中,我们在主线程中创建了一个nameLocal对象,并且向里面写入了一个字符串。接下来,我们又开启了一个新的子线程,又向同一个nameLocal再写入一个数据。按理说,既然nameLocal都是一个ThreadLocal对象,因此调用get方法去获取存入的字符串时,应该是一个相同的字符串。但实际结果是怎样?实际输出结果如下:

03-12 15:49:49.726 19346-19346/? I/System.out: Thread[main,5,main]:我是UI主线程存储的数据
03-12 15:49:49.726 19346-19358/? I/System.out: Thread[Thread-209,5,main]:我是子线程存储的数据

通过打印的结果可以看出:在不同线程中,即使操作的同一个ThreadLocal对象,也能够实现数据的私密存储。但是,我们调用ThreadLocal的set方法的时候,操作的是同一个ThreadLocal对象,而且也没有不同的Key去区分不同的value值,为什么不会覆盖上一次存储的value?调用get方法为什么能获取到当前线程存储的数据?带着上面的两个问题,我们一起走入ThreadLocal的源码世界,一探究竟。

ThreadLocal源码分析

  • ThreadLocal存储值set方法详解

从上面的示例可以看出,我们利用ThreadLocal保存数据的时候,只需要简单的在ThreadLocal对象上调用set(value)方法,即可以实现数据的存储,而且实现线程的区分,怎么实现的?接下来进入ThreadLocalset方法一探究竟:

 public void set(T var1) {
        //获取调用set方法的当前线程
        Thread var2 = Thread.currentThread();
        //从线程中获取当前线程中保存的ThreadLocal的存储对象ThreadLocalMap
        ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
        if(var3 != null) {
        //如果以前对ThreadLocalMap进行过初始化,直接保存
            var3.set(this, var1);
        } else {
        //未进行过初始化,调用createMap方法先创建,再保存数据
            this.createMap(var2, var1);
        }
    }

set方法内部实现逻辑非常简便清晰,先获取到当前线程的ThreadLocalMap对象,再通过这个对象去实现信息的存储。说起来简单,但我们仍然对getMap干了什么事情?set方法和createMap方法怎么进行信息存储?有很大的疑惑,接下来我们一一解析。先看getMap内部实现逻辑:

//ThreadLocal类中的getMap方法
    ThreadLocal.ThreadLocalMap getMap(Thread var1) {
        return var1.threadLocals;
    }

//Thread类中的threadLocals变量定义,每个线程都有这个变量
ThreadLocalMap threadLocals = null;

//ThreadLocalMap类的定义
static class ThreadLocalMap {
        //数组的初始容量
        private static final int INITIAL_CAPACITY = 16;
        //存储的ThreadLocal数组,我们实现线程间私有化数据,就存放在这个数组中
        private ThreadLocal.ThreadLocalMap.Entry[] table;
        //数组的大小,不包括空数据
        private int size;
        //用于存储数组的总容量,包括为空的数据
        private int threshold;
  }

上边罗列了getMap方法的实现和它用到的ThreadLocalMap 类的定义。getMap方法就是从当前线程中获取它的ThreadLocalMap成员变量而已。关于ThreadLocalMap,我们现在只要知道它是一个存储数据的对象即可,至于它内部实现机制,我们接下来会详细讲解。

接下来看一下ThreadLocalMap中关键的set方法是如何实现数据存储的,源码如下:

private void set(ThreadLocal<?> var1, Object var2) {
            //ThreadLocal.ThreadLocalMap.Entry的定义见该方法下,其实它也是ThreadLocal对象,只是是虚引用对象
            ThreadLocal.ThreadLocalMap.Entry[] var3 = this.table;
            //获取数组的大小
            int var4 = var3.length;
            //通过按位与运算,获取var1在数组中的存储位置。
            //补充(&类似于取模(%)运算,但是效率比%高很多,a%b可以用位运算计算:a&b-1)
            int var5 = var1.threadLocalHashCode & var4 - 1;
            //从数组中取出ThreadLocal对象,相当于遍历数组中存储的数据,只要取出来的数据不为空,就一直通过nextIndex方法获取下一个位置的对象。一开始的时候,数组为空,不会执行for循环中的代码。
            for(ThreadLocal.ThreadLocalMap.Entry var6 = var3[var5]; var6 != null; var6 = var3[var5 = nextIndex(var5, var4)]) {
            //当数组不为空的时候,进入循环遍历数组操作,nextIndex方法就是取下一个位置
            //获取到从数组中取出来的那个ThreadLocal对象
                ThreadLocal var7 = (ThreadLocal)var6.get();
                //如果以前通过var1对象存储过数据,只更新其值,结束方法(var即是调用set方法的那个ThreadLocal对象)
                if(var7 == var1) {
                    var6.value = var2;
                    return;
                }
                //遍历完数组,没有找到以前通过var1存储过数据的痕迹,就把数据存储到数组第一个为null的位置.结束方法
                if(var7 == null) {
                    this.replaceStaleEntry(var1, var2, var5);
                    return;
                }
            }
            //当数组为空的时候,直接创建一个节点,并且添加到数组中
            var3[var5] = new ThreadLocal.ThreadLocalMap.Entry(var1, var2);
            //下面的操作是对数组进行重新计算操作
            int var8 = ++this.size;
            if(!this.cleanSomeSlots(var5, var8) && var8 >= this.threshold) {
                this.rehash();
            }
        }

//Entry 对象的定义,比ThreadLocal多了一个value对象
static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> var1, Object var2) {
                super(var1);
                this.value = var2;
            }
        }

最后,我们看一下通过ThreadLocal保存数据时,整个调用链:

//创建ThreadLocal对象
ThreadLocal<String> nameLocal = new ThreadLocal<>();
//调用set方法保存数据
nameLocal.set("value");
//获取当前线程保存的ThreadLocalMap 对象
Thread var2 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
//调用set方法或createMap保存数据
var3.set(this, var1);//存在同一个ThreadLocal对象this.createMap(var2, var1);//不存在同一个ThreadLocal对象
//最后就是向table数组中添加新的节点或更新旧结点
调用我们上面分析的set方法:
//当数组为空时,直接调用如下代码添加节点
var3[var5] = new ThreadLocal.ThreadLocalMap.Entry(var1, var2);
//当数组不为空时,循环更新或添加。更新为var6.value = var2覆盖其值,添加是调用如下方法
this.replaceStaleEntry(var1, var2, var5);
  • ThreadLocal存储值get方法详解

    相比通过set方法保存数据来说,获取数据的get方法就要简便得多。我们一般是通过调用nameLocal.get()来获取数据,我们就先看一下ThreadLocal类的get方法是怎么获取数据的。方法实现如下:

 public T get() {
        //首先还是拿到当前线程关联的ThreadLocalMap对象
        Thread var1 = Thread.currentThread();
        ThreadLocal.ThreadLocalMap var2 = this.getMap(var1);
        if(var2 != null) {
        //从数组中取到指定位置(即通过hashcode和数组大小计算出的,和上面存储时获取var5类似)的那个Entry对象,方法详情见下
            ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
            if(var3 != null) {
            //有数据就直接返回存储的数据
                Object var4 = var3.value;
                return var4;
            }
        }
        //当没有存储的有数据,就是设置数据为null并且返回。方法实现逻辑会在后面说到。
        return this.setInitialValue();
    }

ThreadLocalMapgetEntry方法是怎么获取到指定ThreadLocal.ThreadLocalMap.Entry对象的?看一下内部实现:

private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> var1) {
            //获取在数组中的位置
            int var2 = var1.threadLocalHashCode & this.table.length - 1;
            //取当前位置的数据
            ThreadLocal.ThreadLocalMap.Entry var3 = this.table[var2];
            return var3 != null && var3.get() == var1?var3:this.getEntryAfterMiss(var1, var2, var3);
        }

ThreadLocalMapsetInitialValue方法又是怎么去设置默认值并返回数据的?看一下它的源码:

 private T setInitialValue() {
         //this.initialValue()方法就是返回了一个null,因此var赋值为null
        Object var1 = this.initialValue();
        //获取与当前线程相关联的ThreadLocalMap对象
        Thread var2 = Thread.currentThread();
        ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
        if(var3 != null) {
           //上面讲过方法的含义,这里是重写为null值
            var3.set(this, var1);
        } else {
            //上面讲过方法的含义,这里是添加一个为null的新节点
            this.createMap(var2, var1);
        }
        //返回null值
        return var1;
    }

//上面提到的initialValue()方法源码
   protected T initialValue() {
        return null;
    }

最后,还是看一下完整的get方法调用链:

//调用set方法从nameLocal中获取数据
nameLocal.get();
//获取当前线程保存的ThreadLocalMap 对象
Thread var2 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
//如果var3为空,就创建一个ThreadLocalMap,并且给数据赋值为null,并返回。调用如下代码
return this.setInitialValue();、
//如果var3不为空,就获取指定位置(通过hashcode计算而来,原理上面讲过)的Entry对象
ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
//返回entry中的数据
Object var4 = var3.value;
return var4;

总结:通过ThreadLocal实现数据的保存和获取原理到现在已经告一段落了。它是怎么实现数据的线程私有化?其实很简单,主要是通过线程的私有成员变量ThreadLocalMap实现的,而ThreadLocalMap中又有一个ThreadLocal.ThreadLocalMap.Entry[]实现数据的存储。每次我们保存或获取数据都是对这个数组进行操作而已。关于Android多线程的详细讲解,大家可以去 Android多线程相关知识总结 查看。希望能帮助到你额。。。

猜你喜欢

转载自blog.csdn.net/chenbaige/article/details/79526390