JVM線程資源同步及交互機制

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的動作分為以下五步:

  1. 裝載i
    向main memory發(fā)起read i指令。
    當read i執(zhí)行完畢咧织,線程會將i的值從main memory復制到working memory中嗓袱。
  2. 讀取i
    從main memory中讀取i。
  3. 進行i+1操作
    由線程完成习绢。
  4. 存儲i
    將i+1的值賦值給i渠抹,然后存儲到working memory中。
  5. 寫入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í)行寫操作,一般都需要考慮線程同步街立,否則就可能影響線程安全舶衬。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市赎离,隨后出現(xiàn)的幾起案子逛犹,更是在濱河造成了極大的恐慌,老刑警劉巖梁剔,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件虽画,死亡現(xiàn)場離奇詭異,居然都是意外死亡荣病,警方通過查閱死者的電腦和手機码撰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來个盆,“玉大人脖岛,你說我怎么就攤上這事〖樟粒” “怎么了柴梆?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長终惑。 經(jīng)常有香客問我绍在,道長,這世上最難降的妖魔是什么雹有? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任偿渡,我火速辦了婚禮,結(jié)果婚禮上霸奕,老公的妹妹穿的比我還像新娘溜宽。我一直安慰自己,他們只是感情好质帅,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布坑质。 她就那樣靜靜地躺著,像睡著了一般临梗。 火紅的嫁衣襯著肌膚如雪涡扼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天盟庞,我揣著相機與錄音吃沪,去河邊找鬼。 笑死什猖,一個胖子當著我的面吹牛票彪,可吹牛的內(nèi)容都是我干的红淡。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼降铸,長吁一口氣:“原來是場噩夢啊……” “哼在旱!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起推掸,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤桶蝎,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后谅畅,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體登渣,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年毡泻,在試婚紗的時候發(fā)現(xiàn)自己被綠了胜茧。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡仇味,死狀恐怖呻顽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情丹墨,我是刑警寧澤廊遍,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站带到,受9級特大地震影響昧碉,放射性物質(zhì)發(fā)生泄漏英染。R本人自食惡果不足惜揽惹,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望四康。 院中可真熱鬧搪搏,春花似錦、人聲如沸闪金。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽哎垦。三九已至囱嫩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間漏设,已是汗流浹背墨闲。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留郑口,地道東北人鸳碧。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓盾鳞,卻偏偏與公主長得像,于是被迫代替她去往敵國和親瞻离。 傳聞我的和親對象是個殘疾皇子腾仅,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容