JAVA之ThreadLocal
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA之ThreadLocal相关的知识,希望对你有一定的参考价值。
前面Handler消息处理机制中提到了线程会将自己的Looper对象放到ThreadLocal中,因而我们有必要看看ThreadLocal是什么?
ThreadLocal是什么?
ThreadLocal也是用来进行多线程并发的,可以理解为是线程的局部变量,作用就是为每个线程提供一个变量值的副本,每个线程可以独立的改变自己的副本而不影响其他线程。
ThreadLocal与synchronized的区别?
两者都可以实现多线程的并发
ThreadLocal:采用空间换时间的方式
synchronized:采用时间换空间的方式
ThreadLocal是怎么做到为每个线程维护变量副本的呢?
每个线程内部都有一个ThreadLocalMap的变量,用来存储每个变量的副本,而ThreadLocalMap采用Entry数组来存储ThreadLocal---Object键值对;
在分析源码前,先来看看我们平常使用多线程的例子
实例1:
- public class ThreadLocalTest {
- public static void main(String[] args) {
- Test test = new Test(0);
- MyThread thread1 = new MyThread(test);
- MyThread thread2 = new MyThread(test);
- thread1.start();
- thread2.start();
- }
- }
- class MyThread extends Thread
- {
- public Test test;
- public MyThread() {
- }
- public MyThread(Test test)
- {
- this.test = test;
- }
- @Override
- public void run() {
- for(int i = 0;i < 3;i++)
- {
- System.out.println(Thread.currentThread().getName()+" value: "+test.getNum());
- }
- }
- }
- class Test
- {
- public int count;
- public Test(int count)
- {
- this.count = count;
- }
- public int getNum()
- {
- return count++;
- }
- }
输出:
Thread-0 value: 0
Thread-0 value: 2
Thread-0 value: 3
Thread-1 value: 1
Thread-1 value: 4
Thread-1 value: 5
可以发现线程0和线程1是交错的在改变count值的,因为两个线程共用Test里面的count变量,而且两者改变的顺序是不固定的;
实例2:(使用ThreadLocal后的情况)
- public class ThreadLocalTest {
- public static void main(String[] args) {
- Test test = new Test();
- MyThread thread1 = new MyThread(test);
- MyThread thread2 = new MyThread(test);
- thread1.start();
- thread2.start();
- }
- }
- class MyThread extends Thread
- {
- public Test test;
- public MyThread() {
- }
- public MyThread(Test test)
- {
- this.test = test;
- }
- @Override
- public void run() {
- for(int i = 0;i < 3;i++)
- {
- System.out.println(Thread.currentThread().getName()+" value: "+test.getNum());
- }
- }
- }
- class Test
- {
- public ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>()
- {
- protected Integer initialValue()
- {
- return 0;
- };
- };
- public int getNum()
- {
- threadLocal.set(threadLocal.get()+1);
- return threadLocal.get();
- }
- }
输出:
Thread-1 value: 1
Thread-0 value: 1
Thread-1 value: 2
Thread-0 value: 2
Thread-1 value: 3
Thread-0 value: 3
发现线程0和1的输出并不是互相影响的,虽然他们共用test,但是对于count变量,他们各自都有自己的备份,修改的时候并不会影响另一个线程;
如果实例2还不明显的话,我们再来一个实例看看:
实例3:
- public class ThreadLocalTest {
- public static void main(String[] args) {
- final Test test = new Test();
- test.init();//设置ThreadLocal里面的值
- System.out.println("ThreadID: "+test.intThreadLocal.get());
- System.out.println("ThreadName: "+test.stringThreadLocal.get());
- new Thread(){
- public void run()
- {
- test.init();//设置ThreadLocal里面的值
- System.out.println("ThreadID: "+test.intThreadLocal.get());
- System.out.println("ThreadName: "+test.stringThreadLocal.get());
- };
- }.start();
- //子线程执行结束之后休眠5秒钟,为了查看子线程中的ThreadLocal值是否与主线程相关
- try {
- Thread.sleep(5000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("ThreadID: "+test.intThreadLocal.get());
- System.out.println("ThreadName: "+test.stringThreadLocal.get());
- }
- }
- class Test
- {
- ThreadLocal<Integer> intThreadLocal = new ThreadLocal<Integer>();
- ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>();
- public void init()
- {
- intThreadLocal.set((int)Thread.currentThread().getId());
- stringThreadLocal.set(Thread.currentThread().getName());
- }
- }
输出:
ThreadID: 1
ThreadName: main
ThreadID: 9
ThreadName: Thread-0
ThreadID: 1
ThreadName: main
可以发现main中我们首先输出了主线程的ThreadID以及主线程的名字,随后开启子线程输出子线程的ThreadID和子线程的名字,随后休眠程序5秒钟直接输出主线程的ThreadID以及主线程的名字,在这次输出时我们并没有通过test.init( )初始化ThreadLocal的值,但是他仍然会输出主线程的信息,这说明一点,只要设置了线程的ThreadLocal值之后,及时有别的线程开启也不会影响原来现成的ThreadLocal,也就是说ThreadLocal是线程独享的;
下面从源码角度进行分析:
ThreadLocal类提供了4种主要方法:
- public T get() {}
- public void set(T value) {}
- public void remove() {}
- protected T initialValue() {}
剩下的方法都是让这四种方法间接调用的,在JDK1.2之前,方法里面的T全部是Object
下面分别讲解每个方法的实现:
get( ):
- public T get() {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null) {
- ThreadLocalMap.Entry e = map.getEntry(this);
- if (e != null)
- return (T)e.value;
- }
- return setInitialValue();
- }
解释:
get方法首先会获取当前线程,然后通过getMap方法获取到当前线程的ThreadLocalMap属性值,来看看getMap(Thread thread)方法:
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
解释:
getMap很简单,就是直接返回当前线程的属性值,也就是Thread里面肯定有threadLocals属性,果不其然
- class Thread implements Runnable {
- ThreadLocal.ThreadLocalMap threadLocals = null;
- }
那么ThreadLocalMap又是什么东西呢?
他是ThreadLocal的静态内部类,源码如下:
- static class ThreadLocalMap {
- static class Entry extends WeakReference<ThreadLocal> {
- Object value;
- Entry(ThreadLocal k, Object v) {
- super(k);
- value = v;
- }
- }
- }
这个静态内部类里面又有一个静态内部类Entry,实现了WeakReference类,并且在类中将ThreadLocal通过super(k)调用WeakReference的构造函数将当前ThreadLocal设置成软引用,便于GC及时回收ThreadLocal占用的内存,从这里也就看出来ThreadLocalMap其实就是一个以ThreadLocal---Object为键值的Map;
回到get方法,在获取到ThreadLocalMap之后,接下来分两种情况讨论,(1)如果map不为空的话,通过ThreadLocalMap的getEntry方法获取到当前ThreadLocal的键值对,这里getEntry传入的参数是ThreadLocal变量而不是当前线程,接着如果Entry值不为空的话,返回Entry的value值即可;(2)如果map为空的话,调用setInitialValue来生成一个具有默认值的ThreadLocal,并且返回这个值;
看下setInitialValue的源码:
- private T setInitialValue() {
- T value = initialValue();
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- return value;
- }
解释:
首先会调用initialValue方法来获得设置的初始值,这个方法是protected修饰的,因此实际中是由我们自己来重写的,方法原型是:
- protected T initialValue() {
- return null;
- }
随后获取当前线程的ThreadLocalMap属性值,如果这个值不为null的话,则将默认value值设置到当前的ThreadLocal中,如果这个值为空的话,则调用createMap创建一个ThreadLocalMap,他的初始里面仅仅包含一个键值对,键为当前的ThreadLocal,值为
initialValue设置的初始值,最后返回初始值即可;
好了,get方法分析完毕,其实他就是返回ThreadLocal在当前线程中保存的变量副本而已;
set( )方法:
- public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- }
看到没有呢?其实set方法的实现和initialValue方法几乎是一样的,只不过一个是我们在使用的过程中设置的,一个是我们在初始化的时候设置的;
remove( )方法:
- public void remove() {
- ThreadLocalMap m = getMap(Thread.currentThread());
- if (m != null)
- m.remove(this);
- }
也很简单,调用ThreadLocalMap的remove方法,从当前线程的ThreadLocalMap中移除当前ThreadLocal;
总结一下:
(1)一个Thread中可以有多个ThreadLocal,他们是作为键存储在ThreadLocalMap中的,值为变量的值;
(2)我们在使用ThreadLocal的get方法之前要不先调用set方法,设置ThreadLocal的值,要不重写ThreadLocal的protected修饰的initialValue方法来初始化ThreadLocal值;否则会发生空指针异常;
实例4:
- public class ThreadLocalTest {
- public static void main(String[] args) {
- final Test test = new Test();
- test.init();//设置ThreadLocal里面的值
- System.out.println("ThreadID: "+test.intThreadLocal.get());
- System.out.println("ThreadName: "+test.stringThreadLocal.get());
- }
- }
- class Test
- {
- ThreadLocal<Integer> intThreadLocal = new ThreadLocal<Integer>();
- ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>();
- public int getInt()
- {
- return intThreadLocal.get();
- }
- public void init()
- {
- System.out.println(getInt());
- intThreadLocal.set((int)Thread.currentThread().getId());
- stringThreadLocal.set(Thread.currentThread().getName());
- }
- }
发现会在return intThreadLocal.get()这行报空指针异常;
解决:
- public class ThreadLocalTest {
- public static void main(String[] args) {
- final Test test = new Test();
- test.init();//设置ThreadLocal里面的值
- System.out.println("ThreadID: "+test.intThreadLocal.get());
- System.out.println("ThreadName: "+test.stringThreadLocal.get());
- }
- }
- class Test
- {
- ThreadLocal<Integer> intThreadLocal = new ThreadLocal<Integer>(){
- protected Integer initialValue() {
- return 0;
- };
- };
- ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>();
- public int getInt()
- {
- return intThreadLocal.get();
- }
- public void init()
- {
- System.out.println(getInt());
- intThreadLocal.set((int)Thread.currentThread().getId());
- stringThreadLocal.set(Thread.currentThread().getName());
- }
- }
这样子就不会报空指针异常啦,原因很简单,我们在在get方法的源码分子中已经看到,如果当前ThreadLocalMap为空的话,会调用setInitialValue方法进行初始化,进而会调用到initialValue方法,这也就是protected方法,只要我们重写就可以设置我们想要初始化的值啦;
以上是关于JAVA之ThreadLocal的主要内容,如果未能解决你的问题,请参考以下文章