Java程序採用多線程來支持大量併發。尤其是在多核或者多CPU系統中,多線程執行程序帶來的最明顯的問題是線程之間同步管理的資源競爭以及線程交互的問題。
JVM的線程實現及其調度方式(搶佔、協作)取決於操作系統,不在本文贅述。
線程資源同步機制
有如下程序:
int i=0;
public int getNextId(){
return i++;
}
以上程序在JVM中執行的步驟如下:
(1) JVM在堆中給i分配一個內存存儲場所(main memory),並存儲其值爲1。
(2) 線程啓動後,自動分配一片操作數棧(working memory),當線程執行到return i++時,JVM的動作分爲以下五步:
- 裝載i
向main memory發起read i指令。
當read i執行完畢,線程會將i的值從main memory複製到working memory中。 - 讀取i
從main memory中讀取i。 - 進行i+1操作
由線程完成。 - 存儲i
將i+1的值賦值給i,然後存儲到working memory中。 - 寫入i
將i的值回寫到main memory中。
從以上步驟中不難發現,從working memory到main memory的存取是需要時間的(反過來也是);i++是由多個操作完成的(讀取 自增 存儲),如果是多線程,就會出現髒讀、誤讀等現象。
對於多線程的髒讀、誤讀等現象,JVM把對於working memory的操作分爲了use、assign、load、store、lock和unlock。
對於main memory操作分爲了read、write、lock和unlock。
不難理解lock和unlock就是鎖的使用。對此,JVM提供了synchronized關鍵字、volatile關鍵字和lock/unlock機制。
採用synchronized改造如下:
public synchronized int getNextId(){
return i++;
}
對於lock/unlock機制,可能發生死鎖,可以看看如下代碼:
private Object a=new Object();
private Object b=new Object();
public void callAB(){
synchronized(a){
synchronized(b){
//do something
}
}
}
public void executeAB(){
synchronized(b){
synchronized(a){
//do something
}
}
}
volatile機制有所不同,它僅用於控制線程中對象的可見性,並不能保證在此對象上操作的原子性。就像上面的i++操作,即使把i定義爲volatile也是沒用的。但對於定義爲volatile的變量,線程不會將其從main memory 複製到work memory中,而是直接在main memory上操作,它的代價雖然低,但是不能保證原子性。
可見性,是指線程之間的可見性,一個線程修改的狀態對另一個線程是可見的。也就是一個線程修改的結果。另一個線程馬上就能看到。 用volatile修飾的變量,就會具有可見性。volatile修飾的變量不允許線程內部緩存和重排序,即直接修改內存。所以對其他線程是可見的。
volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。
線程交互機制
線程交互最典型的就是連接池。連接池中通常會有get和return兩種方法。return的時候會講連接返回到緩存列表中,並將連接數+1。而get方法在判斷可使用連接數爲0後,就進入一個等待狀態,當有連接返回到連接池時,應該通知get方法不需要等待了。JVM通過wait/notify/notifyAll來支持這種等待和喚醒的需求。
典型的代碼如下:
public Connection get(){
synchronized(this){
if(free>0){
free--;
return cacheConnections.poll();
}
else{
this.wait();
}
}
}
public void close(Connection conn){
synchronized(this){
free++;
cacheConnection.offer(conn);
this.notifyAll();
}
}
在Sun JDK中,object.wait()還有可能被虛假喚醒(也就是說原本只能喚醒一個人,現在喚醒了兩個人,都先後拿到了鎖,然而池中只有一根冰棒,),因此需要在此確認狀態是否變更了,這種做法稱爲double check。(具體可以看看懶漢單例模式或者生產者消費者模式)
單例模式:
public static Singleton2 getInstance(){
if(instance == null) {
synchronized (Singleton2.class){
instance = new Singleton2();
}
}
return instance;
}
變更爲:
public static Singleton2 getInstance(){
if(instance == null) {
synchronized (Singleton2.class){
if (instance == null){
instance = new Singleton2();
}
}
}
return instance;
}
這裏解釋一下爲什麼synchronized爲什麼不寫成這樣:
public static Singleton2 getInstance(){
synchronized (Singleton2.class){
if (instance == null){
instance = new Singleton2();
}
}
return instance;
}
其實這是一個效率問題:是由於如果加在synchronized下面的話,這其實與方法加鎖沒什麼區別。每次運行進來,線程都會阻塞。而double check保證了在創建了新實例的時候,不會阻塞。
SpringMVC怎麼保障線程安全的?
對此,我們先來看三個栗子:
@Controller
public void MainController{
private int index=0;
private static int STATICINDEX=0;
@RequestMapping(xxx)
public void getIndex(){
....
System.out.println("index:"+
(index++)+",static index"+(STATICINDEX++));
....
}
}
結果:
index:0,static index:0
index:1,static index:1
index:2,static index:2
我們在類加上@Scope(value="prototype")
後,其輸出結果爲:
結果:
index:0,static index:0
index:0,static index:1
index:0,static index:2
如果將@Scope(value="prototype")
改爲@Scope(value="singleton")
,那麼,輸出結果爲:
結果:
index:0,static index:0
index:1,static index:1
index:2,static index:2
做一個總結:springMVC默認爲單例模式(包括controller/service/dao)。由上面三個例子可以知道:
- 單例模式的意思就是隻有一個實例。單例模式確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例。這個類稱爲單例類。當多用戶同時請求一個服務時,容器會給每一個請求分配一個線程,這是多個線程會併發執行該請求多對應的業務邏輯(成員方法),此時就要注意了,如果該處理邏輯中有對該單列狀態的修改(體現爲該單列的成員屬性),則必須考慮線程同步問題。
- 儘量不要在controller裏面去定義屬性,如果在特殊情況需要定義屬性的時候,那麼就在類上面加上註解@Scope("prototype")改爲多例的模式。
與SpringMVC不同的是,struts是基於類的屬性進行發的,定義屬性可以整個類通用,所以默認是多例,不然多線程訪問肯定是共用類裏面的屬性值的,肯定是不安全的。所以對此,又產生如下的問題:
- SpringMVC是單例的,高併發情況下,如何保證性能的?
首先在大家的思考中,肯定有影響的,你想想,單例顧名思義:一個個排隊過... 高訪問量的時候,你能想象服務器的壓力了... 而且用戶體驗也不怎麼好,等待太久~
實質上這種理解是錯誤的,Java裏有個API叫做ThreadLocal,spring單例模式下用它來切換不同線程之間的參數。用ThreadLocal是爲了保證線程安全,實際上ThreadLoacal的key就是當前線程的Thread實例。單例模式下,spring把每個線程可能存在線程安全問題的參數值放進了ThreadLocal。這樣雖然是一個實例在操作,但是不同線程下的數據互相之間都是隔離的,因爲運行時創建和銷燬的bean大大減少了,所以大多數場景下這種方式對內存資源的消耗較少,而且併發越高優勢越明顯。
- ThreadLocal和線程同步機制相比有什麼優勢呢?
ThreadLocal和線程同步機制都是爲了解決多線程中相同變量的訪問衝突問題。
在同步機制中,通過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機制要求程序慎密地分析什麼時候對變量進行讀寫,什麼時候需要鎖定某個對象,什麼時候釋放對象鎖等繁雜的問題,程序設計和編寫難度相對較大。
而ThreadLocal則從另一個角度來解決多線程的併發訪問。ThreadLocal會爲每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問衝突。因爲每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的變量封裝進ThreadLocal。
- 哪些因素造成了線程不安全?
如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。 或者說:一個類或者程序所提供的接口對於線程來說是原子操作或者多個線程之間的切換不會導致該接口的執行結果存在二義性,也就是說我們不用考慮同步的問題。線程安全問題都是由全局變量及靜態變量引起的。
若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則就可能影響線程安全。