ThreadLocal及InheritableThreadLocal的原理剖析

咱們知道,線程的不安全問題,主要是因爲多線程併發讀取一個變量而引發的,那麼有沒有一種辦法可讓一個變量是線程獨有的呢,這樣不就能夠解決線程安全問題了麼。其實JDK已經爲咱們提供了ThreadLocal這個東西。java


ThreadLocal基本使用
git

當使用ThreadLocal維護變量時,ThreadLocal爲每一個使用該變量的線程提供獨立的變量副本,因此每個線程均可以獨立地改變本身的副本,而不會影響其它線程所對應的副本。
ThreadLocal 的主要方法有這麼幾個:github


initialValue 初始化
set 賦值
get 取值
remove 清空複製代碼

下面來看一個簡單的使用代碼示例:安全

複製代碼
public class ThreadLocalDemo {
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() { 
       @Override       
 public Integer initialValue() { 
           return 0;  
      } 
   };    
static class ThreadDemo implements Runnable {   
     @Override   
     public void run() {   
         for (int i = 0; i < 1000; i++) {     
           threadLocal.set(threadLocal.get() + 1);   
         }          
  System.out.println("thread :" + Thread.currentThread().getId() + " is" + threadLocal.get());      
  } 
   } 
   public static void main(String[] args) {  
      for (int i = 0; i < 10; i++) {   
         new Thread(new ThreadDemo()).start();  
      } 
   }
}複製代碼

上方代碼使用了10個線程循環對一個threadLocal的值進行一千次的加法,若是咱們不知道ThreadLocal的原理的話咱們可能會以爲最後打印的值必定是1000、2000、3000。。10000或者是線程不安全的值。
可是若是你執行這段代碼你會發現最後打印的都是1000。bash


ThreadLocal原理剖析
多線程

如今咱們來看一下ThreadLocal是如何實現爲每一個線程單獨維護一個變量的呢。
先來看一下初始化方法。併發

複製代碼
protected T initialValue() {      
  return null;
    }複製代碼

initialValue 默認是返回空的,因此爲了不空指針問題重寫了這個方法設置了默認返回值爲0,可是呢,雖然這個方法好像是設置默認值的,可是尚未生效,具體請接着往下看。ide

複製代碼
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方法首先會獲取當前線程,而後經過一個getMap方法獲取了ThreadLocalMap,接着來看一下這個map是怎麼來的呢。ui

123複製代碼
ThreadLocalMap getMap(Thread t) {  
     return t.threadLocals;  
 }複製代碼

這個map是在Thread類維護的一個map,下方是Thread類維護的此變量。默認這個map是空的。this

1複製代碼
ThreadLocal.ThreadLocalMap threadLocals = null;複製代碼

接着往下看代碼,若是獲取的時候map不爲空,則經過set方法把Thread類的threadLocals變量更新。若是是第一次建立的時候則初始化Thread的threadLocals變量。
下方是createMap的代碼:

123複製代碼
void createMap(Thread t, T firstValue) {    
    t.threadLocals = new ThreadLocalMap(this, firstValue); 
   }複製代碼

接下來看個get方法就比較容易理解了。

12345678910111213複製代碼
public T get() {  
      Thread t = Thread.currentThread();    
    ThreadLocalMap map = getMap(t);      
  if (map != null) {       
     ThreadLocalMap.Entry e = map.getEntry(this);   
         if (e != null) {                
@SuppressWarnings("unchecked")       
         T result = (T)e.value;            
    return result;        
    }     
   }     
   return setInitialValue();  
  }複製代碼

注意關注最後的一個return,看到調用的這個方法名咱們就能夠發現這個ThreadLocal的初始化原來是當第一調用get方法時若是尚未被set的時候纔會去獲取initialValue 方法的返回值。

12345678910複製代碼
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;    
}複製代碼


使用ThreadLocal最應該注意的事項

首先來看一下線程退出的辦法:

123456789101112複製代碼
private void exit() {   
     if (group != null) {    
        group.threadTerminated(this);   
        group = null;  
      }     
   target = null;    
    threadLocals = null; 
       inheritableThreadLocals = null;   
     inheritedAccessControlContext = null;  
      blocker = null;     
   uncaughtExceptionHandler = null;  
  }複製代碼

咱們看到當線程結束的時候上方第7行會把ThreadLocal的值製爲空,這個東西自己是沒問題的。可是,若是你是使用的線程池,這個問題可就大了!!!
要知道線程池裏的線程執行完一個任務以後緊接着下一個,這中間線程可不會結束,下一個任務得到Thread的值但是上一個任務的遺留數據。
下面是這個問題的示例代碼:

12345678910111213141516171819202122複製代碼
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {    
    @Override  
      public Integer initialValue() {  
          return 0;   
     }  
  };   
 static class ThreadDemo implements Runnable {    
    @Override       
 public void run() {         
   for (int i = 0; i < 1000; i++) {      
          threadLocal.set(threadLocal.get() + 1);         
   }         
   System.out.println("thread :" + Thread.currentThread().getId() + " is" + threadLocal.get());       
     //threadLocal.remove();    
    }   
 }   
 public static void main(String[] args) {      
  ExecutorService executorService= Executors.newFixedThreadPool(5);   
     for (int i = 0; i < 10; i++) {       
     executorService.submit(new Thread(new ThreadDemo()));       
 } 
   }複製代碼

執行這段代碼你就會發現一樣的操做在線程池裏已經得不到同樣的結果了。想要解決這種問題也很簡單,只須要把ThreadLocal的值在線程執行完清空就能夠了。把第14行註釋的代碼放開再執行如下你就明白了。


InheritableThreadLocal

其實ThreadLocal還有一個比較強大的子類InheritableThreadLocal,它呢能夠把父線程生成的變量傳遞給子線程。
下面來看一下代碼示例:

123456789101112131415161718複製代碼
public class InheritableThreadLocalDemo {    private static  InheritableThreadLocal<Integer> inheritableThreadLocal = new  InheritableThreadLocal<Integer>();    static class ThreadDemo implements Runnable {        @Override        public void run() {            for (int i = 0; i < 1000; i++) {                inheritableThreadLocal.set(inheritableThreadLocal.get() + 1);            }            System.out.println("thread :" + Thread.currentThread().getId() + " is" + inheritableThreadLocal.get());        }    }    public static void main(String[] args) {        inheritableThreadLocal.set(24);        for (int i = 0; i < 10; i++) {            new Thread(new ThreadDemo()).start();        }    }}複製代碼

執行代碼會發現程序輸出全是1024,這就是由於InheritableThreadLocal吧在主線程設置的值24傳遞到了那10個子線程中。


InheritableThreadLocal原理剖析

接下來咱們來看一下InheritableThreadLocal爲何能夠實現這種功能呢。
InheritableThreadLocal是ThreadLocal的子類,
與ThreadLocal相同的set方法

12345678複製代碼
public void set(T value) {       Thread t = Thread.currentThread();       ThreadLocalMap map = getMap(t);       if (map != null)           map.set(this, value);       else           createMap(t, value);   }複製代碼

不一樣點是InheritableThreadLocal重寫了createMap方法,將值賦值給了線程的inheritableThreadLocals變量。

123複製代碼
void createMap(Thread t, T firstValue) {        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);    }複製代碼

再跟進去Thread類的源碼看inheritableThreadLocals變量你會發現:我去,這不是跟Threadlocal同樣麼,一樣初始值爲null,線程退出的時候清空。沒錯,就是這樣的。也就是說它其實也是一個線程私有的變量,ThreadLocal的功能它是都有的。

那麼它又是怎麼把父線程的變量傳遞到子線程的呢?
接着看Thread的構造方法

123複製代碼
public Thread() {    init(null, null, "Thread-" + nextThreadNum(), 0);}複製代碼

一路追蹤init方法你會看見這段代碼:

12345678910111213141516171819202122232425262728293031323334353637383940複製代碼
private void init(ThreadGroup g, Runnable target, String name,                      long stackSize, AccessControlContext acc) {        if (name == null) {            throw new NullPointerException("name cannot be null");        }        this.name = name;        Thread parent = currentThread();        SecurityManager security = System.getSecurityManager();        if (g == null) {            if (security != null) {                g = security.getThreadGroup();            }            if (g == null) {                g = parent.getThreadGroup();            }        }        g.checkAccess();        if (security != null) {            if (isCCLOverridden(getClass())) {                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);            }        }        g.addUnstarted();        this.group = g;        this.daemon = parent.isDaemon();        this.priority = parent.getPriority();        if (security == null || isCCLOverridden(parent.getClass()))            this.contextClassLoader = parent.getContextClassLoader();        else            this.contextClassLoader = parent.contextClassLoader;        this.inheritedAccessControlContext =                acc != null ? acc : AccessController.getContext();        this.target = target;        setPriority(priority);        if (parent.inheritableThreadLocals != null)            this.inheritableThreadLocals =                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);        this.stackSize = stackSize;        tid = nextThreadID();    }複製代碼

仔細觀察倒數第5行到倒數第二行你就明白了。

本文全部源碼github.com/shiyujun/sy…

相關文章
相關標籤/搜索