您的位置:首页 > 移动开发 > Android开发

ThreadLocal应用场景以及源码分析

2017-11-02 10:53 501 查看


一、应用篇


ThreadLocal介绍

   ThreadLocal如果单纯从字面上理解的话好像是“本地线程”的意思,其实并不是这个意思,只是这个名字起的太容易让人误解了,它的真正的意思是线程本地变量。

   实现一个线程本地的存储,也就是说,每个线程都有自己的局部变量。所有线程都共享一个ThreadLocal对象,但是每个线程在访问这些变量的时候能得到不同的值,每个线程可以更改这些变量并且不会影响其他的线程,并且支持null值。


ThreadLocal理解

我们先看下属性动画为每个线程设置AnimationHeadler的

private static AnimationHandler getOrCreateAnimationHandler() {
AnimationHandler handler = sAnimationHandler.get();
if (handler == null) {
handler = new AnimationHandler();
sAnimationHandler.set(handler);
}
return handler;
}


因为protected static ThreadLocal<AnimationHandler> sAnimationHandler =new ThreadLocal<AnimationHandler>();这里没有采用初始化值,这里不是通过一个变量的拷贝而是每个线程通过new创建一个对象出来然后保存。很多人认为ThreadLocal是为了解决共享对象的多线程访问问题的,这是错误的说法,因为无论是通过初始化变量的拷贝还是直接通过new创建自己局部变量,ThreadLocal.set()
到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象,改变的也是自己独立的对象,本身就不属于同一个对象,没有共享的概念,更加不可能是解决共享对象的多线程访问的。

通过上面的理解总结以下几点

 1.每个线程读拥有自己的局部变量

     每个线程都有一个独立于其他线程的上下文来保存这个变量,一个线程的本地变量对其他线程是不可见的

 2.独立于变量的初始化副本,或者初始化一个属于自己的变量

     ThreadLocal可以给一个初始值,而每个线程都会获得这个初始化值的一个副本,这样才能保证不同的线程都有一份拷贝,同样也可以new的方式为线程创建一个变量

 3.变量改变只与当前线程关联,线程之间互不干扰

    ThreadLocal 不是用于解决共享变量的问题的,不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制。

所以ThreadLocal既不是为了解决共享多线程的访问问题,更不是为了解决线程同步问题,ThreadLocal的设计初衷就是为了提供线程内部的局部变量,方便在本线程内随时随地的读取,并且与其他线程隔离。


ThreadLocal使用场景

    说了那么多的概念,归根到底我们在什么时候才使用ThreadLocal呢?很多时候我们会创建一些静态域来保存全局对象,那么这个对象就可能被任意线程访问,如果能保证是线程安全的,那倒是没啥问题,但是有时候很难保证线程安全,这时候我们就需要为每个线程都创建一个对象的副本,我们也可以用ConcurrentMap<Thread, Object>来保存这些对象,这样会比较麻烦,比如当一个线程结束的时候我们如何删除这个线程的对象副本呢?如果使用ThreadLocal就不用有这个担心了,ThreadLocal保证每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且
ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。经查阅资料大致得到以下两种场景:


1.)当某些数据以线程为作用域,并且不同线程拥有不同数据副本的时候。

   ThreadLocal使用场合主要解决多线程中数据因并发产生不一致的问题。ThreadLocal以空间换时间,为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,但大大减少了线程同步所带来的线程消耗,也减少了线程并发控制的复杂度。

   例如Android的Handler消息机制,对于Handler来说,它需要获取当前线程的looper很显然Looper的作用域就是线程并且不同线程具有不同的Looper,这个时候通过ThreadLocal就可以轻松实现Looper在线程中的存取。再例如开源框架EventBus,EventBus需要获取当前线程的PostingThreadState对象,不同的PostingThreadState同样作用于不同的线程,EventBus可以很轻松的获取当前线程下的PostingThreadState对象,然后进行相关操作。


2.)复杂逻辑下对象传递,比如监听器的传递

   使用参数传递的话:当函数调用栈更深时,设计会很糟糕,为每一个线程定义一个静态变量监听器,如果是多线程的话,一个线程就需要定义一个静态变量,无法扩展,这时候使用ThreadLocal就可以解决问题。


 ThreadLocal使用举例

  举一个简单的例子,让每个线程拥有自己唯一的一个任务队列,类似EventBus的实现。

private static final ThreadLocal<PriorityQueue<TaskItem>> queueThreadLocal = new ThreadLocal<PriorityQueue<TaskItem>>() {
@Override
protected PriorityQueue<TaskItem> initialValue() {
return new PriorityQueue<>(5);
}
};

public PriorityQueue<TaskItem> getTaskQueue() {
PriorityQueue<TaskItem> taskItems = queueThreadLocal.get();
return taskItems;
}

public void addTask(TaskItem taskItem) {
PriorityQueue<TaskItem> taskItems = queueThreadLocal.get();
taskItems.add(taskItem);
}

public void removeTask(TaskItem taskItem) {
PriorityQueue<TaskItem> taskItems = queueThreadLocal.get();
if (taskItems.contains(taskItem)) {
taskItems.remove(taskItem);
}
}

private void exceTask() {
PriorityQueue<TaskItem> taskItems = queueThreadLocal.get();
if (!taskItems.isEmpty()) {
TaskItem taskItem = taskItems.poll();
taskItem.exceTask();
}
}


 附上TaskItme代码:

public class TaskItem implements Comparable {
private long Id;
private String name;
private int priority;

public long getId() {
return Id;
}

public void setId(long id) {
Id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getPriority() {
return priority;
}

public void setPriority(int priority) {
this.priority = priority;
}

@Override
public int compareTo(Object arg0) {
if (TaskItem.class.isInstance(arg0)) {
TaskItem tm = (TaskItem) arg0;
if (tm.priority > priority) {
return -1;
} else if (tm.priority < priority) {
return 1;
}
}
return 0;
}

public void exceTask() {
Log.e("exceTask", "exceTask---id:" + Id + " name:" + name);
}

}


经过上面代码可以看到,你是在哪个线程提交的任务自然而然的就添加到线程所属的任务队列里面,这里其实通过ConcurrentMap<Thread, Object>保存也是可以的,上面也说了相对比较麻烦。

 


二、源码篇

 

       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的这种问题。

       介绍了那么多ThreadLocal的知识,可能还是有点抽象,下面通过实际的例子为大家演示ThreadLocal的真正含义。首先定义一个ThreadLocal对象,这里选择Boolean类型的,如下所示:

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

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

[java] view
plain copy

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的值,根据前面对ThreadLocal的描述,这个时候,主线程中应该是true,子线程1中应该是false,而子线程2中由于没有设置值,所以应该是null,安装并运行程序,日志如下所示:

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的两个使用场景的理论分析,大家应该就能比较好地理解ThreadLocal的使用方法了。ThreadLocal之所以有这么奇妙的效果,是因为不同线程访问同一个ThreadLocal的get方法,ThreadLocal内部会从各自的线程中取出一个数组,然后再从数组中根据当前ThreadLocal的索引去查找出对应的value值,很显然,不同线程中的数组是不同的,这就是为什么通过ThreadLocal可以在不同的线程中维护一套数据的副本并且彼此互不干扰。

       对ThreadLocal的使用方法和工作过程做了一个介绍后,下面分析下ThreadLocal的内部实现, ThreadLocal是一个泛型类,它的定义为public class ThreadLocal<T>,只要弄清楚ThreadLocal的get和set方法就可以明白它的工作原理。

  2.1  set方法

       首先看ThreadLocal的set方法,如下所示:

public void set(T var1) {
Thread var2 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
if(var3 != null)
{
var3.set(this, var1);
else {
this.createMap(var2, var1);
}

}
首先会获取当前线程,然后通过getMap创建map集合。如下所示:ThreadLocal.ThreadLocalMap getMap(Thread var1) {
return var1.threadLocals;
}

void createMap(Thread var1, T var2) {
var1.threadLocals = new ThreadLocal.ThreadLocalMap(this, var2);
}getmap返回的是ThreadLocal.ThreadLocalMap对象,我们接着往下看ThreadLocalMap(ThreadLocal<?> var1, Object var2) {
this.size = 0;
this.table = new ThreadLocal.ThreadLocalMap.Entry[16];
int var3 = var1.threadLocalHashCode & 15;
this.table[var3] = new ThreadLocal.ThreadLocalMap.Entry(var1, var2);
this.size = 1;
this.setThreshold(16);
}我们在来看看Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;

Entry(ThreadLocal<?> var1, Object var2) {
super(var1);
this.value = var2;
}
}
通过弱引用Entry集合来存储数据,这样的好处待到下次内存回收时垃圾肯定被回收了。这时将var2的值已经存到了entry的value中。
我们具体来看看var3.set方法里面的代码
private void set(ThreadLocal<?> var1, Object var2) {
ThreadLocal.ThreadLocalMap.Entry[] var3 = this.table;
int var4 = var3.length;
int var5 = var1.threadLocalHashCode & var4 - 1;

for(ThreadLocal.ThreadLocalMap.Entry var6 = var3[var5]; var6 != null; var6 = var3[var5 = nextIndex(var5, var4)]) {
ThreadLocal var7 = (ThreadLocal)var6.get();
if(var7 == var1) {
var6.value = var2;
return;
}

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();
}

}
通过创建table数组来存储弱引用entry集合。
忽略其中算法,最终会把var2值放入到Entry集合中。上面的是在Entry集合中存储数据,下面是从集合中取数据
2.2 get方法
public T get() {
Thread var1 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var2 = this.getMap(var1);
if(var2 != null) {
ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
if(var3 != null) {
Object var4 = var3.value;
return var4;
}
}

return this.setInitialValue();
}
首先是getMap方法,还是返回ThreadLocal.ThreadLocalMap对象,重点我们来看
getEntry方法
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);
}
通过索引varl查找出变量var3(通过table数组),接着返回我们想要索引的Entry对象。


       从ThreadLocal的set和get方法可以看出,它们所操作的对象都是当前线程的localValues对象的table数组,因此在不同线程中访问同一个ThreadLocal的set和get方法,它们对ThreadLocal所做的读写操作仅限于各自线程的内部,这就是为什么ThreadLocal可以在多个线程中互不干扰地存储和修改数据,理解ThreadLocal的实现方式有助于理解Looper的工作原理
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android开发