在前面讲关于Handler的Looper的时候,我们讲到过Looper的获取和设置,是通过ThreadLocal来操作的,如下:
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
在准备Looper的时候,
public static void prepare() { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper()); }
在获取Looper的时候,
/** * Return the Looper object associated with the current thread. Returns * null if the calling thread is not associated with a Looper. */ public static Looper myLooper() { return sThreadLocal.get(); }
那么ThreadLocal的作用到底什么呢?为什么会有ThreadLocal的出现呢?
首先,我们先来看下面一个例子吧,如下:
public class HelperTesting { private String text = "empty"; private static HelperTesting helperTesting = new HelperTesting(); private void printText() { System.out.println(text); } private void changeText(String str) { text = str; } private void printWithLine(String str){ System.out.println("##################### " + str +" #############################"); } public static void main(String[] args) { helperTesting.printWithLine(Thread.currentThread().getName() + " start"); helperTesting.printText(); Thread thread = new Thread(new Runnable() { @Override public void run() { helperTesting.printWithLine(Thread.currentThread().getName() + " start"); helperTesting.printText(); helperTesting.changeText("thread1"); helperTesting.printText(); helperTesting.printWithLine(Thread.currentThread().getName() + " end"); } }); thread.start(); try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } helperTesting.printText(); helperTesting.printWithLine(Thread.currentThread().getName() + " end"); } }
在这个小Demo里面,我们做了几件事情:
1)创建了一个HelperString对象,其有一个 text 字段,初始化其值为 “empty”。
2)创建了一个新的线程,在线程中改变helperString对象的 text 的值为"thread1",运行线程,并打印其值。
3)最后我们在原来的线程中打印出text的值。
运行结果如下:
##################### main start ############################# empty ##################### Thread-0 start ############################# empty thread1 ##################### Thread-0 end ############################# thread1 ##################### main end #############################
我们可以看到,在主线程中的 text 值被改变了。
在多线程的编程过程当中,因为thread共享同一段进程空间,所以A线程的运行是完全有可能在不经意间就改变了某个值,而当B线程也去拿这个值的时候,可能就已经不是其想要获取的数值了。
于是有人就想,能不能让线程也拥有其私有的一些空间呢,让存放在这段空间里面的值并不会被其他线程改变,而这就是ThreadLocal的出现。
注意,其出现只是为了保证线程数据的私有性,并不是为了解决线程间的同步问题,我估计这也是其为什么叫Thread Local 的原因。
接下来,我们再通过一个Demo来看看,怎么使用ThreadLocal,如下:
public class HelperTestingSec { private ThreadLocal<String> sThreadLocal = new ThreadLocal<String>(); private static HelperTestingSec helperTesting = new HelperTestingSec(); private HelperTestingSec(){ sThreadLocal.set("empty"); } private void printText() { System.out.println(sThreadLocal.get()); } private void changeText(String str) { sThreadLocal.set(str); } private void printWithLine(String str){ System.out.println("##################### " + str +" #############################"); } public static void main(String[] args) { helperTesting.printWithLine(Thread.currentThread().getName() + " start"); helperTesting.printText(); Thread thread = new Thread(new Runnable() { @Override public void run() { helperTesting.printWithLine(Thread.currentThread().getName() + " start"); helperTesting.printText(); helperTesting.changeText("thread1"); helperTesting.printText(); helperTesting.printWithLine(Thread.currentThread().getName() + " end"); } }); thread.start(); try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } helperTesting.printText(); helperTesting.printWithLine(Thread.currentThread().getName() + " end"); } }
这个Demo中做的事情,其实跟上一个Demo类似,唯一不同的是,在这个Demo中,我们利用一个ThreadLocal对象来存放这个String的值,运行结果如下:
##################### main start ############################# empty ##################### Thread-0 start ############################# null thread1 ##################### Thread-0 end ############################# empty ##################### main end #############################
从上面的结果,我们可以看到,同一个helperTestingSec对象,在 main线程中,其 sThreadLocal中存放的对象一直是empty。
而在新建的线程当中呢,当在调用changeText进行设值之前,其value是null的,也就是说在新线程中,虽然是同一个对象,但是其值是不一样的,即使我们在新线程中改变了其值,也不会影响其在Main线程中的值。
可见,通过 ThreadLocal,我们就做到了线程间的数据独立了。
那么,ThreadLocal的实现原理是什么,为什么其能做到这个事情呢,我们还是想了解一下其内部的构造的,看一下能不能学到点什么东西,对吧。
按照顺序,一般情况下,我们总是先放东西进去,再去拿东西出来的,所以我们先看一下ThreadLocal 的 set 方法。
有一点要注意的是,这里讲解的ThreadLocal的实现是Android里面的实现,JDK中的实现方式有点不一样,但是原理是一样的。
public void set(T value) { Thread currentThread = Thread.currentThread(); Values values = values(currentThread); if (values == null) { values = initializeValues(currentThread); } values.put(this, value); }
在Demo2中,我们调用sThreadLocal的 set方法的时候,我们就进入了上面的逻辑。
可以看到,第一步,就是先利用Thread.currentThread拿到当前的线程,再根据这个线程去获取一个叫做Values的对象。再看一下values函数,我们可看到其是从哪里去获取这个Values对象的,如下:
/** * Creates Values instance for this thread and variable type. */ Values initializeValues(Thread current) { return current.localValues = new Values(); } /** * Gets Values instance for this thread and variable type. */ Values values(Thread current) { return current.localValues; }
可以看到,其是从current这个线程里去获取Values对象的,而如果不存在这个对象,其就会调用initializeValues方法,为线程的localValues创建一个新的Values对象。
最近再调用values的put方法,以当前ThreadLocal对象为key值,将我们的valule给存放到这个Values对象中,我们会马上意识到,Values实现的应该是一个类似Map的键值对的数据结构。
从这里,我们就可以意识到:
1)在每个线程中都存在一个Values对象。
2)无论我们在哪个线程中调用ThreadLocal对象,都会先去当前线程中取得一个Values对象(如果不存在,就创建一个)再对这个Values对象进行键值操作,比如ThreadLocal的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); }
3)一个ThreadLocal对象其实是可以给所有线程共享的,因为其只是作为一个入口和一个WeakRefreence的Key值,真正的数据都是存在Values中,而这个Values对象是线程独有,且在当前线程中是唯一的。
所以这也是为什么在Looper类中,sThreadLocal对象是一个静态的对象就行了。
而Values的内部实现,其实是用一个Object数组来做为数据结构的,偶数位(0,2,4....)等作为Key值,其存放是指向ThreadLocal对象本身的一个WeakReference, 奇数位(1,3,5....)等作为的Value值,存放的是我们设置的值,从上面 get方法中, 我们也可以略窥一二。
在这里,推荐两边文章,相信对大家了解ThreadLocal有一定的帮助。
When and how to use a ThreadLocalHow is ThreadLocal implemented?