程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> Java並發:ThreadLocal詳解

Java並發:ThreadLocal詳解

編輯:JAVA綜合教程

Java並發:ThreadLocal詳解


前言

最近看多線程的時候看到ThreadLocal這個類,就baidu查了一下。在最開始查到的文章對這個類最多的說明就是ThreadLocal為解決多線程程序的並發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多線程程序。現在請忘掉這些說明,因為他徹底地錯了!!!看了這些blog後會讓你更加混亂,因為他們的對ThreadLocal的描述根本無法推出如何解決多線程並發。這讓你看了之後根本搞不清楚ThreadLocal到底是做什麼的。

我們看下這些blog中對於ThreadLocal性質的描述:當使用ThreadLocal維護變量時,ThreadLocal為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本.

看到沒ThreadLocal會為每個使用該變量的線程提供獨立的副本!!!也就是說每個線程間的操作都不會影響到其他線程,相當於建立了一個線程內部聲明周期的局部變量,不會影響其他線程也根本就無所謂的同步了。所以,他們的邏輯都是混亂的,寫這些就是誤人子弟。

我看了這些之後,就一直不明白ThreadLocal的具體作用是那些,直到我看到winwill2012了一篇博客才明白。這篇博客也是參考了該文章,文章地址在這裡[Java並發包學習七]解密ThreadLocal
這篇博客中隊ThreadLocal作用的解釋是:ThreadLocal的作用是提供線程內的局部變量,這種變量在線程的生命周期內起作用,減少同一個線程內多個函數或者組件之間一些公共變量的傳遞的復雜度。

ThreadLocal方法介紹

threadLocal只有4個基本方法,分別是void set(T value),T get(),void remove()以及T initialValue()。下面分別說明:

initialValue()方法

此方法用來返回當前線程在ThreadLocal中的初始值,該函數在調用get()的時候會第一次調用,但是如果一開始就調用了set(),則該函數不會被調用。通常該函數只會被調用一次,除非手動調用了remove()之後又調用get(),這種情況下get()中還是會調用initialValue()。具體實現如下:

protected T  initialValue() {
     return null;
}

該方法是protected類型的,很顯然是建議在子類重載該方法的,所以通常該方法都會以匿名內部類的形式被重載,以指定初始值,例如:

 private static final ThreadLocal value = new ThreadLocal() {
        @Override
        protected Integer initialValue() {
            return Integer.valueOf(1);
        }
    };

get()方法

該方法用戶獲取與當前線程關聯的ThreadLocal值,方法聲明如下

public T get()

set()方法

該方法用於設置與當前線程關聯的ThreadLocal值,方法聲明如下

public void set(T value)

remove()方法

該方法用於將當前線程的ThreadLocal綁定的值刪除,方法聲明如下

public void remove()

示例

用一段代碼來演示ThreadLocal的用法

public class Test {
    private static final ThreadLocal value = new ThreadLocal() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        for(int i = 1 ;i<=5;i++){
            new Thread(new LocalThread(i)).start();
        }
    }

    static class LocalThread implements  Runnable{
        private int index;

        public LocalThread(int index) {
            this.index = index;
        }

        public void run() {
              System.out.println("線程" + index + "的初始value:" + value.get());
                for (int i = 0; i < 10; i++) {
                    value.set(value.get() + i);
                }
                System.out.println("線程" + index + "的累加value:" + value.get());
        }
    }

}   

運行結果:

線程1的初始value:0
線程5的初始value:0
線程1的累加value:45
線程2的初始value:0
線程2的累加value:45
線程3的初始value:0
線程4的初始value:0
線程4的累加value:45
線程3的累加value:45
線程5的累加value:45

可以看到各個線程中的value都是獨立的,本線程的累加操作不會影響到其他線程的值,真正達到了線程內部隔離的效果。

源碼解析

源碼摘自jdk8,我們先看get()源碼:

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();
  }

可以看到get()方法調用時,首先會調用getMap()方法獲取一個ThreadLocalMap對象,我們先來看getMap()的源碼:

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

從源碼上可以看到是獲取的當前線程中的threadLocals成員對象,

ThreadLocal.ThreadLocalMap threadLocals = null;

然後我們再看ThreadLocal get()方法中的setInitailValue()方法

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;
}

createMap()方法源碼:

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

現在我們就大概的知道了get方法獲取值得流程:
1. 首先Thread.currentThread()獲取當前線程
2. 根據當前線程,獲取線程中的一個Map,如果map為空則轉4
3. 如果這個map不為空的話,則在map中獲取以當前ThreadLocal引用作為key相對應的value e,如果e不為空,返回e.value,若為null則轉4
4. Map為空或者e為空,則通過initialValue函數獲取初始值value,然後用ThreadLocal的引用和value作為firstKey和firstValue創建一個新的Map

所以,可以總結一下ThreadLocal的設計思路:
每個Thread維護一個ThreadLocalMap映射表,這個映射表的key是ThreadLocal實例本身,value是真正需要存儲的Object。

為什麼要設計的這麼麻煩而不是直接在ThreadLocal中維護一個Map,然後以線程ID作為Map的key。查閱了一下資料,這樣設計的主要有以下幾點優勢:
* 這樣設計之後每個Map的Entry數量變小了:之前是Thread的數量,現在是ThreadLocal的數量,能提高性能,據說性能的提升不是一點兩點(沒有親測)
* 當Thread銷毀之後對應的ThreadLocalMap也就隨之銷毀了,能減少內存使用量。

內存洩漏問題

網上流傳使用ThreadLocal有可能會導致內存洩漏,具體原因是什麼呢?真的是這樣嗎?

首先ThreadLocal存儲數據都是存在一個ThreadLocalMap類型的對象中,這個類是ThreadLocal的一個內部類,我們先看一下ThreadLocalMap的相關源碼:

static class ThreadLocalMap {

        static class Entry extends WeakReference> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
        ...
        ...
}

可以看到ThreadLocalMap是使用ThreadLocal的弱引用作為Key的

引用關系圖

上面是本文介紹到的一些對象之間的引用關系圖,實線表示強引用,虛線表示弱引用。

然後網上就傳言,ThreadLocal會引發內存洩露,他們的理由是這樣的:

如上圖,ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用引用他,那麼系統gc的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當前線程再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鏈:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永遠無法回收,造成內存洩露。<喎?http://www.Bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjwvYmxvY2txdW90ZT4NCjxwPrWrysfKtbzKyc9UaHJlYWRMb2NhbE1hcLXEyei8xtLRvq2/vMLHtb3V4tbWx+m/9sHLoaPH67+0VGhyZWFkTG9jYWxNYXC1xGdldEVudHJ5t723qLXE1LTC66O6PC9wPg0KPHByZSBjbGFzcz0="brush:java;"> private Entry getEntry(ThreadLocal key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }

getEntryAfterMiss()源碼:

private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
     Entry[] tab = table;
     int len = tab.length;

     while (e != null) {
         ThreadLocal k = e.get();
         if (k == key)
             return e;
         if (k == null)
             expungeStaleEntry(i);
         else
             i = nextIndex(i, len);
         e = tab[i];
     }
     return null;
 }

expungeStaleEntry()源碼:

private int expungeStaleEntry(int staleSlot) {
           Entry[] tab = table;
           int len = tab.length;

           // expunge entry at staleSlot
           tab[staleSlot].value = null;
           tab[staleSlot] = null;
           size--;

           // Rehash until we encounter null
           Entry e;
           int i;
           for (i = nextIndex(staleSlot, len);
                (e = tab[i]) != null;
                i = nextIndex(i, len)) {
               ThreadLocal k = e.get();
               if (k == null) {
                   e.value = null;
                   tab[i] = null;
                   size--;
               } else {
                   int h = k.threadLocalHashCode & (len - 1);
                   if (h != i) {
                       tab[i] = null;

                       // Unlike Knuth 6.4 Algorithm R, we must scan until
                       // null because multiple entries could have been stale.
                       while (tab[h] != null)
                           h = nextIndex(h, len);
                       tab[h] = e;
                   }
               }
           }
           return i;
       }

整理一下ThreadLocalMap的getEntry函數的流程:
1. 首先從ThreadLocal的直接索引位置(通過ThreadLocal.threadLocalHashCode & (len-1)運算得到)獲取Entry e,如果e不為null並且key相同則返回e;
2. 如果e為null或者key不一致則向下一個位置查詢,如果下一個位置的key和當前需要查詢的key相等,則返回對應的Entry,否則,如果key值為null,則擦除該位置的Entry,然後重新調整容器,否則繼續向下一個位置查詢

在這個過程中遇到的key為null的Entry都會被擦除,那麼Entry內的value也就沒有強引用鏈,自然會被回收。仔細研究代碼可以發現,set操作也有類似的思想,將key為null的這些Entry都刪除,防止內存洩露。
但是光這樣還是不夠的,上面的設計思路依賴一個前提條件:要調用ThreadLocalMap的getEntry函數或者set函數。這當然是不可能任何情況都成立的,所以很多情況下需要使用者手動調用ThreadLocal的remove函數,手動刪除不再需要的ThreadLocal,防止內存洩露。所以JDK建議將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命周期就更長,由於一直存在ThreadLocal的強引用,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱引用訪問到Entry的value值,然後remove它,防止內存洩露。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved