Java程序采用多線程來支持大量并發(fā)客峭。尤其是在多核或者多CPU系統(tǒng)中,多線程執(zhí)行程序帶來的最明顯的問題是線程之間同步管理的資源競爭以及線程交互的問題腋腮。
JVM的線程實現(xiàn)及其調(diào)度方式(搶占、協(xié)作)取決于操作系統(tǒng)县踢,不在本文贅述。
線程資源同步機制
有如下程序:
int i=0;
public int getNextId(){
return i++;
}
以上程序在JVM中執(zhí)行的步驟如下:
(1) JVM在堆中給i分配一個內(nèi)存存儲場所(main memory)伟件,并存儲其值為1硼啤。
(2) 線程啟動后,自動分配一片操作數(shù)棧(working memory)斧账,當線程執(zhí)行到return i++時谴返,JVM的動作分為以下五步:
- 裝載i
向main memory發(fā)起read i指令。
當read i執(zhí)行完畢咧织,線程會將i的值從main memory復制到working memory中嗓袱。 - 讀取i
從main memory中讀取i。 - 進行i+1操作
由線程完成习绢。 - 存儲i
將i+1的值賦值給i渠抹,然后存儲到working memory中。 - 寫入i
將i的值回寫到main memory中闪萄。
從以上步驟中不難發(fā)現(xiàn)逼肯,從working memory到main memory的存取是需要時間的(反過來也是);i++是由多個操作完成的(讀取 自增 存儲)桃煎,如果是多線程篮幢,就會出現(xiàn)臟讀、誤讀等現(xiàn)象为迈。
對于多線程的臟讀三椿、誤讀等現(xiàn)象,JVM把對于working memory的操作分為了use葫辐、assign搜锰、load、store耿战、lock和unlock蛋叼。
對于main memory操作分為了read、write剂陡、lock和unlock狈涮。
不難理解lock和unlock就是鎖的使用。對此鸭栖,JVM提供了synchronized關(guān)鍵字歌馍、volatile關(guān)鍵字和lock/unlock機制。
采用synchronized改造如下:
public synchronized int getNextId(){
return i++;
}
對于lock/unlock機制晕鹊,可能發(fā)生死鎖松却,可以看看如下代碼:
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上操作,它的代價雖然低窟社,但是不能保證原子性。
可見性绪钥,是指線程之間的可見性灿里,一個線程修改的狀態(tài)對另一個線程是可見的。也就是一個線程修改的結(jié)果程腹。另一個線程馬上就能看到匣吊。 用volatile修飾的變量,就會具有可見性寸潦。volatile修飾的變量不允許線程內(nèi)部緩存和重排序色鸳,即直接修改內(nèi)存。所以對其他線程是可見的见转。
volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方命雀,因此在讀取volatile類型的變量時總會返回最新寫入的值。
線程交互機制
線程交互最典型的就是連接池斩箫。連接池中通常會有g(shù)et和return兩種方法吏砂。return的時候會講連接返回到緩存列表中,并將連接數(shù)+1乘客。而get方法在判斷可使用連接數(shù)為0后狐血,就進入一個等待狀態(tài),當有連接返回到連接池時易核,應該通知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()還有可能被虛假喚醒(也就是說原本只能喚醒一個人缀匕,現(xiàn)在喚醒了兩個人,都先后拿到了鎖碰逸,然而池中只有一根冰棒弦追,),因此需要在此確認狀態(tài)是否變更了花竞,這種做法稱為double check劲件。(具體可以看看懶漢單例模式或者生產(chǎn)者消費者模式)
單例模式:
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下面的話掸哑,這其實與方法加鎖沒什么區(qū)別。每次運行進來零远,線程都會阻塞苗分。而double check保證了在創(chuàng)建了新實例的時候,不會阻塞牵辣。
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++));
....
}
}
結(jié)果:
index:0,static index:0
index:1,static index:1
index:2,static index:2
我們在類加上@Scope(value="prototype")
后纬向,其輸出結(jié)果為:
結(jié)果:
index:0,static index:0
index:0,static index:1
index:0,static index:2
如果將@Scope(value="prototype")
改為@Scope(value="singleton")
择浊,那么,輸出結(jié)果為:
結(jié)果:
index:0,static index:0
index:1,static index:1
index:2,static index:2
做一個總結(jié):springMVC默認為單例模式(包括controller/service/dao)逾条。由上面三個例子可以知道:
- 單例模式的意思就是只有一個實例琢岩。單例模式確保某一個類只有一個實例,而且自行實例化并向整個系統(tǒng)提供這個實例师脂。這個類稱為單例類担孔。當多用戶同時請求一個服務時,容器會給每一個請求分配一個線程吃警,這是多個線程會并發(fā)執(zhí)行該請求多對應的業(yè)務邏輯(成員方法)糕篇,此時就要注意了,如果該處理邏輯中有對該單列狀態(tài)的修改(體現(xiàn)為該單列的成員屬性)酌心,則必須考慮線程同步問題拌消。
- 盡量不要在controller里面去定義屬性,如果在特殊情況需要定義屬性的時候安券,那么就在類上面加上注解@Scope("prototype")改為多例的模式拼坎。
與SpringMVC不同的是,struts是基于類的屬性進行發(fā)的完疫,定義屬性可以整個類通用泰鸡,所以默認是多例,不然多線程訪問肯定是共用類里面的屬性值的壳鹤,肯定是不安全的盛龄。所以對此,又產(chǎn)生如下的問題:
- SpringMVC是單例的芳誓,高并發(fā)情況下余舶,如何保證性能的?
首先在大家的思考中锹淌,肯定有影響的匿值,你想想,單例顧名思義:一個個排隊過... 高訪問量的時候赂摆,你能想象服務器的壓力了... 而且用戶體驗也不怎么好挟憔,等待太久~
實質(zhì)上這種理解是錯誤的钟些,Java里有個API叫做ThreadLocal,spring單例模式下用它來切換不同線程之間的參數(shù)绊谭。用ThreadLocal是為了保證線程安全政恍,實際上ThreadLoacal的key就是當前線程的Thread實例。單例模式下达传,spring把每個線程可能存在線程安全問題的參數(shù)值放進了ThreadLocal篙耗。這樣雖然是一個實例在操作,但是不同線程下的數(shù)據(jù)互相之間都是隔離的宪赶,因為運行時創(chuàng)建和銷毀的bean大大減少了宗弯,所以大多數(shù)場景下這種方式對內(nèi)存資源的消耗較少,而且并發(fā)越高優(yōu)勢越明顯搂妻。
- ThreadLocal和線程同步機制相比有什么優(yōu)勢呢蒙保?
ThreadLocal和線程同步機制都是為了解決多線程中相同變量的訪問沖突問題。
在同步機制中叽讳,通過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的坟募,使用同步機制要求程序慎密地分析什么時候?qū)ψ兞窟M行讀寫岛蚤,什么時候需要鎖定某個對象,什么時候釋放對象鎖等繁雜的問題懈糯,程序設計和編寫難度相對較大涤妒。
而ThreadLocal則從另一個角度來解決多線程的并發(fā)訪問。ThreadLocal會為每一個線程提供一個獨立的變量副本赚哗,從而隔離了多個線程對數(shù)據(jù)的訪問沖突她紫。因為每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了屿储。ThreadLocal提供了線程安全的共享對象贿讹,在編寫多線程代碼時,可以把不安全的變量封裝進ThreadLocal够掠。
- 哪些因素造成了線程不安全民褂?
如果每次運行結(jié)果和單線程運行的結(jié)果是一樣的,而且其他的變量的值也和預期的是一樣的疯潭,就是線程安全的赊堪。 或者說:一個類或者程序所提供的接口對于線程來說是原子操作或者多個線程之間的切換不會導致該接口的執(zhí)行結(jié)果存在二義性,也就是說我們不用考慮同步的問題。線程安全問題都是由全局變量及靜態(tài)變量引起的竖哩。
若每個線程中對全局變量哭廉、靜態(tài)變量只有讀操作,而無寫操作相叁,一般來說遵绰,這個全局變量是線程安全的辽幌;若有多個線程同時執(zhí)行寫操作,一般都需要考慮線程同步街立,否則就可能影響線程安全舶衬。