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 @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的鎖機制。下面總結下如何處理以上兩個問題。
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 並發編程藝術》[方騰飛 著]