1. 程式人生 > >ThreadLocal 那點事兒

ThreadLocal 那點事兒

ThreadLocal,直譯為“執行緒本地”或“本地執行緒”,如果你真的這麼認為,那就錯了!其實,它就是一個容器,用於存放執行緒的區域性變數,我認為應該叫做 ThreadLocalVariable(執行緒區域性變數)才對,真不理解為什麼當初 Sun 公司的工程師這樣命名。

早在 JDK 1.2 的時代,java.lang.ThreadLocal 就誕生了,它是為了解決多執行緒併發問題而設計的,只不過設計得有些難用,所以至今沒有得到廣泛使用。其實它還是挺有用的,不相信的話,我們一起來看看這個例子吧。

一個序列號生成器的程式,可能同時會有多個執行緒併發訪問它,要保證每個執行緒得到的序列號都是自增的,而不能相互干擾。

先定義一個介面:

1

2

3

4

public interface Sequence {

 

    int getNumber();

}

每次呼叫 getNumber() 方法可獲取一個序列號,下次再呼叫時,序列號會自增。

再做一個執行緒類:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public class ClientThread extends Thread {

 

    

private Sequence sequence;

 

    public ClientThread(Sequence sequence) {

        this.sequence = sequence;

    }

 

    @Override

    public void run() {

        

for (int i = 0; i < 3; i++) {

            System.out.println(Thread.currentThread().getName() + " => " + sequence.getNumber());

        }

    }

}

線上程中連續輸出三次執行緒名與其對應的序列號。

我們先不用 ThreadLocal,來做一個實現類吧。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

public class SequenceA implements Sequence {

 

    private static int number = 0;

 

    public int getNumber() {

        number = number + 1;

        return number;

    }

 

    public static void main(String[] args) {

        Sequence sequence = new SequenceA();

 

        ClientThread thread1 = new ClientThread(sequence);

        ClientThread thread2 = new ClientThread(sequence);

        ClientThread thread3 = new ClientThread(sequence);

 

        thread1.start();

        thread2.start();

        thread3.start();

    }

}

序列號初始值是0,在 main() 方法中模擬了三個執行緒,執行後結果如下:

Thread-0 => 1
Thread-0 => 2
Thread-0 => 3
Thread-2 => 4
Thread-2 => 5
Thread-2 => 6
Thread-1 => 7
Thread-1 => 8
Thread-1 => 9

由於執行緒啟動順序是隨機的,所以並不是0、1、2這樣的順序,這個好理解。為什麼當 Thread-0 輸出了1、2、3之後,而 Thread-2 卻輸出了4、5、6呢?執行緒之間竟然共享了 static 變數!這就是所謂的“非執行緒安全”問題了。

那麼如何來保證“執行緒安全”呢?對應於這個案例,就是說不同的執行緒可擁有自己的 static 變數,如何實現呢?下面看看另外一個實現吧。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

public class SequenceB implements Sequence {

 

    private static ThreadLocal<Integer> numberContainer = new ThreadLocal<Integer>() {

        @Override

        protected Integer initialValue() {

            return 0;

        }

    };

 

    public int getNumber() {

        numberContainer.set(numberContainer.get() + 1);

        return numberContainer.get();

    }

 

    public static void main(String[] args) {

        Sequence sequence = new SequenceB();

 

        ClientThread thread1 = new ClientThread(sequence);

        ClientThread thread2 = new ClientThread(sequence);

        ClientThread thread3 = new ClientThread(sequence);

 

        thread1.start();

        thread2.start();

        thread3.start();

    }

}

通過 ThreadLocal 封裝了一個 Integer 型別的 numberContainer 靜態成員變數,並且初始值是0。再看 getNumber() 方法,首先從 numberContainer 中 get 出當前的值,加1,隨後 set 到 numberContainer 中,最後將 numberContainer 中 get 出當前的值並返回。

是不是很噁心?但是很強大!確實稍微饒了一下,我們不妨把 ThreadLocal 看成是一個容器,這樣理解就簡單了。所以,這裡故意用 Container 這個單詞作為字尾來命名 ThreadLocal 變數。

執行結果如何呢?看看吧。

Thread-0 => 1
Thread-0 => 2
Thread-0 => 3
Thread-2 => 1
Thread-2 => 2
Thread-2 => 3
Thread-1 => 1
Thread-1 => 2
Thread-1 => 3

每個執行緒相互獨立了,同樣是 static 變數,對於不同的執行緒而言,它沒有被共享,而是每個執行緒各一份,這樣也就保證了執行緒安全。 也就是說,TheadLocal 為每一個執行緒提供了一個獨立的副本!

搞清楚 ThreadLocal 的原理之後,有必要總結一下 ThreadLocal 的 API,其實很簡單。

  1. public void set(T value):將值放入執行緒區域性變數中
  2. public T get():從執行緒區域性變數中獲取值
  3. public void remove():從執行緒區域性變數中移除值(有助於 JVM 垃圾回收)
  4. protected T initialValue():返回執行緒區域性變數中的初始值(預設為 null)

為什麼 initialValue() 方法是 protected 的呢?就是為了提醒程式設計師們,這個方法是要你們來實現的,請給這個執行緒區域性變數一個初始值吧。

瞭解了原理與這些 API,其實想想 ThreadLocal 裡面不就是封裝了一個 Map 嗎?自己都可以寫一個 ThreadLocal 了,嘗試一下吧。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

public class MyThreadLocal<T> {

 

    private Map<Thread, T> container = Collections.synchronizedMap(new HashMap<Thread, T>());

 

    public void set(T value) {

        container.put(Thread.currentThread(), value);

    }

 

    public T get() {

        Thread thread = Thread.currentThread();

        T value = container.get(thread);

        if (value == null && !container.containsKey(thread)) {

            value = initialValue();

            container.put(thread, value);

        }

        return value;

    }

 

    public void remove() {

        container.remove(Thread.currentThread());

    }

 

    protected T initialValue() {

        return null;

    }

}

以上完全山寨了一個 ThreadLocal,其中中定義了一個同步 Map(為什麼要這樣?請讀者自行思考),程式碼應該非常容易讀懂。

下面用這 MyThreadLocal 再來實現一把看看。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

public class SequenceC implements Sequence {

 

    private static MyThreadLocal<Integer> numberContainer = new MyThreadLocal<Integer>() {

        @Override

        protected Integer initialValue() {

            return 0;

        }

    };

 

    public int getNumber() {

        numberContainer.set(numberContainer.get() + 1);

        return numberContainer.get();

    }

 

    public static void main(String[] args) {

        Sequence sequence = new SequenceC();

 

        ClientThread thread1 = new ClientThread(sequence);

        ClientThread thread2 = new ClientThread(sequence);

        ClientThread thread3 = new ClientThread(sequence);

 

        thread1.start();

        thread2.start();

        thread3.start();

    }

}

以上程式碼其實就是將 ThreadLocal 替換成了 MyThreadLocal,僅此而已,執行效果和之前的一樣,也是正確的。

其實 ThreadLocal 可以單獨成為一種設計模式,就看你怎麼看了。

ThreadLocal 具體有哪些使用案例呢?

我想首先要說的就是:通過 ThreadLocal 存放 JDBC Connection,以達到事務控制的能力。

如何實現呢?下回分解!

ThreadLocal 那點事兒(續集)

注意:當您在一個類中使用了 static 成員變數的時候,一定要多問問自己,這個 static 成員變數需要考慮“執行緒安全”嗎?(也就是說,多個執行緒需要獨享自己的 static 成員變數嗎?)如果需要考慮,那就請用 ThreadLocal 吧!

 

http://www.importnew.com/20147.html