理解Java中的ThreadLocal

aaanly 8年前發布 | 17K 次閱讀 Java Android Java開發 ThreadLocal

提到ThreadLocal,有些Android或者Java程序員可能有所陌生,可能會提出種種問題,它是做什么的,是不是和線程有關,怎么使用呢?等等問題,本文將總結一下我對ThreadLocal的理解和認識,希望讓大家理解ThreadLocal更加透徹一些。

ThreadLocal是什么

ThreadLocal是一個關于創建線程局部變量的類。

通常情況下,我們創建的變量是可以被任何一個線程訪問并修改的。而使用ThreadLocal創建的變量只能被當前線程訪問,其他線程則無法訪問和修改。

Global && Local

上面的兩個修飾看似矛盾,實則不然。

  • Global 意思是在當前線程中,任何一個點都可以訪問到ThreadLocal的值。
  • Local 意思是該線程的ThreadLocal只能被該線程訪問,一般情況下其他線程訪問不到。

用法簡介

創建,支持泛型

ThreadLocal<String> mStringThreadLocal = new ThreadLocal<>(); 

set方法

mStringThreadLocal.set("droidyue.com"); 

get方法

mStringThreadLocal.get(); 

完整的使用示例

<div><pre class="brush:java; toolbar: true; auto-links: false;">private void testThreadLocal() {
Thread t = new Thread() {
    ThreadLocal<String> mStringThreadLocal = new ThreadLocal<>();

    @Override
    public void run() {
        super.run();
        mStringThreadLocal.set("droidyue.com");
        mStringThreadLocal.get();
    }
};

t.start();

}</pre>
</div>

ThreadLocal初始值

為ThreadLocal設置默認的get初始值,需要重寫initialValue方法,下面是一段代碼,我們將默認值修改成了線程的名字

<div><pre class="brush:java; toolbar: true; auto-links: false;">ThreadLocal<String> mThreadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
  return Thread.currentThread().getName();
}

};</pre>

</div>

Android中的應用

在Android中,Looper類就是利用了ThreadLocal的特性,保證每個線程只存在一個Looper對象。

<div><pre class="brush:java; toolbar: true; auto-links: false;">static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper(quitAllowed)); }</pre>
</div>

如何實現

為了更好的掌握ThreadLocal,我認為了解其內部實現是很有必要的,這里我們以set方法從起始看一看ThreadLocal的實現原理。

下面是ThreadLocal的set方法,大致意思為

  • 首先獲取當前線程
  • 利用當前線程作為句柄獲取一個ThreadLocalMap的對象
  • 如果上述ThreadLocalMap對象不為空,則設置值,否則創建這個ThreadLocalMap對象并設置值

源碼如下

<div><pre class="brush:java; toolbar: true; auto-links: false;">public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
    map.set(this, value);
else
    createMap(t, value);

}</pre>
</div>

下面是一個利用Thread對象作為句柄獲取ThreadLocalMap對象的代碼

<div><pre class="brush:java; toolbar: true; auto-links: false;">ThreadLocalMap getMap(Thread t) {
return t.threadLocals;

}</pre>
</div>

上面的代碼獲取的實際上是Thread對象的threadLocals變量,可參考下面代碼

<div><pre class="brush:java; toolbar: true; auto-links: false;">class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */

ThreadLocal.ThreadLocalMap threadLocals = null;

}</pre>
</div>

而如果一開始設置,即ThreadLocalMap對象未創建,則新建ThreadLocalMap對象,并設置初始值。

<div><br />

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

</div>

總結:實際上ThreadLocal的值是放入了當前線程的一個ThreadLocalMap實例中,所以只能在本線程中訪問,其他線程無法訪問。

對象存放在哪里

在Java中,棧內存歸屬于單個線程,每個線程都會有一個棧內存,其存儲的變量只能在其所屬線程中可見,即棧內存可以理解成線程的私有內存。而堆內存中的對象對所有線程可見。堆內存中的對象可以被所有線程訪問。

問:那么是不是說ThreadLocal的實例以及其值存放在棧上呢?

其實不是,因為ThreadLocal實例實際上也是被其創建的類持有(更頂端應該是被線程持有)。而ThreadLocal的值其實也是被線程實例持有。

它們都是位于堆上,只是通過一些技巧將可見性修改成了線程可見。

關于堆和棧的比較,請參考Java中的堆和棧的區別

真的只能被一個線程訪問么

既然上面提到了ThreadLocal只對當前線程可見,是不是說ThreadLocal的值只能被一個線程訪問呢?

使用InheritableThreadLocal可以實現多個線程訪問ThreadLocal的值。

如下,我們在主線程中創建一個InheritableThreadLocal的實例,然后在子線程中得到這個InheritableThreadLocal實例設置的值。

<div><pre class="brush:java; toolbar: true; auto-links: false;">private void testInheritableThreadLocal() {
final ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("droidyue.com");
Thread t = new Thread() {
    @Override
    public void run() {
        super.run();
        Log.i(LOGTAG, "testInheritableThreadLocal =" + threadLocal.get());
    }
};

t.start();

}</pre>
</div>

上面的代碼輸出的日志信息為

<div><pre class="brush:java; toolbar: true; auto-links: false;">I/MainActivity( 5046): testInheritableThreadLocal =droidyue.com</pre><br />
</div>
<p>
    使用InheritableThreadLocal可以將某個線程的ThreadLocal值在其子線程創建時傳遞過去。因為在線程創建過程中,有相關的處理邏輯。
</p>

<div><pre class="brush:java; toolbar: true; auto-links: false;">//Thread.java

private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc) { //code goes here if (parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); / Stash the specified stack size in case the VM cares / this.stackSize = stackSize;

    /* Set thread ID */
    tid = nextThreadID();

}</pre>
</div>

上面代碼就是在線程創建的時候,復制父線程的inheritableThreadLocals的數據。

會導致內存泄露么

有網上討論說ThreadLocal會導致內存泄露,原因如下

  • 首先ThreadLocal實例被線程的ThreadLocalMap實例持有,也可以看成被線程持有。
  • 如果應用使用了線程池,那么之前的線程實例處理完之后出于復用的目的依然存活
  • 所以,ThreadLocal設定的值被持有,導致內存泄露。

上面的邏輯是清晰的,可是ThreadLocal并不會產生內存泄露,因為ThreadLocalMap做選擇key的時候,并不是直接選擇ThreadLocal實例,而是ThreadLocalMap實例的弱引用。

<div><pre class="brush:java; toolbar: true; auto-links: false;">static class ThreadLocalMap {

/**

  • The entries in this hash map extend WeakReference, using
  • its main ref field as the key (which is always a
  • ThreadLocal object). Note that null keys (i.e. entry.get()
  • == null) mean that the key is no longer referenced, so the
  • entry can be expunged from table. Such entries are referred to
  • as "stale entries" in the code that follows. */ static class Entry extends WeakReference<ThreadLocal<?>> {

      /** The value associated with this ThreadLocal. */
      Object value;
    
      Entry(ThreadLocal<?> k, Object v) {
          super(k);
          value = v;
      }
    

    } }</pre>
    </div>

      所以實際上從ThreadLocal設計角度來說是不會導致內存泄露的。關于弱引用,了解更多,請訪問<a target="_self" href="/misc/goto?guid=4958961779571501899">譯文:理解Java中的弱引用 </a> 
    

    </p>

      使用場景
    

    </h2>

        <li>
            實現單個線程單例以及單個線程上下文信息存儲,比如交易id等
        </li>
        <li>
            實現線程安全,非線程安全的對象使用ThreadLocal之后就會變得線程安全,因為每個線程都會有一個對應的實例
        </li>
        <li>
            承載一些線程相關的數據,避免在方法中來回傳遞參數
        </li>
      

      </ul>

        參考文章
      

      </h2>

          <li>
              <a target="_blank" href="/misc/goto?guid=4959633324378287599">Java ThreadLocal</a> 
          </li>
          <li>
              <a target="_blank" href="/misc/goto?guid=4959669631960440168">Threadlocals and memory leaks in J2EE</a> 
          </li>
          <li>
              <a target="_blank" href="/misc/goto?guid=4959669632040918366">Java Thread Local – How to use and code sample</a> 
          </li>
          <li>
              <a target="_blank" href="/misc/goto?guid=4959669632127108343">ThreadLocal in Java – Example Program and Tutorial<br />
          </a></li>
        

        </ul> 本文來源:技術小黑屋

 本文由用戶 aaanly 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!