當兩條線程同時通路一個類的時候,可能會帶來一些問題。并發線程重入可能會帶來記憶體洩漏、程式不可控等等。不管是線程間的通訊還是線程共享資料都需要使用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 }
上述代碼通過synchronized關鍵字控制生産及銷售方法每次隻能1條線程進入。代碼中使用了isCreate标志位控制生産及銷售的順序。
注意:即使代碼不使用isCreate标志位進行控制,代碼隻會出現 :thread-0 生産---thread-0 生産--- thread-0 生産(生産完畢) ---thread-1 銷售...這種情況,不會出現生産跟銷售交替。原因:使用Synchronized關鍵字對方法進行限制,預設鎖定的是對一個的object類,直到代碼結束,才會把鎖給釋放。是以使用該關鍵字進行限制時不會出現線程交疊現象。
備注:預設的使用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 }
代碼編譯後功能如下:
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 }
由此可見,使用volatile變量進行自增或自減操作的時候,變量進行temp= temp+1這一步時,多條線程同時可能同時操作這一句代碼,導緻内容出差。線程代碼内的原子性被破壞了。
單純使用volatile來控制boolean或者某一個int類型的時候,感覺不出太大的作用。但當volatile在修飾一個對象的時候,對象必須按照步驟進行。在單線程的情況下new一個對象必須進行三步操作:①開辟存儲空間 ②初始化 ③使用變量指向該記憶體。在并發的情況下,虛拟機建立對象可能依據這三步依次執行。可能③在②之前執行,那麼就可能會導緻程式抛出空指針異常。這時候可以使用volatile保證對象初始化原子性。
以上是對Java鎖機制的總結,如有問題,煩請指出糾正。代碼及例子很大一部分參考了《Java 并發程式設計藝術》[方騰飛 著]