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:

 

[java] view plain copy
  1. public class ThreadLocalTest {  
  2.     public static void main(String[] args) {  
  3.         Test test = new Test(0);  
  4.         MyThread thread1 = new MyThread(test);  
  5.         MyThread thread2 = new MyThread(test);  
  6.         thread1.start();  
  7.         thread2.start();  
  8.     }  
  9. }  
  10. class MyThread extends Thread  
  11. {  
  12.     public Test test;  
  13.     public MyThread() {  
  14.     }  
  15.     public MyThread(Test test)  
  16.     {  
  17.         this.test = test;  
  18.     }  
  19.     @Override  
  20.     public void run() {  
  21.         for(int i = 0;i < 3;i++)  
  22.         {  
  23.             System.out.println(Thread.currentThread().getName()+"  value: "+test.getNum());  
  24.         }  
  25.     }  
  26. }  
  27. class Test   
  28. {  
  29.     public int count;  
  30.     public Test(int count)  
  31.     {  
  32.         this.count = count;  
  33.     }  
  34.     public int getNum()  
  35.     {  
  36.         return count++;  
  37.     }  
  38. }  

输出:

 

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后的情况)

 

[java] view plain copy
  1. public class ThreadLocalTest {  
  2.     public static void main(String[] args) {  
  3.         Test test = new Test();  
  4.         MyThread thread1 = new MyThread(test);  
  5.         MyThread thread2 = new MyThread(test);  
  6.         thread1.start();  
  7.         thread2.start();  
  8.     }  
  9. }  
  10. class MyThread extends Thread  
  11. {  
  12.     public Test test;  
  13.     public MyThread() {  
  14.     }  
  15.     public MyThread(Test test)  
  16.     {  
  17.         this.test = test;  
  18.     }  
  19.     @Override  
  20.     public void run() {  
  21.         for(int i = 0;i < 3;i++)  
  22.         {  
  23.             System.out.println(Thread.currentThread().getName()+"  value: "+test.getNum());  
  24.         }  
  25.     }  
  26. }  
  27. class Test   
  28. {  
  29.     public ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>()  
  30.             {  
  31.                  protected Integer initialValue()   
  32.                  {  
  33.                      return 0;  
  34.                  };  
  35.             };  
  36.     public int getNum()  
  37.     {  
  38.         threadLocal.set(threadLocal.get()+1);  
  39.         return threadLocal.get();  
  40.     }  
  41. }  

输出:

 

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:

 

[java] view plain copy
  1. public class ThreadLocalTest {  
  2.     public static void main(String[] args) {  
  3.         final Test test = new Test();  
  4.         test.init();//设置ThreadLocal里面的值  
  5.         System.out.println("ThreadID:  "+test.intThreadLocal.get());  
  6.         System.out.println("ThreadName:  "+test.stringThreadLocal.get());  
  7.         new Thread(){  
  8.             public void run()   
  9.             {  
  10.                 test.init();//设置ThreadLocal里面的值  
  11.                 System.out.println("ThreadID:  "+test.intThreadLocal.get());  
  12.                 System.out.println("ThreadName:  "+test.stringThreadLocal.get());  
  13.             };  
  14.         }.start();  
  15.         //子线程执行结束之后休眠5秒钟,为了查看子线程中的ThreadLocal值是否与主线程相关  
  16.         try {  
  17.             Thread.sleep(5000);  
  18.         } catch (InterruptedException e) {  
  19.             e.printStackTrace();  
  20.         }  
  21.         System.out.println("ThreadID:  "+test.intThreadLocal.get());  
  22.         System.out.println("ThreadName:  "+test.stringThreadLocal.get());  
  23.     }  
  24. }  
  25. class Test   
  26. {  
  27.     ThreadLocal<Integer> intThreadLocal = new ThreadLocal<Integer>();  
  28.     ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>();  
  29.     public void init()  
  30.     {  
  31.         intThreadLocal.set((int)Thread.currentThread().getId());  
  32.         stringThreadLocal.set(Thread.currentThread().getName());  
  33.     }  
  34. }  

输出:

 

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种主要方法:

 

[java] view plain copy
  1. public T get() {}  
  2. public void set(T value) {}  
  3. public void remove() {}  
  4. protected T initialValue() {}  

剩下的方法都是让这四种方法间接调用的,在JDK1.2之前,方法里面的T全部是Object

 

下面分别讲解每个方法的实现:

get( ):

 

[java] view plain copy
  1. public T get() {  
  2.        Thread t = Thread.currentThread();  
  3.        ThreadLocalMap map = getMap(t);  
  4.        if (map != null) {  
  5.            ThreadLocalMap.Entry e = map.getEntry(this);  
  6.            if (e != null)  
  7.                return (T)e.value;  
  8.        }  
  9.        return setInitialValue();  
  10.    }  

解释:

 

get方法首先会获取当前线程,然后通过getMap方法获取到当前线程的ThreadLocalMap属性值,来看看getMap(Thread thread)方法:

 

[java] view plain copy
  1. ThreadLocalMap getMap(Thread t) {  
  2.        return t.threadLocals;  
  3.    }  

解释:
getMap很简单,就是直接返回当前线程的属性值,也就是Thread里面肯定有threadLocals属性,果不其然

 

 

[java] view plain copy
  1. class Thread implements Runnable {  
  2.      ThreadLocal.ThreadLocalMap threadLocals = null;  
  3. }  

 

那么ThreadLocalMap又是什么东西呢?

他是ThreadLocal的静态内部类,源码如下:

 

[java] view plain copy
  1. static class ThreadLocalMap {  
  2.       
  3.     static class Entry extends WeakReference<ThreadLocal> {  
  4.         Object value;  
  5.         Entry(ThreadLocal k, Object v) {  
  6.             super(k);  
  7.             value = v;  
  8.         }  
  9.     }  
  10. }  

这个静态内部类里面又有一个静态内部类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的源码:

 

[java] view plain copy
  1. private T setInitialValue() {  
  2.         T value = initialValue();  
  3.         Thread t = Thread.currentThread();  
  4.         ThreadLocalMap map = getMap(t);  
  5.         if (map != null)  
  6.             map.set(this, value);  
  7.         else  
  8.             createMap(t, value);  
  9.         return value;  
  10.     }  

解释:

 

首先会调用initialValue方法来获得设置的初始值,这个方法是protected修饰的,因此实际中是由我们自己来重写的,方法原型是:

 

[java] view plain copy
  1. protected T initialValue() {  
  2.         return null;  
  3.     }  

随后获取当前线程的ThreadLocalMap属性值,如果这个值不为null的话,则将默认value值设置到当前的ThreadLocal中,如果这个值为空的话,则调用createMap创建一个ThreadLocalMap,他的初始里面仅仅包含一个键值对,键为当前的ThreadLocal,值为

initialValue设置的初始值,最后返回初始值即可;

好了,get方法分析完毕,其实他就是返回ThreadLocal在当前线程中保存的变量副本而已;

set( )方法:

 

[java] view plain copy
  1. public void set(T value) {  
  2.         Thread t = Thread.currentThread();  
  3.         ThreadLocalMap map = getMap(t);  
  4.         if (map != null)  
  5.             map.set(this, value);  
  6.         else  
  7.             createMap(t, value);  
  8.     }  

看到没有呢?其实set方法的实现和initialValue方法几乎是一样的,只不过一个是我们在使用的过程中设置的,一个是我们在初始化的时候设置的;

 

remove( )方法:

 

[java] view plain copy
  1. public void remove() {  
  2.       ThreadLocalMap m = getMap(Thread.currentThread());  
  3.       if (m != null)  
  4.           m.remove(this);  
  5.   }  

也很简单,调用ThreadLocalMap的remove方法,从当前线程的ThreadLocalMap中移除当前ThreadLocal;
总结一下:

 

(1)一个Thread中可以有多个ThreadLocal,他们是作为键存储在ThreadLocalMap中的,值为变量的值;

(2)我们在使用ThreadLocal的get方法之前要不先调用set方法,设置ThreadLocal的值,要不重写ThreadLocal的protected修饰的initialValue方法来初始化ThreadLocal值;否则会发生空指针异常;

实例4:

 

[java] view plain copy
  1. public class ThreadLocalTest {  
  2.     public static void main(String[] args) {  
  3.         final Test test = new Test();  
  4.         test.init();//设置ThreadLocal里面的值  
  5.         System.out.println("ThreadID:  "+test.intThreadLocal.get());  
  6.         System.out.println("ThreadName:  "+test.stringThreadLocal.get());  
  7.     }  
  8. }  
  9. class Test   
  10. {  
  11.     ThreadLocal<Integer> intThreadLocal = new ThreadLocal<Integer>();  
  12.     ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>();  
  13.     public int getInt()  
  14.     {  
  15.         return intThreadLocal.get();  
  16.     }  
  17.     public void init()  
  18.     {  
  19.         System.out.println(getInt());  
  20.         intThreadLocal.set((int)Thread.currentThread().getId());  
  21.         stringThreadLocal.set(Thread.currentThread().getName());  
  22.     }  
  23. }  

发现会在return intThreadLocal.get()这行报空指针异常;

 

解决:

 

[java] view plain copy
  1. public class ThreadLocalTest {  
  2.     public static void main(String[] args) {  
  3.         final Test test = new Test();  
  4.         test.init();//设置ThreadLocal里面的值  
  5.         System.out.println("ThreadID:  "+test.intThreadLocal.get());  
  6.         System.out.println("ThreadName:  "+test.stringThreadLocal.get());  
  7.     }  
  8. }  
  9. class Test   
  10. {  
  11.     ThreadLocal<Integer> intThreadLocal = new ThreadLocal<Integer>(){  
  12.         protected Integer initialValue() {  
  13.             return 0;  
  14.         };  
  15.     };  
  16.     ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>();  
  17.     public int getInt()  
  18.     {  
  19.         return intThreadLocal.get();  
  20.     }  
  21.     public void init()  
  22.     {  
  23.         System.out.println(getInt());  
  24.         intThreadLocal.set((int)Thread.currentThread().getId());  
  25.         stringThreadLocal.set(Thread.currentThread().getName());  
  26.     }  
  27. }  

这样子就不会报空指针异常啦,原因很简单,我们在在get方法的源码分子中已经看到,如果当前ThreadLocalMap为空的话,会调用setInitialValue方法进行初始化,进而会调用到initialValue方法,这也就是protected方法,只要我们重写就可以设置我们想要初始化的值啦;

以上是关于JAVA之ThreadLocal的主要内容,如果未能解决你的问题,请参考以下文章

java基础之java程序基础之字符和字符串

Java面向对象之异常详解

尚硅谷全套课件整理:Java前端大数据安卓面试题

java之spring之配置讲解

java的nio之:java的nio系列教程之DatagramChannel

java基础之java程序基础--之浮点运算