日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

java线程安全总结

發布時間:2023/12/19 编程问答 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java线程安全总结 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

最近想將java基礎的一些東西都整理整理,寫下來,這是對知識的總結,也是一種樂趣。已經擬好了提綱,大概分為這幾個主題:?java線程安全,java垃圾收集,java并發包詳細介紹,java profile和jvm性能調優?。慢慢寫吧。本人jameswxx原創文章,轉載請注明出處,我費了很多心血,多謝了。關于java線程安全,網上有很多資料,我只想從自己的角度總結對這方面的考慮,有時候寫東西是很痛苦的,知道一些東西,但想用文字說清楚,卻不是那么容易。我認為要認識java線程安全,必須了解兩個主要的點:java的內存模型,java的線程同步機制。特別是內存模型,java的線程同步機制很大程度上都是基于內存模型而設定的。后面我還會寫java并發包的文章,詳細總結如何利用java并發包編寫高效安全的多線程并發程序。暫時寫得比較倉促,后面會慢慢補充完善。

?

淺談java內存模型?
?????? 不同的平臺,內存模型是不一樣的,但是jvm的內存模型規范是統一的。其實java的多線程并發問題最終都會反映在java的內存模型上,所謂線程安全無非是要控制多個線程對某個資源的有序訪問或修改。總結java的內存模型,要解決兩個主要的問題:可見性和有序性。我們都知道計算機有高速緩存的存在,處理器并不是每次處理數據都是取內存的。JVM定義了自己的內存模型,屏蔽了底層平臺內存管理細節,對于java開發人員,要清楚在jvm內存模型的基礎上,如果解決多線程的可見性和有序性。
???????那么,何謂可見性??多個線程之間是不能互相傳遞數據通信的,它們之間的溝通只能通過共享變量來進行。Java內存模型(JMM)規定了jvm有主內存,主內存是多個線程共享的。當new一個對象的時候,也是被分配在主內存中,每個線程都有自己的工作內存,工作內存存儲了主存的某些對象的副本,當然線程的工作內存大小是有限制的。當線程操作某個對象時,執行順序如下:
?(1) 從主存復制變量到當前工作內存 (read and load)
?(2)
執行代碼,改變共享變量值 (use and assign)
?(3)
用工作內存數據刷新主存相關內容 (store and write)

JVM規范定義了線程對主存的操作指令:read,load,use,assign,store,write。當一個共享變量在多個線程的工作內存中都有副本時,如果一個線程修改了這個共享變量,那么其他線程應該能夠看到這個被修改后的值,這就是多線程的可見性問題。
????????那么,什么是有序性呢??線程在引用變量時不能直接從主內存中引用,如果線程工作內存中沒有該變量,則會從主內存中拷貝一個副本到工作內存中,這個過程為read-load,完成后線程會引用該副本。當同一線程再度引用該字段時,有可能重新從主存中獲取變量副本(read-load-use),也有可能直接引用原來的副本(use),也就是說 read,load,use順序可以由JVM實現系統決定。
? ????? 線程不能直接為主存中中字段賦值,它會將值指定給工作內存中的變量副本(assign),完成后這個變量副本會同步到主存儲區(store-write),至于何時同步過去,根據JVM實現系統決定.有該字段,則會從主內存中將該字段賦值到工作內存中,這個過程為read-load,完成后線程會引用該變量副本,當同一線程多次重復對字段賦值時,比如:

Java代碼?

1.?for(int?i=0;i<10;i++)??

2.??a++;??

?


線程有可能只對工作內存中的副本進行賦值,只到最后一次賦值后才同步到主存儲區,所以assign,store,weite順序可以由JVM實現系統決定。假設有一個共享變量x,線程a執行x=x+1。從上面的描述中可以知道x=x+1并不是一個原子操作,它的執行過程如下:
1 從主存中讀取變量x副本到工作內存
2
x1
3
x1后的值寫回主?存
如果另外一個線程b執行x=x-1,執行過程如下:
1 從主存中讀取變量x副本到工作內存
2
x1
3
x1后的值寫回主存?
那么顯然,最終的x的值是不可靠的。假設x現在為10,線程a加1,線程b減1,從表面上看,似乎最終x還是為10,但是多線程情況下會有這種情況發生:
1:線程a從主存讀取x副本到工作內存,工作內存中x值為10
2
:線程b從主存讀取x副本到工作內存,工作內存中x值為10
3
:線程a將工作內存中x1,工作內存中x值為11
4
:線程ax提交主存中,主存中x11
5
:線程b將工作內存中x值減1,工作內存中x值為9
6
:線程bx提交到中主存中,主存中x9?
同樣,x有可能為11,如果x是一個銀行賬戶,線程a存款,線程b扣款,顯然這樣是有嚴重問題的,要解決這個問題,必須保證線程a和線程b是有序執行的,并且每個線程執行的加1或減1是一個原子操作。看看下面代碼:

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.}??

?


第一次執行結果為10200,第二次執行結果為1060,每次執行的結果都是不確定的,因為線程的執行順序是不可預見的。這是java同步產生的根源,synchronized關鍵字保證了多個線程對于同步塊是互斥的,synchronized作為一種同步手段,解決java多線程的執行有序性和內存可見性,而volatile關鍵字之解決多線程的內存可見性問題。后面將會詳細介紹。

?


synchronized關鍵字?
??????? 上面說了,java用synchronized關鍵字做為多線程并發環境的執行有序性的保證手段之一。當一段代碼會修改共享變量,這一段代碼成為互斥區或臨界區,為了保證共享變量的正確性,synchronized標示了臨界區。典型的用法如下:

Java代碼?

1.?synchronized(鎖){??

2.??????臨界區代碼??

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.?}??

?


剛才不是說了synchronized的用法是這樣的嗎:

Java代碼?

1.?synchronized(鎖){??

2.?臨界區代碼??

3.?}??

?


那么對于publicsynchronized void add(int num)這種情況,意味著什么呢?其實這種情況,鎖就是這個方法所在的對象。同理,如果方法是public? static synchronized voidadd(int num),那么鎖就是這個方法所在的class。
??????? 理論上,每個對象都可以做為鎖,但一個對象做為鎖時,應該被多個線程共享,這樣才顯得有意義,在并發環境下,一個沒有共享的對象作為鎖是沒有意義的。假如有這樣的代碼:

Java代碼?

1.?public?class?ThreadTest{??

2.???public?void?test(){??

3.??????Object?lock=new?Object();??

4.??????synchronized?(lock){??

5.?????????//do?something??

6.??????}??

7.???}??

8.?}??

?


lock變量作為一個鎖存在根本沒有意義,因為它根本不是共享對象,每個線程進來都會執行Object lock=new Object();每個線程都有自己的lock,根本不存在鎖競爭。
??????? 每個鎖對象都有兩個隊列,一個是就緒隊列,一個是阻塞隊列,就緒隊列存儲了將要獲得鎖的線程,阻塞隊列存儲了被阻塞的線程,當一個被線程被喚醒(notify)后,才會進入到就緒隊列,等待cpu的調度。當一開始線程a第一次執行account.add方法時,jvm會檢查鎖對象account的就緒隊列是否已經有線程在等待,如果有則表明account的鎖已經被占用了,由于是第一次運行,account的就緒隊列為空,所以線程a獲得了鎖,執行account.add方法。如果恰好在這個時候,線程b要執行account.withdraw方法,因為線程a已經獲得了鎖還沒有釋放,所以線程b要進入account的就緒隊列,等到得到鎖后才可以執行。
一個線程執行臨界區代碼過程如下:
1 獲得同步鎖
2 清空工作內存
3 從主存拷貝變量副本到工作內存
4 對這些變量計算
5 將變量從工作內存寫回到主存
6 釋放鎖
可見,synchronized既保證了多線程的并發有序性,又保證了多線程的內存可見性。


生產者/消費者模式?
??????? 生產者/消費者模式其實是一種很經典的線程同步模型,很多時候,并不是光保證多個線程對某共享資源操作的互斥性就夠了,往往多個線程之間都是有協作的。
??????? 假設有這樣一種情況,有一個桌子,桌子上面有一個盤子,盤子里只能放一顆雞蛋,A專門往盤子里放雞蛋,如果盤子里有雞蛋,則一直等到盤子里沒雞蛋,B專門從盤子里拿雞蛋,如果盤子里沒雞蛋,則等待直到盤子里有雞蛋。其實盤子就是一個互斥區,每次往盤子放雞蛋應該都是互斥的,A的等待其實就是主動放棄鎖,B等待時還要提醒A放雞蛋。
如何讓線程主動釋放鎖
很簡單,調用鎖的wait()方法就好。wait方法是從Object來的,所以任意對象都有這個方法。看這個代碼片段:

Java代碼?

1.?Object?lock=new?Object();//聲明了一個對象作為鎖??

2.????synchronized?(lock)?{??

3.????????balance?=?balance?-?num;??

4.????????//這里放棄了同步鎖,好不容易得到,又放棄了??

5.????????lock.wait();??

6.?}??

?


如果一個線程獲得了鎖lock,進入了同步塊,執行lock.wait(),那么這個線程會進入到lock的阻塞隊列。如果調用lock.notify()則會通知阻塞隊列的某個線程進入就緒隊列。
聲明一個盤子,只能放一個雞蛋

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();//清空盤子??

14.????notify();//喚醒阻塞隊列的某線程到就緒隊列??

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);//往盤子里放雞蛋??

26.????notify();//喚醒阻塞隊列的某線程到就緒隊列??

27.??}??

28.}??

?


聲明一個Plate對象為plate,被線程A和線程B共享,A專門放雞蛋,B專門拿雞蛋。假設
1 開始,A調用plate.putEgg方法,此時eggs.size()0,因此順利將雞蛋放到盤子,還執行了notify()方法,喚醒鎖的阻塞隊列的線程,此時阻塞隊列還沒有線程。
2
又有一個A線程對象調用plate.putEgg方法,此時eggs.size()不為0,調用wait()方法,自己進入了鎖對象的阻塞隊列。
3
此時,來了一個B線程對象,調用plate.getEgg方法,eggs.size()不為0,順利的拿到了一個雞蛋,還執行了notify()方法,喚醒鎖的阻塞隊列的線程,此時阻塞隊列有一個A線程對象,喚醒后,它進入到就緒隊列,就緒隊列也就它一個,因此馬上得到鎖,開始往盤子里放雞蛋,此時盤子是空的,因此放雞蛋成功。
4
假設接著來了線程A,就重復2;假設來料線程B,就重復3?
整個過程都保證了放雞蛋,拿雞蛋,放雞蛋,拿雞蛋。

?


volatile關鍵字?
?????? volatile是java提供的一種同步手段,只不過它是輕量級的同步,為什么這么說,因為volatile只能保證多線程的內存可見性,不能保證多線程的執行有序性。而最徹底的同步要保證有序性和可見性,例如synchronized。任何被volatile修飾的變量,都不拷貝副本到工作內存,任何修改都及時寫在主存。因此對于Valatile修飾的變量的修改,所有線程馬上就能看到,但是volatile不能保證對變量的修改是有序的。什么意思呢?假如有這樣的代碼:

Java代碼?

1.?public?class?VolatileTest{??

2.???public?volatile?int?a;??

3.???public?void?add(int?count){??

4.????????a=a+count;??

5.???}??

6.?}??

?


??????? 當一個VolatileTest對象被多個線程共享,a的值不一定是正確的,因為a=a+count包含了好幾步操作,而此時多個線程的執行是無序的,因為沒有任何機制來保證多個線程的執行有序性和原子性。volatile存在的意義是,任何線程對a的修改,都會馬上被其他線程讀取到,因為直接操作主存,沒有線程對工作內存和主存的同步。所以,volatile的使用場景是有限的,在有限的一些情形下可以使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時滿足下面兩個條件:
1)對變量的寫操作不依賴于當前值。
2)
該變量沒有包含在具有其他變量的不變式中?
volatile只保證了可見性,所以Volatile適合直接賦值的場景,如

Java代碼?

1.?public?class?VolatileTest{??

2.???public?volatile?int?a;??

3.???public?void?setA(int?a){??

4.???????this.a=a;??

5.???}??

6.?}??

?


在沒有volatile聲明時,多線程環境下,a的最終值不一定是正確的,因為this.a=a;涉及到給a賦值和將a同步回主存的步驟,這個順序可能被打亂。如果用volatile聲明了,讀取主存副本到工作內存和同步a到主存的步驟,相當于是一個原子操作。所以簡單來說,volatile適合這種場景:一個變量被多個線程共享,線程直接給這個變量賦值。這是一種很簡單的同步場景,這時候使用volatile的開銷將會非常小。

---------來自?jameswxx 樵夫后院的博客

總結

以上是生活随笔為你收集整理的java线程安全总结的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。