Java多線程(二) 多線程的鎖機制,java多線程鎖機制
當兩條線程同時訪問一個類的時候,可能會帶來一些問題。並發線程重入可能會帶來記憶體流失、程式不可控等等。不管是線程間的通訊還是線程共用資料都需要使用Java的鎖機制控制並發代碼產生的問題。本篇總結主要著名Java的鎖機制,闡述多線程下如何使用鎖機制進行並發線程溝通。
1、並發下的程式異常
先看下下面兩個代碼,查看異常內容。
異常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 @Override13 public void run()14 {15 try16 {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 MySingle31 {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 @Override13 public void run()14 {15 try16 {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 @Override36 public void run()37 {38 try39 {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 FactoryCopy59 {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的鎖機制。下面總結下如何處理以上兩個問題。
2、使用多線程編程目的及一些Java多線程的基本知識
使用多線程無非是期望程式能夠更快地完成任務,這樣並發編程就必須完成兩件事情:線程同步及線程通訊。
線程同步指的是:控制不同線程發生的先後順序。
線程通訊指的是:不同線程之間如何共用資料。
Java線程的記憶體模型:每個線程擁有自己的棧,堆記憶體共用 [來源:Java並發編程藝術 ],如所示。 鎖是線程間記憶體和資訊溝通的載體,瞭解線程間通訊會對線程鎖有個比較深入的瞭解。後面也會詳細總結Java是如何根據鎖的資訊進行兩條線程之間的通訊。
2、使用Java的鎖機制
Java語音設計和資料庫一樣,同樣存在著代碼鎖.實現Java代碼鎖比較簡單,一般使用兩個關鍵字對代碼進行線程鎖定。最常用的就是volatile和synchronized兩個
2.1 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方法將在第三篇線程總結中講解。
2.2 volatile
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 並發編程藝術》[方騰飛 著]