1. 程式人生 > >Java資料結構和算法系列3--ThreadLocal類原理詳解

Java資料結構和算法系列3--ThreadLocal類原理詳解

1.ThreadLocal介紹

Java實現多執行緒的2種方式,繼承Thread類和實現Runnable介面。今天我們介紹下另外一種常用的多執行緒類ThreadLocal類。
ThreadLocal在維護變數時,為每個使用變數的執行緒提供了獨立的副本,所以每個執行緒都可以獨立的改變自己的副本,而不影響其他執行緒對應的副本。

2.原理

ThreadLocal類介面很簡單,只有4個方法,我們先來了解一下:

void set(Object value)設定當前執行緒的執行緒區域性變數的值。

public Object get()該方法返回當前執行緒所對應的執行緒區域性變數。

public void remove()將當前執行緒區域性變數的值刪除,目的是為了減少記憶體的佔用,該方法是JDK 5.0新增的方法。需要指出的是,當執行緒結束後,對應該執行緒的區域性變數將自動被垃圾回收,所以顯式呼叫該方法清除執行緒的區域性變數並不是必須的操作,但它可以加快記憶體回收的速度。

protected Object initialValue()返回該執行緒區域性變數的初始值,該方法是一個protected的方法,顯然是為了讓子類覆蓋而設計的。這個方法是一個延遲呼叫方法,線上程第1次呼叫get()或set(Object)時才執行,並且僅執行1次。ThreadLocal中的預設實現直接返回一個null。

  值得一提的是,在JDK5.0中,ThreadLocal已經支援泛型,該類的類名已經變為ThreadLocal。API方法也相應進行了調整,新版本的API方法分別是void set(T value)、T get()以及T initialValue()。

  ThreadLocal是如何做到為每一個執行緒維護變數的副本的呢?其實實現的思路很簡單:在ThreadLocal類中有一個Map,用於儲存每一個執行緒的變數副本,Map中元素的鍵為執行緒物件,而值對應執行緒的變數副本。
  

3.Thread和ThreadLocal例項對比

//Thread例項

package com.tngtech.thread;

/*
 * 
 * @author tngtech
 * @date 2015年12月29日
 *<p>部落格:http://blog.csdn.net/jacman
 *<p>Github:https://github.com/tangthis
 *
 */
public class ThreadDemo implements Runnable{

    private Integer i = 1;

    @Override
    public
void run() { for(int j = 0 ; j < 10 ; j++){ i = i + j; } System.out.println("變數值為:" + i); } public static void main(String[] args) { ThreadDemo threadDemoRunnable = new ThreadDemo(); Thread thread1 = new Thread(threadDemoRunnable); Thread thread2 = new Thread(threadDemoRunnable); thread1.start(); //為了看到更明顯的效果,執行緒睡眠1s,再啟動另外一個執行緒 try{ Thread.sleep(1000); }catch(Exception e){ e.printStackTrace(); } thread2.start(); //列印結果 //變數值為:46 //變數值為:91 //變數在多個執行緒間是共享的 } }

在ThreadDemo中,我們修改變數i的值,根據輸出結果,發現i變數在多個執行緒間是共享的。下面看下ThreadLocal的例項:

/**
 * ThreadLocal例項
 */
package com.tngtech.thread;

/*
 * ThreadLocal例項
 * @author tngtech
 * @date 2015年12月29日
 *<p>部落格:http://blog.csdn.net/jacman
 *<p>Github:https://github.com/tangthis
 *
 */
public class ThreadLocalDemo implements Runnable{
    //支援泛型
    ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 1;
        }
    };
    public void set(Integer i){
        local.set(i);
    }

    public Integer get(){
        return ((Integer)local.get()).intValue();
    }

    @Override
    public void run() {
        Integer i = get();
        for(int j = 0 ; j < 10; j ++){
            i = i + j;
        }
        set(i);


        System.out.println("變數值為:" + i);
    }

    public static void main(String[] args) {
        ThreadLocalDemo threadRunnable = new ThreadLocalDemo();
        Thread thread1 = new Thread(threadRunnable);
        Thread thread2 = new Thread(threadRunnable);
        thread1.start();

        //為了看到更明顯的效果,執行緒睡眠1s
        try{
            Thread.sleep(1000);
        }catch(Exception e){
            e.printStackTrace();
        }
        thread2.start();

        //列印結果
        //變數值為:46
        //變數值為:46
    }

}

通過ThreadLocalDemo我們可以看出,變數i在多個執行緒見是獨立的,互不影響。

4.同步機制

ThreadLocal和執行緒同步機制比較有什麼優勢呢?它們相同點都是為了解決多執行緒中變數訪問衝突的問題。

在同步機制中,通過鎖機制來保證變數在同一個時間點只能被一個執行緒所持有。這個時候,該變數在多個執行緒間是共享的。使用同步機制,要求程式縝密的分析什麼時候對變數進行讀寫,什麼時候要鎖定物件,什麼時候需要釋放鎖等一系列問題,讓我們的程式變得複雜。

而ThreadLocal則從另一個方面解決多執行緒變數併發訪問。ThreadLocal會為每一個執行緒提供一個獨立的變數副本,從而隔離了多個執行緒對資料的訪問衝突。因為每一個執行緒都擁有自己的變數副本,從而也就沒有必要對該變數進行同步了。ThreadLocal提供了執行緒安全的共享物件,在編寫多執行緒程式碼時,可以把不安全的變數封裝進ThreadLocal。

概括起來說,對於多執行緒資源共享的問題,同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。前者僅提供一份變數,讓不同的執行緒排隊訪問,而後者為每一個執行緒都提供了一份變數,因此可以同時訪問而互不影響。

Spring使用ThreadLocal解決執行緒安全問題我們知道在一般情況下,只有無狀態的Bean才可以在多執行緒環境下共享,在Spring中,絕大部分Bean都可以宣告為singleton作用域。就是因為Spring對一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非執行緒安全狀態採用ThreadLocal進行處理,讓它們也成為執行緒安全的狀態,因為有狀態的Bean就可以在多執行緒中共享了。

一般的Web應用劃分為展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過介面向上層開放功能呼叫。在一般情況下,從接收請求到返回響應所經過的所有程式呼叫都同屬於一個執行緒,如圖
這裡寫圖片描述

同一執行緒貫通三層這樣你就可以根據需要,將一些非執行緒安全的變數以ThreadLocal存放,在同一次請求響應的呼叫執行緒中,所有關聯的物件引用到的都是同一個變數。

下面的例項能夠體現Spring對有狀態Bean的改造思路:
TestDao:非執行緒安全

package com.tngtech.thread;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

public class TestDao {
    private Connection conn;// ①一個非執行緒安全的變數

    public void addTopic() throws SQLException {
        Statement stat = conn.createStatement();// ②引用非執行緒安全變數
        // …
    }
}

由於①處的conn是成員變數,因為addTopic()方法是非執行緒安全的,必須在使用時建立一個新TopicDao例項(非singleton)。下面使用ThreadLocal對conn這個非執行緒安全的“狀態”進行改造:
TestDao:執行緒安全

package com.tngtech.thread;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

public class TestDaoNew {
    // ①使用ThreadLocal儲存Connection變數
    private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();

    public static Connection getConnection() {
        // ②如果connThreadLocal沒有本執行緒對應的Connection建立一個新的Connection,
        // 並將其儲存到執行緒本地變數中。
        if (connThreadLocal.get() == null) {
            Connection conn = getConnection();
            connThreadLocal.set(conn);
            return conn;
        } else {
            return connThreadLocal.get();// ③直接返回執行緒本地變數
        }
    }

    public void addTopic() throws SQLException {
        // ④從ThreadLocal中獲取執行緒對應的Connection
        Statement stat = getConnection().createStatement();
    }
}

不同的執行緒在使用TopicDao時,先判斷connThreadLocal.get()是否是null,如果是null,則說明當前執行緒還沒有對應的Connection物件,這時建立一個Connection物件並新增到本地執行緒變數中;如果不為null,則說明當前的執行緒已經擁有了Connection物件,直接使用就可以了。這樣,就保證了不同的執行緒使用執行緒相關的Connection,而不會使用其它執行緒的Connection。因此,這個TopicDao就可以做到singleton共享了。
  當然,這個例子本身很粗糙,將Connection的ThreadLocal直接放在DAO只能做到本DAO的多個方法共享Connection時不發生執行緒安全問題,但無法和其它DAO共用同一個Connection,要做到同一事務多DAO共享同一Connection,必須在一個共同的外部類使用ThreadLocal儲存Connection。

package com.tngtech.thread;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConnectionManager {

    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
        @Override
        protected Connection initialValue() {
            Connection conn = null;
            try {
                conn = DriverManager.getConnection(
                        "jdbc:mysql://localhost:3306/test", "username",
                        "password");
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return conn;
        }
    };

    public static Connection getConnection() {
        return connectionHolder.get();
    }

    public static void setConnection(Connection conn) {
        connectionHolder.set(conn);
    }
}

4.ThreadLocal具體實現

那麼到底ThreadLocal類是如何實現這種“為每個執行緒提供不同的變數拷貝”的呢?先來看一下ThreadLocal的set()方法的原始碼是如何實現的:

 /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

在這個方法內部我們看到,首先通過getMap(Thread t)方法獲取一個和當前執行緒相關的ThreadLocalMap,然後將變數的值設定到這個ThreadLocalMap物件中,當然如果獲取到的ThreadLocalMap物件為空,就通過createMap方法建立。

執行緒隔離的祕密,就在於ThreadLocalMap這個類。ThreadLocalMap是ThreadLocal類的一個靜態內部類,它實現了鍵值對的設定和獲取(對比Map物件來理解),每個執行緒中都有一個獨立的ThreadLocalMap副本,它所儲存的值,只能被當前執行緒讀取和修改。ThreadLocal類通過操作每一個執行緒特有的ThreadLocalMap副本,從而實現了變數訪問在不同執行緒中的隔離。因為每個執行緒的變數都是自己特有的,完全不會有併發錯誤。還有一點就是,ThreadLocalMap儲存的鍵值對中的鍵是this物件指向的ThreadLocal物件,而值就是你所設定的物件了。

為了加深理解,我們接著看上面程式碼中出現的getMap和createMap方法的實現:

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     * @param map the map to store.
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

接下來再看一下ThreadLocal類中的get()方法:

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

再來看setInitialValue()方法:

 /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    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;
    }

獲取和當前執行緒繫結的值時,ThreadLocalMap物件是以this指向的ThreadLocal物件為鍵進行查詢的,這當然和前面set()方法的程式碼是相呼應的。

進一步地,我們可以建立不同的ThreadLocal例項來實現多個變數在不同執行緒間的訪問隔離,為什麼可以這麼做?因為不同的ThreadLocal物件作為不同鍵,當然也可以線上程的ThreadLocalMap物件中設定不同的值了。通過ThreadLocal物件,在多執行緒中共享一個值和多個值的區別,就像你在一個HashMap物件中儲存一個鍵值對和多個鍵值對一樣,僅此而已。

5.總結

ThreadLocal是解決執行緒安全問題一個很好的思路,它通過為每個執行緒提供一個獨立的變數副本解決了變數併發訪問的衝突問題。在很多情況下,ThreadLocal比直接使用synchronized同步機制解決執行緒安全問題更簡單,更方便,且結果程式擁有更高的併發性。