java线程安全总结
最近想將java基礎(chǔ)的一些東西都整理整理,寫(xiě)下來(lái),這是對(duì)知識(shí)的總結(jié),也是一種樂(lè)趣。已經(jīng)擬好了提綱,大概分為這幾個(gè)主題:?java線程安全,java垃圾收集,java并發(fā)包詳細(xì)介紹,java profile和jvm性能調(diào)優(yōu)?。慢慢寫(xiě)吧。本人jameswxx原創(chuàng)文章,轉(zhuǎn)載請(qǐng)注明出處,我費(fèi)了很多心血,多謝了。關(guān)于java線程安全,網(wǎng)上有很多資料,我只想從自己的角度總結(jié)對(duì)這方面的考慮,有時(shí)候?qū)憱|西是很痛苦的,知道一些東西,但想用文字說(shuō)清楚,卻不是那么容易。我認(rèn)為要認(rèn)識(shí)java線程安全,必須了解兩個(gè)主要的點(diǎn):java的內(nèi)存模型,java的線程同步機(jī)制。特別是內(nèi)存模型,java的線程同步機(jī)制很大程度上都是基于內(nèi)存模型而設(shè)定的。后面我還會(huì)寫(xiě)java并發(fā)包的文章,詳細(xì)總結(jié)如何利用java并發(fā)包編寫(xiě)高效安全的多線程并發(fā)程序。暫時(shí)寫(xiě)得比較倉(cāng)促,后面會(huì)慢慢補(bǔ)充完善。
?
淺談java內(nèi)存模型?
?????? 不同的平臺(tái),內(nèi)存模型是不一樣的,但是jvm的內(nèi)存模型規(guī)范是統(tǒng)一的。其實(shí)java的多線程并發(fā)問(wèn)題最終都會(huì)反映在java的內(nèi)存模型上,所謂線程安全無(wú)非是要控制多個(gè)線程對(duì)某個(gè)資源的有序訪問(wèn)或修改。總結(jié)java的內(nèi)存模型,要解決兩個(gè)主要的問(wèn)題:可見(jiàn)性和有序性。我們都知道計(jì)算機(jī)有高速緩存的存在,處理器并不是每次處理數(shù)據(jù)都是取內(nèi)存的。JVM定義了自己的內(nèi)存模型,屏蔽了底層平臺(tái)內(nèi)存管理細(xì)節(jié),對(duì)于java開(kāi)發(fā)人員,要清楚在jvm內(nèi)存模型的基礎(chǔ)上,如果解決多線程的可見(jiàn)性和有序性。
???????那么,何謂可見(jiàn)性??多個(gè)線程之間是不能互相傳遞數(shù)據(jù)通信的,它們之間的溝通只能通過(guò)共享變量來(lái)進(jìn)行。Java內(nèi)存模型(JMM)規(guī)定了jvm有主內(nèi)存,主內(nèi)存是多個(gè)線程共享的。當(dāng)new一個(gè)對(duì)象的時(shí)候,也是被分配在主內(nèi)存中,每個(gè)線程都有自己的工作內(nèi)存,工作內(nèi)存存儲(chǔ)了主存的某些對(duì)象的副本,當(dāng)然線程的工作內(nèi)存大小是有限制的。當(dāng)線程操作某個(gè)對(duì)象時(shí),執(zhí)行順序如下:
?(1) 從主存復(fù)制變量到當(dāng)前工作內(nèi)存 (read and load)
?(2) 執(zhí)行代碼,改變共享變量值 (use and assign)
?(3) 用工作內(nèi)存數(shù)據(jù)刷新主存相關(guān)內(nèi)容 (store and write)
JVM規(guī)范定義了線程對(duì)主存的操作指令:read,load,use,assign,store,write。當(dāng)一個(gè)共享變量在多個(gè)線程的工作內(nèi)存中都有副本時(shí),如果一個(gè)線程修改了這個(gè)共享變量,那么其他線程應(yīng)該能夠看到這個(gè)被修改后的值,這就是多線程的可見(jiàn)性問(wèn)題。
????????那么,什么是有序性呢??線程在引用變量時(shí)不能直接從主內(nèi)存中引用,如果線程工作內(nèi)存中沒(méi)有該變量,則會(huì)從主內(nèi)存中拷貝一個(gè)副本到工作內(nèi)存中,這個(gè)過(guò)程為read-load,完成后線程會(huì)引用該副本。當(dāng)同一線程再度引用該字段時(shí),有可能重新從主存中獲取變量副本(read-load-use),也有可能直接引用原來(lái)的副本(use),也就是說(shuō) read,load,use順序可以由JVM實(shí)現(xiàn)系統(tǒng)決定。
? ????? 線程不能直接為主存中中字段賦值,它會(huì)將值指定給工作內(nèi)存中的變量副本(assign),完成后這個(gè)變量副本會(huì)同步到主存儲(chǔ)區(qū)(store-write),至于何時(shí)同步過(guò)去,根據(jù)JVM實(shí)現(xiàn)系統(tǒng)決定.有該字段,則會(huì)從主內(nèi)存中將該字段賦值到工作內(nèi)存中,這個(gè)過(guò)程為read-load,完成后線程會(huì)引用該變量副本,當(dāng)同一線程多次重復(fù)對(duì)字段賦值時(shí),比如:
Java代碼?
1.?for(int?i=0;i<10;i++)??
2.??a++;??
?
線程有可能只對(duì)工作內(nèi)存中的副本進(jìn)行賦值,只到最后一次賦值后才同步到主存儲(chǔ)區(qū),所以assign,store,weite順序可以由JVM實(shí)現(xiàn)系統(tǒng)決定。假設(shè)有一個(gè)共享變量x,線程a執(zhí)行x=x+1。從上面的描述中可以知道x=x+1并不是一個(gè)原子操作,它的執(zhí)行過(guò)程如下:
1 從主存中讀取變量x副本到工作內(nèi)存
2 給x加1
3 將x加1后的值寫(xiě)回主?存
如果另外一個(gè)線程b執(zhí)行x=x-1,執(zhí)行過(guò)程如下:
1 從主存中讀取變量x副本到工作內(nèi)存
2 給x減1
3 將x減1后的值寫(xiě)回主存?
那么顯然,最終的x的值是不可靠的。假設(shè)x現(xiàn)在為10,線程a加1,線程b減1,從表面上看,似乎最終x還是為10,但是多線程情況下會(huì)有這種情況發(fā)生:
1:線程a從主存讀取x副本到工作內(nèi)存,工作內(nèi)存中x值為10
2:線程b從主存讀取x副本到工作內(nèi)存,工作內(nèi)存中x值為10
3:線程a將工作內(nèi)存中x加1,工作內(nèi)存中x值為11
4:線程a將x提交主存中,主存中x為11
5:線程b將工作內(nèi)存中x值減1,工作內(nèi)存中x值為9
6:線程b將x提交到中主存中,主存中x為9?
同樣,x有可能為11,如果x是一個(gè)銀行賬戶,線程a存款,線程b扣款,顯然這樣是有嚴(yán)重問(wèn)題的,要解決這個(gè)問(wèn)題,必須保證線程a和線程b是有序執(zhí)行的,并且每個(gè)線程執(zhí)行的加1或減1是一個(gè)原子操作。看看下面代碼:
Java代碼?
1.?public?class?Account?{??
2.???
3.?????private?int?balance;??
4.???
5.?????public?Account(int?balance)?{??
6.?????????this.balance?=?balance;??
7.?????}??
8.???
9.?????public?int?getBalance()?{??
10.????????return?balance;??
11.????}??
12.??
13.????public?void?add(int?num)?{??
14.????????balance?=?balance?+?num;??
15.????}??
16.??
17.????public?void?withdraw(int?num)?{??
18.????????balance?=?balance?-?num;??
19.????}??
20.??
21.????public?static?void?main(String[]?args)?throws?InterruptedException?{??
22.????????Account?account?=?new?Account(1000);??
23.????????Thread?a?=?new?Thread(new?AddThread(account,?20),?"add");??
24.????????Thread?b?=?new?Thread(new?WithdrawThread(account,?20),?"withdraw");??
25.????????a.start();??
26.????????b.start();??
27.????????a.join();??
28.????????b.join();??
29.????????System.out.println(account.getBalance());??
30.????}??
31.??
32.????static?class?AddThread?implements?Runnable?{??
33.????????Account?account;??
34.????????int?????amount;??
35.??
36.????????public?AddThread(Account?account,?int?amount)?{??
37.????????????this.account?=?account;??
38.????????????this.amount?=?amount;??
39.????????}??
40.??
41.????????public?void?run()?{??
42.????????????for?(int?i?=?0;?i?<?200000;?i++)?{??
43.????????????????account.add(amount);??
44.????????????}??
45.????????}??
46.????}??
47.??
48.????static?class?WithdrawThread?implements?Runnable?{??
49.????????Account?account;??
50.????????int?????amount;??
51.??
52.????????public?WithdrawThread(Account?account,?int?amount)?{??
53.????????????this.account?=?account;??
54.????????????this.amount?=?amount;??
55.????????}??
56.??
57.????????public?void?run()?{??
58.????????????for?(int?i?=?0;?i?<?100000;?i++)?{??
59.????????????????account.withdraw(amount);??
60.????????????}??
61.????????}??
62.????}??
63.}??
?
第一次執(zhí)行結(jié)果為10200,第二次執(zhí)行結(jié)果為1060,每次執(zhí)行的結(jié)果都是不確定的,因?yàn)榫€程的執(zhí)行順序是不可預(yù)見(jiàn)的。這是java同步產(chǎn)生的根源,synchronized關(guān)鍵字保證了多個(gè)線程對(duì)于同步塊是互斥的,synchronized作為一種同步手段,解決java多線程的執(zhí)行有序性和內(nèi)存可見(jiàn)性,而volatile關(guān)鍵字之解決多線程的內(nèi)存可見(jiàn)性問(wèn)題。后面將會(huì)詳細(xì)介紹。
?
synchronized關(guān)鍵字?
??????? 上面說(shuō)了,java用synchronized關(guān)鍵字做為多線程并發(fā)環(huán)境的執(zhí)行有序性的保證手段之一。當(dāng)一段代碼會(huì)修改共享變量,這一段代碼成為互斥區(qū)或臨界區(qū),為了保證共享變量的正確性,synchronized標(biāo)示了臨界區(qū)。典型的用法如下:
Java代碼?
1.?synchronized(鎖){??
2.??????臨界區(qū)代碼??
3.?}???
?
為了保證銀行賬戶的安全,可以操作賬戶的方法如下:
Java代碼?
1.?public?synchronized?void?add(int?num)?{??
2.??????balance?=?balance?+?num;??
3.?}??
4.?public?synchronized?void?withdraw(int?num)?{??
5.??????balance?=?balance?-?num;??
6.?}??
?
剛才不是說(shuō)了synchronized的用法是這樣的嗎:
Java代碼?
1.?synchronized(鎖){??
2.?臨界區(qū)代碼??
3.?}??
?
那么對(duì)于publicsynchronized void add(int num)這種情況,意味著什么呢?其實(shí)這種情況,鎖就是這個(gè)方法所在的對(duì)象。同理,如果方法是public? static synchronized voidadd(int num),那么鎖就是這個(gè)方法所在的class。
??????? 理論上,每個(gè)對(duì)象都可以做為鎖,但一個(gè)對(duì)象做為鎖時(shí),應(yīng)該被多個(gè)線程共享,這樣才顯得有意義,在并發(fā)環(huán)境下,一個(gè)沒(méi)有共享的對(duì)象作為鎖是沒(méi)有意義的。假如有這樣的代碼:
Java代碼?
1.?public?class?ThreadTest{??
2.???public?void?test(){??
3.??????Object?lock=new?Object();??
4.??????synchronized?(lock){??
5.?????????//do?something??
6.??????}??
7.???}??
8.?}??
?
lock變量作為一個(gè)鎖存在根本沒(méi)有意義,因?yàn)樗静皇枪蚕韺?duì)象,每個(gè)線程進(jìn)來(lái)都會(huì)執(zhí)行Object lock=new Object();每個(gè)線程都有自己的lock,根本不存在鎖競(jìng)爭(zhēng)。
??????? 每個(gè)鎖對(duì)象都有兩個(gè)隊(duì)列,一個(gè)是就緒隊(duì)列,一個(gè)是阻塞隊(duì)列,就緒隊(duì)列存儲(chǔ)了將要獲得鎖的線程,阻塞隊(duì)列存儲(chǔ)了被阻塞的線程,當(dāng)一個(gè)被線程被喚醒(notify)后,才會(huì)進(jìn)入到就緒隊(duì)列,等待cpu的調(diào)度。當(dāng)一開(kāi)始線程a第一次執(zhí)行account.add方法時(shí),jvm會(huì)檢查鎖對(duì)象account的就緒隊(duì)列是否已經(jīng)有線程在等待,如果有則表明account的鎖已經(jīng)被占用了,由于是第一次運(yùn)行,account的就緒隊(duì)列為空,所以線程a獲得了鎖,執(zhí)行account.add方法。如果恰好在這個(gè)時(shí)候,線程b要執(zhí)行account.withdraw方法,因?yàn)榫€程a已經(jīng)獲得了鎖還沒(méi)有釋放,所以線程b要進(jìn)入account的就緒隊(duì)列,等到得到鎖后才可以執(zhí)行。
一個(gè)線程執(zhí)行臨界區(qū)代碼過(guò)程如下:
1 獲得同步鎖
2 清空工作內(nèi)存
3 從主存拷貝變量副本到工作內(nèi)存
4 對(duì)這些變量計(jì)算
5 將變量從工作內(nèi)存寫(xiě)回到主存
6 釋放鎖
可見(jiàn),synchronized既保證了多線程的并發(fā)有序性,又保證了多線程的內(nèi)存可見(jiàn)性。
生產(chǎn)者/消費(fèi)者模式?
??????? 生產(chǎn)者/消費(fèi)者模式其實(shí)是一種很經(jīng)典的線程同步模型,很多時(shí)候,并不是光保證多個(gè)線程對(duì)某共享資源操作的互斥性就夠了,往往多個(gè)線程之間都是有協(xié)作的。
??????? 假設(shè)有這樣一種情況,有一個(gè)桌子,桌子上面有一個(gè)盤(pán)子,盤(pán)子里只能放一顆雞蛋,A專門(mén)往盤(pán)子里放雞蛋,如果盤(pán)子里有雞蛋,則一直等到盤(pán)子里沒(méi)雞蛋,B專門(mén)從盤(pán)子里拿雞蛋,如果盤(pán)子里沒(méi)雞蛋,則等待直到盤(pán)子里有雞蛋。其實(shí)盤(pán)子就是一個(gè)互斥區(qū),每次往盤(pán)子放雞蛋應(yīng)該都是互斥的,A的等待其實(shí)就是主動(dòng)放棄鎖,B等待時(shí)還要提醒A放雞蛋。
如何讓線程主動(dòng)釋放鎖
很簡(jiǎn)單,調(diào)用鎖的wait()方法就好。wait方法是從Object來(lái)的,所以任意對(duì)象都有這個(gè)方法。看這個(gè)代碼片段:
Java代碼?
1.?Object?lock=new?Object();//聲明了一個(gè)對(duì)象作為鎖??
2.????synchronized?(lock)?{??
3.????????balance?=?balance?-?num;??
4.????????//這里放棄了同步鎖,好不容易得到,又放棄了??
5.????????lock.wait();??
6.?}??
?
如果一個(gè)線程獲得了鎖lock,進(jìn)入了同步塊,執(zhí)行l(wèi)ock.wait(),那么這個(gè)線程會(huì)進(jìn)入到lock的阻塞隊(duì)列。如果調(diào)用lock.notify()則會(huì)通知阻塞隊(duì)列的某個(gè)線程進(jìn)入就緒隊(duì)列。
聲明一個(gè)盤(pán)子,只能放一個(gè)雞蛋
Java代碼?
1.?package?com.jameswxx.synctest;??
2.?public?class?Plate{??
3.???List<Object>?eggs=new?ArrayList<Object>();??
4.???public?synchronized??Object?getEgg(){??
5.??????if(eggs.size()==0){??
6.?????????try{??
7.?????????????wait();??
8.?????????}catch(InterruptedException?e){??
9.?????????}??
10.?????}??
11.??
12.????Object?egg=eggs.get(0);??
13.????eggs.clear();//清空盤(pán)子??
14.????notify();//喚醒阻塞隊(duì)列的某線程到就緒隊(duì)列??
15.????return?egg;??
16.}??
17.??
18.?public?synchronized??void?putEgg(Object?egg){??
19.????If(eggs.size()>0){??
20.??????try{??
21.?????????wait();??
22.??????}catch(InterruptedException?e){??
23.??????}??
24.????}??
25.????eggs.add(egg);//往盤(pán)子里放雞蛋??
26.????notify();//喚醒阻塞隊(duì)列的某線程到就緒隊(duì)列??
27.??}??
28.}??
?
聲明一個(gè)Plate對(duì)象為plate,被線程A和線程B共享,A專門(mén)放雞蛋,B專門(mén)拿雞蛋。假設(shè)
1 開(kāi)始,A調(diào)用plate.putEgg方法,此時(shí)eggs.size()為0,因此順利將雞蛋放到盤(pán)子,還執(zhí)行了notify()方法,喚醒鎖的阻塞隊(duì)列的線程,此時(shí)阻塞隊(duì)列還沒(méi)有線程。
2 又有一個(gè)A線程對(duì)象調(diào)用plate.putEgg方法,此時(shí)eggs.size()不為0,調(diào)用wait()方法,自己進(jìn)入了鎖對(duì)象的阻塞隊(duì)列。
3 此時(shí),來(lái)了一個(gè)B線程對(duì)象,調(diào)用plate.getEgg方法,eggs.size()不為0,順利的拿到了一個(gè)雞蛋,還執(zhí)行了notify()方法,喚醒鎖的阻塞隊(duì)列的線程,此時(shí)阻塞隊(duì)列有一個(gè)A線程對(duì)象,喚醒后,它進(jìn)入到就緒隊(duì)列,就緒隊(duì)列也就它一個(gè),因此馬上得到鎖,開(kāi)始往盤(pán)子里放雞蛋,此時(shí)盤(pán)子是空的,因此放雞蛋成功。
4 假設(shè)接著來(lái)了線程A,就重復(fù)2;假設(shè)來(lái)料線程B,就重復(fù)3。?
整個(gè)過(guò)程都保證了放雞蛋,拿雞蛋,放雞蛋,拿雞蛋。
?
volatile關(guān)鍵字?
?????? volatile是java提供的一種同步手段,只不過(guò)它是輕量級(jí)的同步,為什么這么說(shuō),因?yàn)関olatile只能保證多線程的內(nèi)存可見(jiàn)性,不能保證多線程的執(zhí)行有序性。而最徹底的同步要保證有序性和可見(jiàn)性,例如synchronized。任何被volatile修飾的變量,都不拷貝副本到工作內(nèi)存,任何修改都及時(shí)寫(xiě)在主存。因此對(duì)于Valatile修飾的變量的修改,所有線程馬上就能看到,但是volatile不能保證對(duì)變量的修改是有序的。什么意思呢?假如有這樣的代碼:
Java代碼?
1.?public?class?VolatileTest{??
2.???public?volatile?int?a;??
3.???public?void?add(int?count){??
4.????????a=a+count;??
5.???}??
6.?}??
?
??????? 當(dāng)一個(gè)VolatileTest對(duì)象被多個(gè)線程共享,a的值不一定是正確的,因?yàn)閍=a+count包含了好幾步操作,而此時(shí)多個(gè)線程的執(zhí)行是無(wú)序的,因?yàn)闆](méi)有任何機(jī)制來(lái)保證多個(gè)線程的執(zhí)行有序性和原子性。volatile存在的意義是,任何線程對(duì)a的修改,都會(huì)馬上被其他線程讀取到,因?yàn)橹苯硬僮髦鞔?#xff0c;沒(méi)有線程對(duì)工作內(nèi)存和主存的同步。所以,volatile的使用場(chǎng)景是有限的,在有限的一些情形下可以使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時(shí)滿足下面兩個(gè)條件:
1)對(duì)變量的寫(xiě)操作不依賴于當(dāng)前值。
2)該變量沒(méi)有包含在具有其他變量的不變式中?
volatile只保證了可見(jiàn)性,所以Volatile適合直接賦值的場(chǎng)景,如
Java代碼?
1.?public?class?VolatileTest{??
2.???public?volatile?int?a;??
3.???public?void?setA(int?a){??
4.???????this.a=a;??
5.???}??
6.?}??
?
在沒(méi)有volatile聲明時(shí),多線程環(huán)境下,a的最終值不一定是正確的,因?yàn)閠his.a=a;涉及到給a賦值和將a同步回主存的步驟,這個(gè)順序可能被打亂。如果用volatile聲明了,讀取主存副本到工作內(nèi)存和同步a到主存的步驟,相當(dāng)于是一個(gè)原子操作。所以簡(jiǎn)單來(lái)說(shuō),volatile適合這種場(chǎng)景:一個(gè)變量被多個(gè)線程共享,線程直接給這個(gè)變量賦值。這是一種很簡(jiǎn)單的同步場(chǎng)景,這時(shí)候使用volatile的開(kāi)銷將會(huì)非常小。
---------來(lái)自?jameswxx 樵夫后院的博客
總結(jié)
以上是生活随笔為你收集整理的java线程安全总结的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: python 引用(import)文件夹
- 下一篇: 慢慢琢磨JVM