天天看點

Java多線程(二) 多線程的鎖機制

      當兩條線程同時通路一個類的時候,可能會帶來一些問題。并發線程重入可能會帶來記憶體洩漏、程式不可控等等。不管是線程間的通訊還是線程共享資料都需要使用Java的鎖機制控制并發代碼産生的問題。本篇總結主要著名Java的鎖機制,闡述多線程下如何使用鎖機制進行并發線程溝通。

1、并發下的程式異常

  先看下下面兩個代碼,檢視異常内容。

  異常1:單例模式

Java多線程(二) 多線程的鎖機制
Java多線程(二) 多線程的鎖機制
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

    運作結果如下:

Java多線程(二) 多線程的鎖機制

  由上述可見,Thread-7與其他結果不一緻,證明了在多線程并發的情況下這種單例寫法存在問題,問題就在第40行。多個線程同時進入了空值判斷,線程建立了新的類。

  異常2:線程重入,引發程式錯誤

     現在想模拟國企生産規則,每個月生産100件産品,然後當月消費20件,依次更替。模拟該工廠全年的生産與銷售

      備注:舉這個執行個體是為後面的信号量和生産者消費者問題做鋪墊。可以另外舉例,如開辟十條線程,每條線程内的任務就是進行1-10的累加,每條線程輸出的結果不一定是55(線程重入導緻)

Java多線程(二) 多線程的鎖機制
Java多線程(二) 多線程的鎖機制
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的鎖機制。下面總結下如何處理以上兩個問題。

2、使用多線程程式設計目的及一些Java多線程的基本知識

  使用多線程無非是期望程式能夠更快地完成任務,這樣并發程式設計就必須完成兩件事情:線程同步及線程通信。

      線程同步指的是:控制不同線程發生的先後順序。

      線程通信指的是:不同線程之間如何共享資料。 

   Java線程的記憶體模型:每個線程擁有自己的棧,堆記憶體共享 [來源:Java并發程式設計藝術 ],如下圖所示。 鎖是線程間記憶體和資訊溝通的載體,了解線程間通信會對線程鎖有個比較深入的了解。後面也會詳細總結Java是如何根據鎖的資訊進行兩條線程之間的通信。

Java多線程(二) 多線程的鎖機制

2、使用Java的鎖機制

    Java語音設計和資料庫一樣,同樣存在着代碼鎖.實作Java代碼鎖比較簡單,一般使用兩個關鍵字對代碼進行線程鎖定。最常用的就是volatile和synchronized兩個

     2.1 synchronized

       synchronized關鍵字修飾的代碼相當于資料庫上的互斥鎖。確定多個線程在同一時刻隻能由一個線程處于方法或同步塊中,確定線程對變量通路的可見和排它,獲得鎖的對象在代碼結束後,會對鎖進行釋放。

       synchronzied使用方法有兩個:①加在方法上面鎖定方法,②定義synchronized塊。

        模拟生産銷售循環,可以通過synchronized關鍵字控制線程同步。代碼如下:

Java多線程(二) 多線程的鎖機制
Java多線程(二) 多線程的鎖機制
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一樣,能夠塊狀地對代碼進行鎖定。該關鍵字可以看做對修飾的變量進行了讀或寫的同步操作。

  如以下代碼:

Java多線程(二) 多線程的鎖機制
Java多線程(二) 多線程的鎖機制
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 }      

 代碼編譯後功能如下:

Java多線程(二) 多線程的鎖機制
Java多線程(二) 多線程的鎖機制
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 并發程式設計藝術》[方騰飛 著]

繼續閱讀