Java併發程式設計:執行緒封閉和ThreadLocal詳解

方誌朋發表於2017-08-27

轉載請標明出處:
blog.csdn.net/forezp/arti…
本文出自方誌朋的部落格

什麼是執行緒封閉

當訪問共享變數時,往往需要加鎖來保證資料同步。一種避免使用同步的方式就是不共享資料。如果僅在單執行緒中訪問資料,就不需要同步了。這種技術稱為執行緒封閉。在Java語言中,提供了一些類庫和機制來維護執行緒的封閉性,例如區域性變數和ThreadLocal類,本文主要深入講解如何使用ThreadLocal類來保證執行緒封閉。

理解ThreadLocal類

ThreadLocal類能使執行緒中的某個值與儲存值的物件關聯起來,它提供了get、set方法,這些方法為每個使用該變數的執行緒儲存一份獨立的副本,因此get總是set當前執行緒的set最新值。

首先我們來看個例子,這個例子來自於www.cnblogs.com/dolphin0520…


public class Test1 {

    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
    ThreadLocal<String> stringLocal = new ThreadLocal<String>();


    public void set() {
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName());
    }

    public long getLong() {
        return longLocal.get();
    }

    public String getString() {
        return stringLocal.get();
    }
    public static void main(String[] args) throws InterruptedException {
        final Test1 test = new Test1();


        test.set();
        System.out.println(test.getLong());
        System.out.println(test.getString());


        Thread thread1 = new Thread(() -> {
            test.set();
            System.out.println(test.getLong());
            System.out.println(test.getString());
        });
        thread1.start();
        thread1.join();

        System.out.println(test.getLong());
        System.out.println(test.getString());
    }
}複製程式碼

執行該程式,程式碼輸出的結果為:

1

main

10

Thread-0

1

main

從這段程式碼可以看出在mian執行緒和thread1執行緒確實都儲存著各自的副本,它們的副本各自不干擾。

ThreadLocal原始碼解析

來從原始碼的角度來解析ThreadLocal這個類,這個類存放在java.lang包,這個類有很多方法。

image.png
image.png

它內部又個ThreadLocalMap類,主要有set()、get()、setInitialValue 等方法。

首先來看下set方法,獲取當前Thread的 map,如果不存在則新建一個並設定值,如果存在設定值,原始碼如下:

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

跟蹤createMap,可以發現它根據Thread建立來一個ThreadLocalMap。

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

t.threadLocals為當前執行緒的一個變數,也就是ThreadLocal的資料都是存放在當前執行緒的threadLocals變數裡面的,由此可見用ThreadLocal存放的資料是執行緒安全的。因為它對於不同的執行緒來,使用ThreadLocal的set方法都會根據執行緒判斷該執行緒是否存在它的threadLocals成員變數,如果沒有就建一個,有的話就存下資料。

ThreadLocal.ThreadLocalMap threadLocals = null;複製程式碼

ThreadLocalMap為ThreadLocal的一個內部類,原始碼如下:

 static class ThreadLocalMap {

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

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }複製程式碼

可以看到ThreadLocalMap的Entry繼承了WeakReference,並且使用ThreadLocal作為鍵值。

在使用ThreadLocal的get方法之前一定要先set,要不然會報空指標異常。還有一種方式就是在初始化的時候呼叫initialValue()方法賦值。改造下之前的例子,程式碼如下:

public class Test2 {

    ThreadLocal<Long> longLocal = new ThreadLocal<Long>(){

        @Override
        protected Long initialValue() {
            return Thread.currentThread().getId();
        }
    };
    ThreadLocal<String> stringLocal = new ThreadLocal<String>(){
        @Override
        protected String initialValue() {
            return Thread.currentThread().getName();
        }
    };

    public long getLong() {
        return longLocal.get();
    }

    public String getString() {
        return stringLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {
        final Test2 test = new Test2();



        System.out.println(test.getLong());
        System.out.println(test.getString());


        Thread thread1 = new Thread(() -> {

            System.out.println(test.getLong());
            System.out.println(test.getString());
        });
        thread1.start();
        thread1.join();

        System.out.println(test.getLong());
        System.out.println(test.getString());
    }
}複製程式碼

執行該程式,程式碼輸出的結果為:

1

main

10

Thread-0

1

main

ThreadLocal常用的使用場景

通常講JDBC連線儲存在ThreadLocal物件中,每個物件都有屬於自己的連線,程式碼如下:

private static ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
    public Connection initialValue() {
       return DriverManager.getConnection(DB_URL);
    }
};

public static Connection getConnection() {
    return connectionHolder.get();
}複製程式碼

參考資料

《Java併發程式設計實戰》

《深入理解JVM》

關注我的公眾號

精彩內容不能錯過!

forezp.jpg
forezp.jpg

相關文章