本文目錄一覽:
求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,完成後線程會引用該變數副本,當同一線程多次重複對欄位賦值時,比如:
for(int i=0;i10;i++)
a++;
線程有可能只對工作內存中的副本進行賦值,只到最後一次賦值後才同步到主存儲區,所以assign,store,weite順序可以由JVM實現系統決定。假設有一個共享變數x,線程a執行x=x+1。從上面的描述中可以知道x=x+1並不是一個原子操作,它的執行過程如下:
1 從主存中讀取變數x副本到工作內存
2 給x加1
3 將x加1後的值寫回主 存
如果另外一個線程b執行x=x-1,執行過程如下:
1 從主存中讀取變數x副本到工作內存
2 給x減1
3 將x減1後的值寫回主存
那麼顯然,最終的x的值是不可靠的。假設x現在為10,線程a加1,線程b減1,從表面上看,似乎最終x還是為10,但是多線程情況下會有這種情況發生:
1:線程a從主存讀取x副本到工作內存,工作內存中x值為10
2:線程b從主存讀取x副本到工作內存,工作內存中x值為10
3:線程a將工作內存中x加1,工作內存中x值為11
4:線程a將x提交主存中,主存中x為11
5:線程b將工作內存中x值減1,工作內存中x值為9
6:線程b將x提交到中主存中,主存中x為9
同樣,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.}
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.}
public class Account {
private int balance;
public Account(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void add(int num) {
balance = balance + num;
}
public void withdraw(int num) {
balance = balance – num;
}
public static void main(String[] args) throws InterruptedException {
Account account = new Account(1000);
Thread a = new Thread(new AddThread(account, 20), “add”);
Thread b = new Thread(new WithdrawThread(account, 20), “withdraw”);
a.start();
b.start();
a.join();
b.join();
System.out.println(account.getBalance());
}
static class AddThread implements Runnable {
Account account;
int amount;
public AddThread(Account account, int amount) {
this.account = account;
this.amount = amount;
}
public void run() {
for (int i = 0; i 200000; i++) {
account.add(amount);
}
}
}
static class WithdrawThread implements Runnable {
Account account;
int amount;
public WithdrawThread(Account account, int amount) {
this.account = account;
this.amount = amount;
}
public void run() {
for (int i = 0; i 100000; i++) {
account.withdraw(amount);
}
}
}
}
第一次執行結果為10200,第二次執行結果為1060,每次執行的結果都是不確定的,因為線程的執行順序是不可預見的。這是java同步產生的根源,synchronized關鍵字保證了多個線程對於同步塊是互斥的,synchronized作為一種同步手段,解決java多線程的執行有序性和內存可見性,而volatile關鍵字只解決多線程的內存可見性問題。後面將會詳細介紹。
synchronized關鍵字
上面說了,java用synchronized關鍵字做為多線程並發環境的執行有序性的保證手段之一。當一段代碼會修改共享變數,這一段代碼成為互斥區或臨界區,為了保證共享變數的正確性,synchronized標示了臨界區。典型的用法如下:
synchronized(鎖){
臨界區代碼
}
為了保證銀行賬戶的安全,可以操作賬戶的方法如下:
剛才不是說了synchronized的用法是這樣的嗎:
synchronized(鎖) {
臨界區代碼
}
那麼對於public synchronized void add(int num)這種情況,意味著什麼呢?其實這種情況,鎖就是這個方法所在的對象。同理,如果方法是public static synchronized void add(int num),那麼鎖就是這個方法所在的class。
理論上,每個對象都可以做為鎖,但一個對象做為鎖時,應該被多個線程共享,這樣才顯得有意義,在並發環境下,一個沒有共享的對象作為鎖是沒有意義的。假如有這樣的代碼:
Java代碼
1.public class ThreadTest{
2. public void test(){
1. Object lock=new Object();
2. synchronized (lock){
3. //do something
4. }
5. }
6.}
Java代碼
1.public class ThreadTest{
2. public void test(){
3. Object lock=new Object();
4. synchronized (lock){
5. //do something
6. }
7. }
8.}
public class ThreadTest{
public void test(){
Object lock=new Object();
synchronized (lock){
//do something
}
}
}
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.}
Java代碼
1.Object lock=new Object();//聲明了一個對象作為鎖
2. synchronized (lock) {
3. balance = balance – num;
4. //這裡放棄了同步鎖,好不容易得到,又放棄了
5. lock.wait();
6.}
Object lock=new Object();//聲明了一個對象作為鎖
synchronized (lock) {
balance = balance – num;
//這裡放棄了同步鎖,好不容易得到,又放棄了
lock.wait();
}
如果一個線程獲得了鎖lock,進入了同步塊,執行lock.wait(),那麼這個線程會進入到lock的阻塞隊列。如果調用 lock.notify()則會通知阻塞隊列的某個線程進入就緒隊列。
聲明一個盤子,只能放一個雞蛋
Java代碼
1.package com.jameswxx.synctest;
2.public class Plate{
1. ListObject eggs=new ArrayListObject();
2. public synchronized Object getEgg(){
3. if(eggs.size()==0){
4. try{
5. wait();
6. }catch(InterruptedException e){
7. }
8. }
9.
10. Object egg=eggs.get(0);
11. eggs.clear();//清空盤子
12. notify();//喚醒阻塞隊列的某線程到就緒隊列
13. return egg;
14.}
15.
16. public synchronized void putEgg(Object egg){
17. If(eggs.size()0){
18. try{
19. wait();
20. }catch(InterruptedException e){
21. }
22. }
23. eggs.add(egg);//往盤子里放雞蛋
24. notify();//喚醒阻塞隊列的某線程到就緒隊列
25. }
26.}
Java代碼
1.package com.jameswxx.synctest;
2.public class Plate{
3. ListObject eggs=new ArrayListObject();
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.}
package com.jameswxx.synctest;
public class Plate{
ListObject eggs=new ArrayListObject();
public synchronized Object getEgg(){
if(eggs.size()==0){
try{
wait();
}catch(InterruptedException e){
}
}
Object egg=eggs.get(0);
eggs.clear();//清空盤子
notify();//喚醒阻塞隊列的某線程到就緒隊列
return egg;
}
public synchronized void putEgg(Object egg){
If(eggs.size()0){
try{
wait();
}catch(InterruptedException e){
}
}
eggs.add(egg);//往盤子里放雞蛋
notify();//喚醒阻塞隊列的某線程到就緒隊列
}
}
聲明一個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.}
Java代碼
1.public class VolatileTest{
2. public volatile int a;
3. public void add(int count){
4. a=a+count;
5. }
6.}
public class VolatileTest{
public volatile int a;
public void add(int count){
a=a+count;
}
}
當一個VolatileTest對象被多個線程共享,a的值不一定是正確的,因為a=a+count包含了好幾步操作,而此時多個線程的執行是無序的,因為沒有任何機制來保證多個線程的執行有序性和原子性。volatile存在的意義是,任何線程對a的修改,都會馬上被其他線程讀取到,因為直接操作主存,沒有線程對工作內存和主存的同步。所以,volatile的使用場景是有限的,在有限的一些情形下可以使用 volatile 變數替代鎖。要使 volatile 變數提供理想的線程安全,必須同時滿足下面兩個條件:
1)對變數的寫操作不依賴於當前值。
2)該變數沒有包含在具有其他變數的不變式中
Volatile只保證了可見性,所以Volatile適合直接賦值的場景,如
Java代碼
1.public class VolatileTest{
2. public volatile int a;
1. public void setA(int a){
2. this.a=a;
3. }
4.}
Java代碼
1.public class VolatileTest{
2. public volatile int a;
3. public void setA(int a){
4. this.a=a;
5. }
6.}
public class VolatileTest{
public volatile int a;
public void setA(int a){
this.a=a;
}
}
在沒有volatile聲明時,多線程環境下,a的最終值不一定是正確的,因為this.a=a;涉及到給a賦值和將a同步回主存的步驟,這個順序可能被打亂。如果用volatile聲明了,讀取主存副本到工作內存和同步a到主存的步驟,相當於是一個原子操作。所以簡單來說,volatile適合這種場景:一個變數被多個線程共享,線程直接給這個變數賦值。這是一種很簡單的同步場景,這時候使用volatile的開銷將會非常小。
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.}
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.}
Java內存模型FAQ 什麼是內存模型
內存模型描述的是程序中各變數(實例域、靜態域和數組元素)之間的關係,以及在實際計算機系統中將變數存儲到內存和從內存取出變數這樣的低層細節。對象最終存儲在內存中,但編譯器、運行庫、處理器或緩存可以有特權定時地在變數的指定內存位置存入或取出變數值。例如,編譯器為了優化一個循環索引變數,可能會選擇把它存儲到一個寄存器中,或者緩存會延遲到一個更適合的時間,才把一個新的變數值存入主存。所有的這些優化是為了幫助實現更高的性能,通常這對於用戶來說是透明的,但是對多處理系統來說,這些複雜的事情可能有時會完全顯現出來。
JMM 允許編譯器和緩存以數據在處理器特定的緩存(或寄存器)和主存之間移動的次序擁有重要的特權,除非程序員已經使用 synchronized 或 final 明確地請求了某些可見性保證。這意味著在缺乏同步的情況下,從不同的線程角度來看,內存的操作是以不同的次序發生的。
與之相對應地,像 C 和 C++ 這些語言就沒有顯示的內存模型 —— 但 C 語言程序繼承了執行程序處理器的內存模型(儘管一個給定體系結構的編譯器可能知道有關底層處理器的內存模型的一些情況,並且保持一致性的一部分責任也落到了該編譯器的頭上)。這意味著並發的 C 語言程序可以在一個,而不能在另一個,處理器體系結構上正確地運行。雖然一開始 JMM 會有些混亂,但這有個很大的好處 —— 根據 JMM 而被正確同步的程序能正確地運行在任何支持 Java 的平台上。
北大青鳥設計培訓:java多線程的內存模型?
硬體的內存模型物理機並發處理的方案對於jvm的內存模型實現,也有很大的參考作用,畢竟jvm也是在硬體層上來做事情,底層架構也決定了上層的建築建模方式。
計算機並發並非只是多個處理器都參與進來計算就可以了,會牽扯到一些列硬體的問題,最直接的就是要和內存做交互。
但計算機的存儲設備與處理器的預算速度相差太大,完全不能滿足處理器的處理速度,怎麼辦,這就是後續加入的一層讀寫速度接近處理器運算速度的高速緩存來作為處理器和內存之間的緩衝。
高速緩存一邊把使用的數據,從內存複製搬入,方便處理器快速運算,一邊把運算後的數據,再同步到主內存中,如此處理器就無需等待了。
高速緩存雖然解決了處理器和內存的矛盾,但也為計算機帶來了另一個問題:緩存一致性。
特別是當多個處理器都涉及到同一塊主內存區域的時候,將可能會導致各自的緩存數據不一致。
那麼出現不一致情況的時候,以誰的為準?為了解決這個問題,處理器和內存之間的讀寫的時候需要遵循一定的協議來操作,這類協議有:MSI、MESI、MOSI、Synapse、Firefly以及DragonProtocol等。
這就是上圖中處理器、高速緩存、以及內存之間的處理方式。
另外除了高速緩存之外,為了充分利用處理器,處理器還會把輸入的指令碼進行亂序執行優化,只要保證輸出一致,輸入的信息可以亂序執行重組,所以程序中的語句計算順序和輸入代碼的順序並非一致。
JVM內存模型上面我們了解了硬體的內存模型,以此為借鑒,我們看看jvm的內存模型。
jvm定義的一套java內存模型為了能夠跨平台達到一致的內存訪問效果,從而屏蔽掉了各種硬體和操作系統的內存訪問差異。
這點和c和c++並不一樣,C和C++會直接使用物理硬體和操作系統的內存模型來處理,所以在各個平台上會有差異,這一點java不會。
java的內存模型規定了所有的變數都存儲在主內存中,java課程發現每個線程擁有自己的工作內存,工作內存保存了該線程使用到的變數的主內存拷貝,線程對變數所有操作,讀取,賦值,都必須在工作內存中進行,不能直接寫主內存變數,線程間變數值的傳遞均需要主內存來完成。
原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/286798.html