通通透透理解ThreadLocal,實現安全的多線程

來源:互聯網
上載者:User

我們知道Spring通過各種DAO模板類降低了開發人員使用各種資料持久技術的難度。這些模板類都是安全執行緒的,也就是說,多個DAO可以複用同一個模板執行個體而不會發生衝突。

我們使用模板類訪問底層資料,根據持久化技術的不同,模板類需要綁定資料連線或會話的資源。但這些資源本身是非安全執行緒的,也就是說它們不能在同一時刻被多個線程共用。

雖然模板類通過資源集區擷取資料連線或會話,但資源集區本身解決的是資料連線或會話的緩衝問題,並非資料連線或會話的安全執行緒問題。

按照傳統經驗,如果某個對象是非安全執行緒的,在多線程環境下,對對象的訪問必須採用synchronized進行線程同步。但Spring的DAO模板類並未採用線程同步機制,因為線程同步限制了並發訪問,會帶來很大的效能損失。

此外,通過代碼同步解決效能安全問題挑戰性很大,可能會增強好幾倍的實現難度。那模板類究竟仰丈何種魔法神功,可以在無需同步的情況下就化解安全執行緒的難題呢?答案就是ThreadLocal!

ThreadLocal在Spring中發揮著重要的作用,在管理request範圍的Bean、交易管理、任務調度、AOP等模組都出現了它們的身影,起著舉足輕重的作用。要想瞭解Spring交易管理的底層技術,ThreadLocal是必須攻克的山頭堡壘。

ThreadLocal是什麼

早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal為解決多線程程式的並發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多線程程式。

ThreadLocal很容易讓人望文生義,想當然地認為是一個“本地線程”。其實,ThreadLocal並不是一個Thread,而是Thread的局部變數,也許把它命名為ThreadLocalVariable更容易讓人理解一些。

當使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的線程提供獨立的變數副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。

從線程的角度看,目標變數就象是線程的本地變數,這也是類名中“Local”所要表達的意思。

線程局部變數並不是Java的新發明,很多語言(如IBM IBM XL FORTRAN)在文法層面就提供線程局部變數。在Java中沒有提供在語言級支援,而是變相地通過ThreadLocal的類提供支援。

所以,在Java中編寫線程局部變數的代碼相對來說要笨拙一些,因此造成線程局部變數沒有在Java開發人員中得到很好的普及。

ThreadLocal的介面方法

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<T>。API方法
也相應進行了調整,新版本的API方法分別是void set(T value)、T get()以及T initialValue()。

ThreadLocal是如何做到為每一個線程維護變數的副本的呢?其實實現的思路很簡單:在ThreadLocal類中有一個Map,用於儲存每一個線程的變數副本,Map中元素的鍵為線程對象,而值對應線程的變數副本。我們自己就可以提供一個簡單的實現版本:

// 代碼清單1 SimpleThreadLocal

class SimpleThreadLocal {
    private Map valueMap = Collections.synchronizedMap(new HashMap());
    public void set(Object newValue) {
        valueMap.put(Thread.currentThread(), newValue);// ①鍵為線程對象,值為本線程的變數副本
    }
    public Object get() {
        Thread currentThread = Thread.currentThread();
        Object o = valueMap.get(currentThread);// ②返回本線程對應的變數
        if (o == null && !valueMap.containsKey(currentThread)) {// ③如果在Map中不存在,放到Map
            // 中儲存起來。
            o = initialValue();
            valueMap.put(currentThread, o);
        }
        return o;
    }
    public void remove() {
        valueMap.remove(Thread.currentThread());
    }
    public Object initialValue() {
        return null;
    }
}

雖然代碼清單9‑3這個ThreadLocal實現版本顯得比較幼稚,但它和JDK所提供的ThreadLocal類在實現思路上是相近的。

一個TheadLocal執行個體

下面,我們通過一個具體的執行個體瞭解一下ThreadLocal的具體使用方法

package threadLocalDemo;

public class SequenceNumber {
    // ①通過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值
    private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
        public Integer initialValue() {
            return 0;
        }
    };
    // ②擷取下一個序列值
    public int getNextNum() {
        seqNum.set(seqNum.get() + 1);
        return seqNum.get();
    }
    public static void main(String[] args)
    {
        SequenceNumber sn = new SequenceNumber();
        // ③ 3個線程共用sn,各自產生序號
        TestClient t1 = new TestClient(sn);
        TestClient t2 = new TestClient(sn);
        TestClient t3 = new TestClient(sn);
        t1.start();
        t2.start();
        t3.start();
    }
    private static class TestClient extends Thread
    {
        private SequenceNumber sn;

        public TestClient(SequenceNumber sn) {
            this.sn = sn;
        }
        public void run()
        {
            for (int i = 0; i < 3; i++) {
                // ④每個線程打出3個序列值
                System.out.println("thread[" + Thread.currentThread().getName()+"] sn[" + sn.getNextNum() + "]");
            }
        }
    }
}

通常我們通過匿名內部類的方式定義ThreadLocal的子類,提供初始的變數值,如例子中①處所示。TestClient線程產生一組序號,
在③處,我們產生3個TestClient,它們共用同一個SequenceNumber執行個體。運行以上代碼,在控制台上輸出以下的結果:

thread[Thread-2] sn[1]

thread[Thread-0] sn[1]

thread[Thread-1] sn[1]

thread[Thread-2] sn[2]

thread[Thread-0] sn[2]

thread[Thread-1] sn[2]

thread[Thread-2] sn[3]

thread[Thread-0] sn[3]

thread[Thread-1] sn[3]

考察輸出的結果資訊,我們發現每個線程所產生的序號雖然都共用同一個SequenceNumber執行個體,但它們並沒有發生相互幹擾的情況,而是各自產生獨立的序號,這是因為我們通過ThreadLocal為每一個線程提供了單獨的副本。

Thread同步機制的比較

ThreadLocal和線程同步機制相比有什麼優勢呢?ThreadLocal和線程同步機制都是為瞭解決多線程中相同變數的存取違規問題。

在同步機制中,通過對象的鎖機制保證同一時間只有一個線程訪問變數。這時該變數是多個線程共用的,使用同步機制要求程式慎密地分析什麼時候對變數進行讀寫,什麼時候需要鎖定某個對象,什麼時候釋放對象鎖等繁雜的問題,程式設計和編寫難度相對較大。

而ThreadLocal則從另一個角度來解決多線程的並發訪問。ThreadLocal會為每一個線程提供一個獨立的變數副本,從而隔離了多個線
程對資料的存取違規。因為每一個線程都擁有自己的變數副本,從而也就沒有必要對該變數進行同步了。ThreadLocal提供了安全執行緒的共用對象,在編
寫多線程代碼時,可以把不安全的變數封裝進ThreadLocal。

由於ThreadLocal中可以持有任何類型的對象,低版本JDK所提供的get()返回的是Object對象,需要強制類型轉換。但JDK
5.0通過泛型很好的解決了這個問題,在一定程度地簡化ThreadLocal的使用,代碼清單 9 2就使用了JDK
5.0新的ThreadLocal<T>版本。

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

Spring使用ThreadLocal解決安全執行緒問題

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

一般的Web應用劃分為展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過介面向上層開放功能調用。在一般情況下,從接收請求到返迴響應所經過的所有程式調用都同屬於一個線程,9‑2所示:

圖1同一線程貫通三層

這樣你就可以根據需要,將一些非安全執行緒的變數以ThreadLocal存放,在同一次請求響應的調用線程中,所有關聯的對象引用到的都是同一個變數。

下面的執行個體能夠體現Spring對有狀態Bean的改造思路:

代碼清單3 TopicDao:非安全執行緒

public class TopicDao {

private Connection conn;一個非安全執行緒的變數

public void addTopic(){

Statement stat = conn.createStatement();引用非安全執行緒變數

}

}

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

代碼清單4 TopicDao:安全執行緒

package threadLocalDemo;

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

public class SqlConnection {
    // ①使用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() {
        // ④從ThreadLocal中擷取線程對應的Connection
        try {
            Statement stat = getConnection().createStatement();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

不同的線程在使用TopicDao時,先判斷connThreadLocal.get()是否是null,如果是null,則說明當前線程還沒有對
應的Connection對象,這時建立一個Connection對象並添加到本地線程變數中;如果不為null,則說明當前的線程已經擁有了
Connection對象,直接使用就可以了。這樣,就保證了不同的線程使用線程相關的Connection,而不會使用其它線程的
Connection。因此,這個TopicDao就可以做到singleton共用了。

當然,這個例子本身很粗糙,將Connection的ThreadLocal直接放在DAO只能做到本DAO的多個方法共用Connection時
不發生安全執行緒問題,但無法和其它DAO共用同一個Connection,要做到同一事務多DAO共用同一Connection,必須在一個共同的外部類
使用ThreadLocal儲存Connection。但這個執行個體基本上說明了Spring對有狀態類安全執行緒化的解決思路。

小結

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

(轉載至http://www.builder.com.cn/2007/0529/404695.shtml)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.