1. 程式人生 > >Java中ThreadLocal,成員變數和區域性變數。

Java中ThreadLocal,成員變數和區域性變數。

一.成員變數和區域性變數

1.程式例子:

public class HelloThreadTest
{
    public static void main(String[] args)
    {
        HelloThread r = new HelloThread();

        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);

        t1.start();
        t2.start();
    }
}

class HelloThread implements Runnable
{
    int i;

    @Override
    public void run()
    {
        while (true)
        {
            System.out.println("Hello number: " + i++);

            try
            {
                Thread.sleep((long) Math.random() * 1000);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }

            if (50 == i)
            {
                break;
            }
        }
    }
}

        該例子中,HelloThread類實現了Runnable介面,其中run()方法的主要工作是輸出"Hello number: "字串加數字i,並且同時遞增i,當i到達50時,退出迴圈。

  main()方法中生成了一個HelloThread類的物件r,並且利用這個一個物件生成了兩個執行緒。

  程式的執行結果是:順次列印了0到49的數字,共50個數字。

  這是因為,i是成員變數,則HelloThread的物件r只包含這一個i,兩個Thread物件因為由r構造,所以共享了同一個i

  當我們改變程式碼如下時(原先的成員變數i被註釋掉,增加了方法中的區域性變數i):

public class HelloThreadTest
{
    public static void main(String[] args)
    {
        HelloThread r = new HelloThread();

        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);

        t1.start();
        t2.start();

    }

}

class HelloThread implements Runnable
{
    // int i;
    // 若i是成員變數,則HelloThread的物件r只包含這一個i,兩個Thread物件因為由r構造,所以共享了同一個i
    // 列印結果是0到49的數字
    @Override
    public void run()
    {
        int i = 0;
        // 每一個執行緒都會擁有自己的一份區域性變數的拷貝
        // 執行緒之間互不影響
        // 所以會列印100個數字,0到49每個數字都是兩遍
        while (true)
        {
            System.out.println("Hello number: " + i++);

            try
            {
                Thread.sleep((long) Math.random() * 1000);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }

            if (50 == i)
            {
                break;
            }
        }
    }
}

如註釋中所述,由於區域性變數對於每一個執行緒來說都有自己的拷貝,所以各個執行緒之間不再共享同一個變數,輸出結果為100個數字,實際上是兩組,每組都是0到49的50個數字,並且兩組數字之間隨意地穿插在一起。 

2.小結

  如果一個變數是成員變數,那麼多個執行緒對同一個物件的成員變數進行操作時,它們對該成員變數是彼此影響的,也就是說一個執行緒對成員變數的改變會影響到另一個執行緒。

  如果一個變數是區域性變數,那麼每個執行緒都會有一個該區域性變數的拷貝(即便是同一個物件中的方法的區域性變數,也會對每一個執行緒有一個拷貝),一個執行緒對該區域性變數的改變不會影響到其他執行緒。

區域性變數定義:在方法內定義的變數稱為“區域性變數”或“臨時變數”,方法結束後區域性變數佔用的記憶體將被釋放。

成員變數定義:在類體的變數部分中定義的變數,也稱為欄位。

全域性變數定義:java中沒有全域性變數的定義,全域性變數,又稱“外部變數”,它不是屬於哪個方法,作用域從定義的地址開始到原始檔結束。

注意事項:當局部變數與全域性變數重名時,起作用的是區域性變數。

二.ThreadLocal

1.ThreadLocal概述

早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal為解決多執行緒程式的併發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多執行緒程式。
  ThreadLocal,顧名思義,它不是一個執行緒,而是執行緒的一個本地化物件。當工作於多執行緒中的物件使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒分配一個獨立的變數副本。所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其他執行緒所對應的副本。從執行緒的角度看,這個變數就像是執行緒的本地變數,這也是類名中“Local”所要表達的意思。
  執行緒區域性變數並不是Java的新發明,很多語言(如IBM XL、FORTRAN)在語法層面就提供執行緒區域性變數。在Java中沒有提供語言級支援,而以一種變通的方法,通過ThreadLocal的類提供支援。所以,在Java中編寫執行緒區域性變數的程式碼相對來說要笨拙一些,這也是為什麼執行緒區域性變數沒有在Java開發者中得到很好普及的原因。

  學習JDK中的類,首先看下JDK API對此類的描述,描述如下:

  該類提供了執行緒區域性 (thread-local) 變數。這些變數不同於它們的普通對應物,因為訪問某個變數(通過其 get 或 set 方法)的每個執行緒都有自己的區域性變數,它獨立於變數的初始化副本。ThreadLocal 例項通常是類中的 private static 欄位,它們希望將狀態與某一個執行緒(例如,使用者 ID 或事務 ID)相關聯。
  API表達了下面幾種觀點:

  1、ThreadLocal不是執行緒,是執行緒的一個變數,你可以先簡單理解為執行緒類的屬性變數。

  2、ThreadLocal在類中通常定義為靜態變數。

  3、每個執行緒有自己的一個ThreadLocal,它是變數的一個“拷貝”,修改它不影響其他執行緒。

  既然定義為類變數,為何為每個執行緒維護一個副本(姑且稱為“拷貝”容易理解),讓每個執行緒獨立訪問?多執行緒程式設計的經驗告訴我們,對於執行緒共享資源(你可以理解為屬性),資源是否被所有執行緒共享,也就是說這個資源被一個執行緒修改是否影響另一個執行緒的執行,如果影響我們需要使用synchronized同步,讓執行緒順序訪問。

  ThreadLocal適用於資源共享但不需要維護狀態的情況,也就是一個執行緒對資源的修改,不影響另一個執行緒的執行;這種設計是‘空間換時間’,synchronized順序執行是‘時間換取空間’。

2. ThreadLocal方法及使用示例

  ThreadLocal<T>類在Spring,Hibernate等框架中起到了很大的作用。為了解釋ThreadLocal類的工作原理,必須同時介紹與其工作甚密的其他幾個類,包括內部類ThreadLocalMap,和執行緒類Thread。所有方法如下圖:

四個核心方法說明如下:

                   T         get()                   返回此執行緒區域性變數的當前執行緒副本中的值。

 protected  T         initialValue()       返回此執行緒區域性變數的當前執行緒的“初始值”。

                   void remove()             移除此執行緒區域性變數當前執行緒的值。

                   void set(T value)         將此執行緒區域性變數的當前執行緒副本中的值設定為指定值。

 

三.ThreadLocal與區域性變數

1.程式列子

public class ThreadLocalLearn { 

    static ThreadLocal<IntHolder> tl = new ThreadLocal<IntHolder>(){ 
            protected IntHolder initialValue() { 
            return new IntHolder(); 
        } 
    }; 

    public static void main(String args[]) { 
        for(int i=0; i<5; i++) { 
            Thread th = new Thread(new ThreadTest(tl, i)); 
            th.start(); 
        } 
    } 
} 

class ThreadTest implements Runnable{ 

    ThreadLocal<IntHolder> tl ; //threadlocal變數 
    int i;  //執行緒區域性變數 (ThreadTest的成員變數)
    int a = 3; //執行緒區域性變數(ThreadTest的成員變數) 

    public ThreadTest(ThreadLocal<IntHolder> tl, int i) { 
        super(); 
        this.tl = tl; 
        this.i = i; 
    } 

    @Override 
    public void run() { 
        tl.get().increAndGet(); 
        a++; 
        System.out.println(tl.get().getA() + " "); 
        System.out.println("a : " + a); 
    } 
} 

class IntHolder{ 

    int a = 1; 

    public int getA() { 
        return a; 
    } 

    public void setA(int a) { 
        this.a = a; 
    }

    public int increAndGet() {
         return ++a; 
    } 
}

程式碼的執行結果如下:

2 
a : 4
2 
a : 4
2 
a : 4
2 
a : 4
2 
a : 4

          可以看到,區域性變數和ThreadLocal起到的作用是一樣的,保證了併發環境下資料的安全性。

2.小結

根據上述結論那就是說,完全可以用區域性變數來代替ThreadLocal咯,這樣想法對麼?我們看一看官方對於ThreadLocal的描述:

    This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

翻譯起來就是

    ThreadLocal提供的是一種執行緒區域性變數。這些變數不同於其它變數的點在於每個執行緒在獲取變數的時候,都擁有它自己相對獨立的變數初始化拷貝。ThreadL:ocal的例項一般是私有靜態的,可以做到與一個執行緒繫結某一種狀態。PS:有更好的翻譯請指教。

      所以就這段話而言,我們知道ThreadLocal不是為了滿足多執行緒安全而開發出來的,因為區域性變數已經足夠安全。ThreadLocal是為了方便執行緒處理自己的某種狀態。
      可以看到ThreadLocal例項化所處的位置,是一個執行緒共有區域。好比一個銀行和個人,我們可以把錢存在銀行,也可以把錢存在家。存在家裡的錢是區域性變數,僅供個人使用;存在銀行裡的錢也不是說可以讓別人隨便使用,只有我們以個人身份去獲取才能得到。所以說ThreadLocal封裝的變數我們是在外面某個區域儲存了處於我們個人的一個狀態,只允許我們自己去訪問和修改的狀態。
      ThreadLocal同時提供了初始化的機制,在例項化時重寫initialValue()方法,便可實現變數的初始化工作

但注意安全性問題

//method 1 
static ThreadLocal<IntHolder> tl = new ThreadLocal<IntHolder>(){ 
    protected IntHolder initialValue() { 
        return new IntHolder(); 
    } 
}; 

//method 2 
IntHolder intHolder = new IntHolder(); 
    static ThreadLocal<IntHolder> tl = new ThreadLocal<IntHolder>(){ 
        protected IntHolder initialValue() { 
        return intHolder; 
    } 
};

         方法一和方法二都可以實現初始化工作,但是方法二不能保證執行緒變數的安全性,因為引用拷貝指向的是同一個例項,對引用拷貝的修改,等同於對例項的修改。

當然,也可以在判斷ThreadlLocal獲取資料為空時,線上程內部為ThreadLocal例項化一個數據。如下

if(null == tl.get()) {
    tl.set(new IntHolder());
}