當兩條線程同時訪問一個類的時候,可能會帶來一些問題。並發線程重入可能會帶來內存洩漏、程序不可控等等。不管是線程間的通訊還是線程共享數據都需要使用Java的鎖機制控制並發代碼產生的問題。本篇總結主要著名Java的鎖機制,闡述多線程下如何使用鎖機制進行並發線程溝通。
先看下下面兩個代碼,查看異常內容。
異常1:單例模式
1 package com.scl.thread; 2 3 public class SingletonException 4 { 5 public static void main(String[] args) 6 { 7 // 開啟十條線程進行分別測試輸出類的hashCode,測試是否申請到同一個類 8 for (int i = 0; i < 10; i++) 9 { 10 new Thread(new Runnable() 11 { 12 @Override 13 public void run() 14 { 15 try 16 { 17 Thread.sleep(100); 18 } 19 catch (InterruptedException e) 20 { 21 e.printStackTrace(); 22 } 23 System.out.println(Thread.currentThread().getName() + " " + MySingle.getInstance().hashCode()); 24 } 25 }).start(); 26 } 27 } 28 } 29 30 class MySingle 31 { 32 private static MySingle mySingle = null; 33 34 private MySingle() 35 { 36 } 37 38 public static MySingle getInstance() 39 { 40 if (mySingle == null) { mySingle = new MySingle(); } 41 return mySingle; 42 } 43 }view code
運行結果如下:
由上述可見,Thread-7與其他結果不一致,證明了在多線程並發的情況下這種單例寫法存在問題,問題就在第40行。多個線程同時進入了空值判斷,線程創建了新的類。
異常2:線程重入,引發程序錯誤
現在想模擬國企生產規則,每個月生產100件產品,然後當月消費20件,依次更替。模擬該工廠全年的生產與銷售
備注:舉這個實例是為後面的信號量和生產者消費者問題做鋪墊。可以另外舉例,如開辟十條線程,每條線程內的任務就是進行1-10的累加,每條線程輸出的結果不一定是55(線程重入導致)
1 package com.scl.thread; 2 3 //每次生產100件產品,每次消費20件產品,生產消費更替12輪 4 public class ThreadCommunicateCopy 5 { 6 public static void main(String[] args) 7 { 8 final FactoryCopy factory = new FactoryCopy(); 9 new Thread(new Runnable() 10 { 11 12 @Override 13 public void run() 14 { 15 try 16 { 17 Thread.sleep(2000); 18 } 19 catch (InterruptedException e) 20 { 21 e.printStackTrace(); 22 } 23 24 for (int i = 1; i <= 12; i++) 25 { 26 factory.createProduct(i); 27 } 28 29 } 30 }).start(); 31 32 new Thread(new Runnable() 33 { 34 35 @Override 36 public void run() 37 { 38 try 39 { 40 Thread.sleep(2000); 41 } 42 catch (InterruptedException e) 43 { 44 e.printStackTrace(); 45 } 46 47 for (int i = 1; i <= 12; i++) 48 { 49 factory.sellProduct(i); 50 } 51 52 } 53 }).start(); 54 55 } 56 } 57 58 class FactoryCopy 59 { 60 //生產產品 61 public void createProduct(int i) 62 { 63 64 for (int j = 1; j <= 100; j++) 65 { 66 System.out.println("第" + i + "輪生產,產出" + j + "件"); 67 } 68 } 69 //銷售產品 70 public void sellProduct(int i) 71 { 72 for (int j = 1; j <= 20; j++) 73 { 74 System.out.println("第" + i + "輪銷售,銷售" + j + "件"); 75 } 76 77 } 78 }View Code
結果如下:
該結果不能把銷售線程和生產線程的代碼分隔開,如果需要分隔開。可以使用Java的鎖機制。下面總結下如何處理以上兩個問題。
使用多線程無非是期望程序能夠更快地完成任務,這樣並發編程就必須完成兩件事情:線程同步及線程通信。
線程同步指的是:控制不同線程發生的先後順序。
線程通信指的是:不同線程之間如何共享數據。
Java線程的內存模型:每個線程擁有自己的棧,堆內存共享 [來源:Java並發編程藝術 ],如下圖所示。 鎖是線程間內存和信息溝通的載體,了解線程間通信會對線程鎖有個比較深入的了解。後面也會詳細總結Java是如何根據鎖的信息進行兩條線程之間的通信。
Java語音設計和數據庫一樣,同樣存在著代碼鎖.實現Java代碼鎖比較簡單,一般使用兩個關鍵字對代碼進行線程鎖定。最常用的就是volatile和synchronized兩個
synchronized關鍵字修飾的代碼相當於數據庫上的互斥鎖。確保多個線程在同一時刻只能由一個線程處於方法或同步塊中,確保線程對變量訪問的可見和排它,獲得鎖的對象在代碼結束後,會對鎖進行釋放。
synchronzied使用方法有兩個:①加在方法上面鎖定方法,②定義synchronized塊。
模擬生產銷售循環,可以通過synchronized關鍵字控制線程同步。代碼如下:
1 package com.scl.thread; 2 3 //每次生產100件產品,每次消費20件產品,生產消費更替10輪 4 public class ThreadCommunicate 5 { 6 public static void main(String[] args) 7 { 8 final FactoryCopy factory = new FactoryCopy(); 9 new Thread(new Runnable() 10 { 11 12 @Override 13 public void run() 14 { 15 try 16 { 17 Thread.sleep(2000); 18 } 19 catch (InterruptedException e) 20 { 21 e.printStackTrace(); 22 } 23 24 for (int i = 1; i <= 12; i++) 25 { 26 factory.createProduct(i); 27 } 28 29 } 30 }).start(); 31 32 new Thread(new Runnable() 33 { 34 35 @Override 36 public void run() 37 { 38 try 39 { 40 Thread.sleep(2000); 41 } 42 catch (InterruptedException e) 43 { 44 e.printStackTrace(); 45 } 46 47 for (int i = 1; i <= 12; i++) 48 { 49 factory.sellProduct(i); 50 } 51 52 } 53 }).start(); 54 55 } 56 } 57 58 class Factory 59 { 60 private boolean isCreate = true; 61 62 public synchronized void createProduct(int i) 63 { 64 while (!isCreate) 65 { 66 try 67 { 68 this.wait(); 69 } 70 catch (InterruptedException e) 71 { 72 e.printStackTrace(); 73 } 74 } 75 76 for (int j = 1; j <= 100; j++) 77 { 78 System.out.println("第" + i + "輪生產,產出" + j + "件"); 79 } 80 isCreate = false; 81 this.notify(); 82 } 83 84 public synchronized void sellProduct(int i) 85 { 86 while (isCreate) 87 { 88 try 89 { 90 this.wait(); 91 } 92 catch (InterruptedException e) 93 { 94 e.printStackTrace(); 95 } 96 } 97 for (int j = 1; j <= 20; j++) 98 { 99 System.out.println("第" + i + "輪銷售,銷售" + j + "件"); 100 } 101 isCreate = true; 102 this.notify(); 103 } 104 }View Code
上述代碼通過synchronized關鍵字控制生產及銷售方法每次只能1條線程進入。代碼中使用了isCreate標志位控制生產及銷售的順序。
備注:默認的使用synchronized修飾方法, 關鍵字會以當前實例對象作為鎖對象,對線程進行鎖定。
單例模式的修改可以在getInstance方式中添加synchronized關鍵字進行約束,即可。
wait方法和notify方法將在第三篇線程總結中講解。
volatile關鍵字主要用來修飾變量,關鍵字不像synchronized一樣,能夠塊狀地對代碼進行鎖定。該關鍵字可以看做對修飾的變量進行了讀或寫的同步操作。
如以下代碼:
1 package com.scl.thread; 2 3 public class NumberRange 4 { 5 private volatile int unSafeNum; 6 7 public int getUnSafeNum() 8 { 9 return unSafeNum; 10 } 11 12 public void setUnSafeNum(int unSafeNum) 13 { 14 this.unSafeNum = unSafeNum; 15 } 16 17 public int addVersion() 18 { 19 return this.unSafeNum++; 20 } 21 }View Code
代碼編譯後功能如下:
1 package com.scl.thread; 2 3 public class NumberRange 4 { 5 private volatile int unSafeNum; 6 7 public synchronized int getUnSafeNum() 8 { 9 return unSafeNum; 10 } 11 12 public synchronized void setUnSafeNum(int unSafeNum) 13 { 14 this.unSafeNum = unSafeNum; 15 } 16 17 public int addVersion() 18 { 19 int temp = getUnSafeNum(); 20 temp = temp + 1; 21 setUnSafeNum(temp); 22 return temp; 23 } 24 25 }View Code
由此可見,使用volatile變量進行自增或自減操作的時候,變量進行temp= temp+1這一步時,多條線程同時可能同時操作這一句代碼,導致內容出差。線程代碼內的原子性被破壞了。
以上是對Java鎖機制的總結,如有問題,煩請指出糾正。代碼及例子很大一部分參考了《Java 並發編程藝術》[方騰飛 著] PDF+源碼下載見 http://www.linuxidc.com/Linux/2016-07/133404.htm