天天看點

Spring如何處理線程并發

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

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

雖然模闆類通過資源池擷取資料連接配接或會話,但資源池本身解決的是資料連接配接或會話的緩存問題,并非資料連接配接或會話的線程安全問題。

按照傳統經驗,如果某個對象是非線程安全的,在多線程環境下,對對象的通路必須采用synchronized進行線程同步。但Spring的DAO模闆類并未采用線程同步機制,因為線程同步限制了并發通路,會帶來很大的性能損失。

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

ThreadLocal在Spring中發揮着重要的作用,在管理request作用域的Bean、事務管理、任務排程、AOP等子產品都出現了它們的身影,起着舉足輕重的作用。要想了解Spring事務管理的底層技術,ThreadLocal是必須攻克的山頭堡壘。

ThreadLocal是什麼

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

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

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

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

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

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

ThreadLocal的接口方法

ThreadLocal類接口很簡單,隻有4個方法,我們先來了解一下:

void set(Object value)

設定目前線程的線程局部變量的值。

public Object get()

該方法傳回目前線程所對應的線程局部變量。

public void remove()

将目前線程局部變量的值删除,目的是為了減少記憶體的占用,該方法是JDK5.0新增的方法。需要指出的是,當線程結束後,對應該線程的局部變量将自動被垃圾回收,是以顯式調用該方法清除線程的局部變量并不是必須的操作,但它可以加快記憶體回收的速度。

protected Object initialValue()

傳回該線程局部變量的初始值,該方法是一個protected的方法,顯然是為了讓子類覆寫而設計的。這個方法是一個延遲調用方法,線上程第1次調用get()或set(Object)時才執行,并且僅執行1次。ThreadLocal中的預設實作直接傳回一個null。

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

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

// 代碼清單1 SimpleThreadLocal
class SimpleThreadLocal {
    private MapvalueMap = Collections.synchronizedMap(new HashMap());
    public voidset(Object newValue) {
       valueMap.put(Thread.currentThread(), newValue);//①鍵為線程對象,值為本線程的變量副本
    }
    publicObject 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 voidremove() {
       valueMap.remove(Thread.currentThread());
    }
    publicObject initialValue() {
       return null;
    }
}
           

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

一個TheadLocal執行個體

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

package threadLocalDemo;
public class SequenceNumber {
    //①通過匿名内部類覆寫ThreadLocal的initialValue()方法,指定初始值
    privatestatic ThreadLocal<Integer> seqNum =new ThreadLocal<Integer>() {
       public Integer initialValue() {
           return 0;
       }
    };
    //②擷取下一個序列值
    public intgetNextNum() {
       seqNum.set(seqNum.get() + 1);
       return seqNum.get();
    }
    publicstatic 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();
    }
    privatestatic 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對象,需要強制類型轉換。但JDK5.0通過泛型很好的解決了這個問題,在一定程度地簡化ThreadLocal的使用,代碼清單 9 2就使用了JDK5.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變量
    privatestatic ThreadLocal<Connection>connThreadLocal = newThreadLocal<Connection>();
    publicstatic Connection getConnection() {
       // ②如果connThreadLocal沒有本線程對應的Connection建立一個新的Connection,
       // 并将其儲存到線程本地變量中。
       if (connThreadLocal.get() == null) {
           Connection conn = getConnection();
           connThreadLocal.set(conn);
           return conn;
       } else {
           return connThreadLocal.get();
           // ③直接傳回線程本地變量
       }
    }
    public voidaddTopic() {
       // ④從ThreadLocal中擷取線程對應的Connection
       try {
           Statement stat = getConnection().createStatement();
       } catch (SQLException e) {
           e.printStackTrace();
       }
    }
}
           

繼續閱讀