天天看點

Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock

  在JDK5裡面,提供了一個Lock接口。該接口通過底層架構的形式為設計更面向對象、可更加細粒度控制線程代碼、更靈活控制線程通信提供了基礎。實作Lock接口且使用得比較多的是可重入鎖(ReentrantLock)以及讀寫鎖(ReentrantReadWriteLock)。

1. ReentrantLock

  在Java多線程(二) 多線程的鎖機制 裡面,已經總結過通過使用Synchronized關鍵字實作線程内的方法鎖定。但使用Synchronized關鍵字有一些局限性,上鎖和釋放鎖是由JVM決定的,使用者沒法上鎖和釋放進行控制。那麼問題就來了:假如有一個線程業務類管理某一全局變量的讀和寫。對于每條線程,在讀的時候資料是共享的可以讓多個線程同時去讀。但有某個線程在對該全局變量進行寫的時候,其他的線程都不能夠對變量進行讀或者寫(對應資料庫内的讀共享寫互斥)。可能會有如下僞代碼:

1 package com.scl.thread.lock;
 2 
 3 public class MyCounter
 4 {
 5     public int count;
 6 
 7     public int readCount()
 8     {
 9         return this.count;
10     }
11 
12     public void writeCount()
13     {
14         synchronized(this)
15         {
16             count++;
17         }
18     }
19 }      

  盡管對寫操作進行了空值,但是在寫的時候,線程還是能夠進行讀操作!由此,JDK5并發庫内提供了Lock接口。程式員可以通過實作Lock接口對代碼塊進行更靈活的鎖控制.

 JDK5通過使用AbstractQueuedSynchronizer(簡寫為AQS)抽象類把Lock接口的功能實作了一大部分功能,如果程式員需要編寫一套跟有自身邏輯的"鎖"時,可以簡單地通過實作public boolean tryAcquire(int acquire) 及 public boolean tryRelease(int releases) 進行加鎖及釋放鎖功能。AQS為整個并發内容的核心架構,類似synchronized的鎖(ReentrantLock :可重入鎖)就是使用了AQS架構進行建構。ReentrantLock提供了一個可中斷、擁有并發競争機制[指線程對鎖的競争方式:公平競争或不公平競争]的方式,該部分的内容的源碼分析可以檢視: ReentrantLock 實作原理深入探究

        正如ReentrantLock跟Synchronized關鍵字所使用的功能基本一樣,而且Synchronized還能自己釋放鎖,那什麼時候使用ReentrantLock?

  ① 在中斷線程的時候,可以使用ReentrantLock進行控制

    如線程1有一個耗時很大的任務在執行,執行時線程2必須進行等待。當線程1執行的任務時間實在太長了,線程2放棄等待進行線程後續的操作。該情況下如果使用Synchronized,隻能通過抛出異常的形式進行異常操作。

  ② 多條件變量通訊

    如有3條線程,線程1完成任務後通知線程2執行,線程2執行完業務邏輯以後通知線程3執行,線程3執行完通知線程1繼續執行。用Synchronized關鍵字很難處理這種問題。用Lock卻可以很好的處理這些内容。當然,線程1 、2、3 同樣地可以換由一個線程組去執行這些任務。  

   1.1  可中斷的線程控制

       1.1.1  Java的線程中斷機制

  Java中斷線程可以通過執行個體方法: stop 或 interrupt 進行線程中斷,兩者有什麼差別?先檢視以下兩段代碼及運作結果。

Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
1 package com.scl.thread.interrupt;
  2 
  3 public class TestInterrupt
  4 {
  5     // 各線程可見的線程狀态标志位
  6     public static volatile boolean isStop = false;
  7 
  8     public static void main(String[] args) throws InterruptedException
  9     {
 10         // 建立三條線程,線程1使用stop方法中斷,線程2使用interrupt方法中斷,線程3與線程2比較使用了interrupt後是否因中斷退出
 11         Thread th1 = new Thread(new SubThread1(), "SubThread1");
 12         Thread th2 = new Thread(new SubThread2(), "SubThread2");
 13         Thread th3 = new Thread(new SubThread3(), "SubThread3");
 14 
 15         System.out.println("==============subThread1 code block result==============");
 16         System.out.println("Main Thread call subThread1 to start");
 17         th1.start();
 18         Thread.sleep(3000);
 19         System.out.println("Main Thread start to stop subThread1");
 20         th1.stop();
 21         System.out.println("subThread1 was stopped by Main Thread");
 22         // 等待子線程進行stop,讓子線程有充分時間處理相關業務
 23         Thread.sleep(20);
 24         System.out.println("===================================================");
 25         Thread.sleep(20);
 26 
 27         System.out.println("==============subThread2 code block result==============");
 28         System.out.println("Main Thread call subThread2 to start");
 29         th2.start();
 30         Thread.sleep(3000);
 31         System.out.println("Main Thread start to interrupt subThread2");
 32         // 設定标志位,令子線程2可以按順序退出
 33         isStop = true;
 34         th2.interrupt();
 35         // 等待子線程進行interrupt,讓子線程有充分時間處理相關業務
 36         Thread.sleep(20);
 37         System.out.println(" subThread2 was interruptted by Main Thread");
 38         System.out.println("===================================================");
 39         Thread.sleep(20);
 40 
 41         System.out.println("==============subThread3 code block result==============");
 42         System.out.println("Main Thread call subThread3 to start");
 43         th3.start();
 44         Thread.sleep(3000);
 45         System.out.println("Main Thread start to interrupt subThread3");
 46         th2.interrupt();
 47         // 等待子線程進行interrupt,讓子線程有充分時間處理相關業務
 48         Thread.sleep(20);
 49         System.out.println("subThread3 was interrupted by Main Thread");
 50         System.out.println("===================================================");
 51         Thread.sleep(20);
 52 
 53         System.out.println("Main Thread end");
 54     }
 55 }
 56 
 57 class SubThread1 implements Runnable
 58 {
 59     @Override
 60     public void run()
 61     {
 62         while (!TestInterrupt.isStop)
 63         {
 64             try
 65             {
 66                 // 子線程1進行睡眠
 67                 Thread.sleep(2000);
 68             }
 69             catch (InterruptedException e)
 70             {
 71                 e.printStackTrace();
 72             }
 73 
 74             System.out.println(Thread.currentThread().getName() + " is running...");
 75         }
 76         // 調用stop方法,該語句不會被執行,因為線程整個退出了
 77         System.out.println(Thread.currentThread().getName() + " is ready to cancle");
 78     }
 79 }
 80 
 81 class SubThread2 implements Runnable
 82 {
 83     @Override
 84     public void run()
 85     {
 86         while (!TestInterrupt.isStop)
 87         {
 88             try
 89             {
 90                 // 子線程1進行睡眠
 91                 Thread.sleep(200);
 92             }
 93             catch (InterruptedException e)
 94             {
 95                 e.printStackTrace();
 96             }
 97 
 98             System.out.println(Thread.currentThread().getName() + " is running...");
 99         }
100         // 使用interrupt方法,在發現線程2被阻塞或休眠(sleep)的情況下,會收到一個interrupt的異常。但不會終止線程,僅設定線程是否可以中斷的标志位
101         System.out.println(Thread.currentThread().getName() + " is ready to cancle");
102     }
103 }
104 
105 // 調用interrupt方法,對比子線程2,發現使用interrupt方法根本沒有中斷整個線程,設定後線程也沒有進行退出。一直運作
106 class SubThread3 implements Runnable
107 {
108     @Override
109     public void run()
110     {
111         // 使用true代替标志位,判斷調用interrupt方法後是否正常中斷線程
112         while (true)
113         {
114             try
115             {
116                 Thread.sleep(2000);
117             }
118             catch (InterruptedException e)
119             {
120                 e.printStackTrace();
121             }
122             System.out.println(Thread.currentThread().getName() + " is running...");
123         }
124     }
125 }      

stop方法及interrupt方法對比

Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
1 ==============subThread1 code block result==============
 2 Main Thread call subThread1 to start
 3 SubThread1 is running...
 4 Main Thread start to stop subThread1
 5 subThread1 was stopped by Main Thread
 6 ===================================================
 7 ==============subThread2 code block result==============
 8 Main Thread call subThread2 to start
 9 SubThread2 is running...
10 SubThread2 is running...
11 SubThread2 is running...
12 SubThread2 is running...
13 SubThread2 is running...
14 SubThread2 is running...
15 SubThread2 is running...
16 SubThread2 is running...
17 SubThread2 is running...
18 SubThread2 is running...
19 SubThread2 is running...
20 SubThread2 is running...
21 SubThread2 is running...
22 SubThread2 is running...
23 Main Thread start to interrupt subThread2
24 SubThread2 is running...
25 SubThread2 is ready to cancle
26 java.lang.InterruptedException: sleep interrupted
27     at java.lang.Thread.sleep(Native Method)
28     at com.scl.thread.interrupt.SubThread2.run(TestInterrupt.java:91)
29     at java.lang.Thread.run(Thread.java:744)
30  subThread2 was interruptted by Main Thread
31 ===================================================
32 ==============subThread3 code block result==============
33 Main Thread call subThread3 to start
34 SubThread3 is running...
35 Main Thread start to interrupt subThread3
36 subThread3 was interrupted by Main Thread
37 ===================================================
38 Main Thread end
39 SubThread3 is running...
40 SubThread3 is running...      

運作結果

  從代碼運作結果上面,可以看到兩個函數的差異:stop把線程給結束了,而interrupt方法沒有結束線程,是以兩者總結如下:

  ① 線上程執行個體調用stop 方法後把線程給終結了,在子線程1内while以外的代碼塊将不會被執行

  ② 線上程執行個體調用interrupt 方法後,線程沒有被終結且該方法通過線程間協作的關系,可以有序地退出線程。且執行個體線程在被中斷後,若線程被阻塞或者休眠的情況下,将收到一個InterruptedException。

  ③ 子線程3的運作結果充分證明了線程執行個體調用interrupt方法根本沒有把線程給中斷,隻是把線程的标志狀态進行更改。讓線程執行個體在适當的時機進行退出。

    由此可見public void interrupt( )并非馬上對線程進行中斷(強行結束線程),而是通過協作的方法把線程的狀态設定為可中斷。告知阻塞中的線程在特定的時刻可以對進行終結。同時,Java提供了兩個檢驗線程中斷方法① 執行個體方法 public boolean isInterrupted() ②靜态方法 public static boolean interrupted();

     兩個方法的差別是:靜态方法會去清理線程狀态資訊。執行個體方法不會清理狀态标志。即當線程執行個體調用interrupt 的前提下,若再調用靜态方法,第一次會傳回true,後面全傳回false;若在前提下,調用執行個體方法isInterrupt,如果線程正常退出,會一直傳回false [判斷線程Thread對象是否已經是終止狀态,與線程狀态無關]。靜态方法偏向于判斷線程狀态。而執行個體方法更關心線程是否存活。

  需要特别注意的是當線程調用interrupt方法時,假如線程在等待鎖或者被休眠了。中斷狀态會被設定為false。JDK的API裡面明确指出了這一點。

下面開始檢視Lock與synchronized關鍵字與ReentrantLock的一些差別。

  1.1.2 ReentrantLock對線程中斷的控制

        首先,單純地使用synchronized關鍵字不能進行鎖中斷控制. 在synchronized關鍵字控制的代碼塊内,不會因為線程中斷而做出相關處理。

 先檢視使用synchronized關鍵字在處理線程中斷時的結果。

業務邏輯主要為:開辟兩條線程,一條線程對檔案進行讀操作,另一條線程對檔案進行寫操作。寫操作内容需要時間較長,且先執行。讀操作後執行,若讀線程等待超過4秒。讓讀線程中斷,進行格式化檔案。

① 使用接口,區分使用synchronized關鍵字及Lock方式控制線程中斷的業務邏輯

Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
1 package com.scl.thread.interrupt;
 2 
 3 //檔案讀寫接口,使用Synchronized關鍵字控制線程中斷以及使用Lock控制線程中斷都實作該接口
 4 public interface IFileHandler
 5 {
 6     boolean isGetReadLock = false;
 7 
 8     void read();
 9 
10     void write();
11 
12     void formatFile();
13 }      

控制檔案接口

② 在synchronized關鍵字控制代碼塊的前提下,對線程進行中斷的業務邏輯代碼。

Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
1 package com.scl.thread.interrupt;
 2 
 3 public class SyncFileHandler implements IFileHandler
 4 {
 5     private volatile boolean isGetReadLock = false;
 6 
 7     public boolean isGetReadLock()
 8     {
 9         return isGetReadLock;
10     }
11 
12     public void read()
13     {
14         synchronized (FileHandlerByThreads.class.getClass())
15         {
16             System.out.println(Thread.currentThread().getName() + " start");
17             // 能進來則設定變量标志位
18             isGetReadLock = true;
19         }
20     }
21 
22     // 模拟運作時間比較久的寫操作
23     public void write()
24     {
25         try
26         {
27             synchronized (FileHandlerByThreads.class.getClass())
28             {
29                 System.out.println(Thread.currentThread().getName() + " start");
30                 long startTime = System.currentTimeMillis();
31                 // 模拟一個耗時較長的操作
32                 for (;;)
33                 {
34                     if (System.currentTimeMillis() - startTime > Integer.MAX_VALUE)
35                     {
36                         break;
37                     }
38                 }
39             }
40 
41             System.out.println("Writer has writered down everything! bravo");
42         }
43         catch (Exception e)
44         {
45             e.printStackTrace();
46         }
47 
48     }
49 
50     public void formatFile()
51     {
52         System.out.println("begin to format the file");
53         // format the file
54     }
55 }      

synchronized控制下的線程中斷

③ 用戶端測試代碼

Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
1 package com.scl.thread.interrupt;
 2 
 3 public class TestLockInterruptibly
 4 {
 5     public static void main(String[] args) throws Exception
 6     {
 7         // 1. 根據lock控制中斷
 8         // FileHandlerByThreads fileControl = new FileHandlerByThreads();
 9         // Thread readthr = new Thread(new ReadThread(fileControl), "reader");
10         // Thread writethr = new Thread(new WriteThread(fileControl), "writer");
11 
12         // 2. 使用synchronized關鍵字控制中斷線程
13         SyncFileHandler sync = new SyncFileHandler();
14 
15         Thread readthr = new Thread(new ReadThread(sync), "reader");
16         Thread writethr = new Thread(new WriteThread(sync), "writer");
17         writethr.start();
18         readthr.start();
19 
20         long startTime = System.currentTimeMillis();
21         // 循環判是否有線程擷取到了讀鎖斷
22         while (!sync.isGetReadLock())
23         {
24             long endTime = System.currentTimeMillis();
25             // 如果4秒後讀線程仍然沒有等到讀鎖,離開等待
26             if (endTime - startTime > 4000)
27             {
28                 readthr.interrupt();
29                 System.out.println("4 seconds have passed,try to interrupt reader Thread");
30                 break;
31             }
32         }
33 
34     }
35 }
36 
37 class ReadThread implements Runnable
38 {
39     private IFileHandler fileControl;
40 
41     public ReadThread(IFileHandler fileControl)
42     {
43         this.fileControl = fileControl;
44     }
45 
46     @Override
47     public void run()
48     {
49         fileControl.read();
50         // 測試單純使用synchronized關鍵字控制線程中斷
51         System.out.println("reader thread end");
52         fileControl.formatFile();
53     }
54 }
55 
56 class WriteThread implements Runnable
57 {
58     private IFileHandler fileControl;
59 
60     public WriteThread(IFileHandler fileControl)
61     {
62         this.fileControl = fileControl;
63     }
64 
65     @Override
66     public void run()
67     {
68         fileControl.write();
69     }
70 }      

用戶端測試代碼

代碼運作結果:線程未中斷,控制台輸出如下

Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
1 writer start
2 4 seconds have passed,try to interrupt reader Thread      

synchronized代碼運作結果

    如上面的結果,我們期望的是在讀線程在運作4秒後能夠被中斷,且去運作格式化代碼的任務。但是在讀線程在調用interrupt方法後,讀方法後面的代碼并沒有執行。反而是一直等待。控制台并沒有輸出"reader thread end",以及格式化代碼的操作。由此可見synchronized關鍵字不會去響應線程中斷。

   檢視了大部分部落格後,發現大家寫的都是synchronized并不響應中斷。但使用synchronized是否不能完成可中斷線程的響應呢?

   要接收到中斷資訊,無非有兩種方法 ①等待鎖(使用wait、join等方法) ②進入休眠。這兩個做法都需要使用循環,讓程式等待。第一種方法完全不可行,我現在就是想要什麼時候能夠擷取到鎖,JDK通過synchronized沒有提供方法讓程式員知道:"我的代碼擷取到鎖了嗎"這個條件,其次等待對象wait方法,需要在synchrnized裡面。synchronized (FileHandlerByThreads.class.getClass())這個條件本來就進不去,更别談裡面的wait方法了。第二種方法,讓程式進入休眠。是以有以下代碼

Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
1     public void read()
 2     {
 3         try
 4         {
 5 System.out.println(Thread.currentThread().getName() + " start");        
 6                 while (true)
 7                 {
 8                     Thread.sleep(100);
 9                     if (Thread.currentThread().isInterrupted())
10                     {
11                         break;
12                     }
13                 }
14                 synchronized (FileHandlerByThreads.class.getClass())
15                 {
16                     System.out.println(Thread.currentThread().getName() + " start");
17                 }
18             
19 
20         }
21         catch (InterruptedException e)
22         {
23             e.printStackTrace();
24             System.out.println("reader Thread leave the file and going to format the file");
25         }
26         finally
27         {
28             // lock.unlock();
29         }
30 
31     }      

未蔔先知型 sleep

    這樣寫,終于能夠擷取到interrupt發送過來的資訊,并且捕獲到異常了。但是,read方法一直都在休眠。這做法不是未蔔先知了嗎,因為你都知道了read方法肯定是得不到鎖的,不斷地在休眠。

   是以,使用synchronized真的沒有方法合理地中斷線程的響應。

  下面使用ReentrantLock實作可中斷線程控制,過程非常簡單。把程式稍修改一下就可以了

Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
1 package com.scl.thread.interrupt;
 2 
 3 import java.util.concurrent.locks.ReentrantLock;
 4 
 5 public class FileHandlerByThreads implements IFileHandler
 6 {
 7 
 8     private volatile boolean isGetReadLock = false;
 9     private ReentrantLock lock = new ReentrantLock();
10 
11     public boolean isGetReadLock()
12     {
13         return isGetReadLock;
14     }
15 
16     public void read()
17     {
18 
19         try
20         {
21             // 等待20毫秒再進行後續操作,防止主線程操作過快
22             Thread.sleep(50);
23             // 使用reentrantlock
24             lock.lockInterruptibly();
25             System.out.println(Thread.currentThread().getName() + " start");
26             isGetReadLock = true;
27         }
28         catch (InterruptedException e)
29         {
30             e.printStackTrace();
31             System.out.println("reader Thread leave the file and going to format the file");
32         }
33         // 不能在此處進行鎖釋放,因為被阻塞的線程可能根本沒有擷取到鎖,若調用unlock方法會抛出IllegalMonitorStateException異常
34         // finally
35         // {
36         // lock.unlock();
37         // }
38 
39     }
40 
41     // 模拟運作時間比較久的寫操作
42     public void write()
43     {
44         try
45         {
46 
47             // 1.使用lock實作寫鎖定
48             // 等待20毫秒再進行後續操作,防止主線程操作過快
49             Thread.sleep(20);
50             lock.lock();
51             System.out.println(Thread.currentThread().getName() + " start");
52             long startTime = System.currentTimeMillis();
53             // 模拟一個耗時較長的操作
54             for (;;)
55             {
56                 if (System.currentTimeMillis() - startTime > Integer.MAX_VALUE)
57                 {
58                     break;
59                 }
60             }
61 
62             System.out.println("Writer has writered down everything! bravo");
63         }
64         catch (Exception e)
65         {
66             e.printStackTrace();
67         }
68         finally
69         {
70             lock.unlock();
71         }
72     }
73 
74     public void formatFile()
75     {
76         System.out.println("begin to format the file");
77         // format the file
78     }
79 }      

Reentrantlock實作可中斷線程

Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
1 package com.scl.thread.interrupt;
 2 
 3 public class TestLockInterruptibly
 4 {
 5     public static void main(String[] args) throws Exception
 6     {
 7         // 1. 根據lock控制中斷
 8         FileHandlerByThreads fileControl = new FileHandlerByThreads();
 9         Thread readthr = new Thread(new ReadThread(fileControl), "reader");
10         Thread writethr = new Thread(new WriteThread(fileControl), "writer");
11 
12         // 2. 使用synchronized關鍵字控制中斷線程
13         // SyncFileHandler sync = new SyncFileHandler();
14         // Thread readthr = new Thread(new ReadThread(sync), "reader");
15         // Thread writethr = new Thread(new WriteThread(sync), "writer");
16 
17         writethr.start();
18         readthr.start();
19 
20         long startTime = System.currentTimeMillis();
21         // 循環判是否有線程擷取到了讀鎖斷
22         while (!fileControl.isGetReadLock())
23         {
24             long endTime = System.currentTimeMillis();
25             // 如果4秒後讀線程仍然沒有等到讀鎖,離開等待
26             if (endTime - startTime > 4000)
27             {
28                 readthr.interrupt();
29                 System.out.println("4 seconds have passed,try to interrupt reader Thread");
30                 break;
31             }
32         }
33 
34     }
35 }
36 
37 class ReadThread implements Runnable
38 {
39     private IFileHandler fileControl;
40 
41     public ReadThread(IFileHandler fileControl)
42     {
43         this.fileControl = fileControl;
44     }
45 
46     @Override
47     public void run()
48     {
49         try
50         {
51             fileControl.read();
52         }
53         catch (InterruptedException e)
54         {
55             // 測試單純使用synchronized關鍵字控制線程中斷
56             System.out.println("reader thread end");
57             fileControl.formatFile();
58         }
59     }
60 }
61 
62 class WriteThread implements Runnable
63 {
64     private IFileHandler fileControl;
65 
66     public WriteThread(IFileHandler fileControl)
67     {
68         this.fileControl = fileControl;
69     }
70 
71     @Override
72     public void run()
73     {
74         fileControl.write();
75     }
76 }      
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
1 writer start
 2 4 seconds have passed,try to interrupt reader Thread
 3 reader Thread leave the file and going to format the file
 4 java.lang.InterruptedException
 5     at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:896)
 6     at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1221)
 7     at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:340)
 8     at com.scl.thread.interrupt.FileHandlerByThreads.read(FileHandlerByThreads.java:24)
 9     at com.scl.thread.interrupt.ReadThread.run(TestLockInterruptibly.java:51)
10     at java.lang.Thread.run(Thread.java:744)      

  需要注意的是FileHandlerByThreads這個類裡面的read方法,不能用finally去對lock給解鎖。因為被阻塞的讀線程基本不可能擷取到鎖,如果再去釋放鎖的話會抛出一個java.lang.IllegalMonitorStateException異常。

   在總結這随筆之前,本人也有好幾個疑問:

 ① Reentrantlock鎖的是什麼對象?

synchronized關鍵字的實作中,本人通過使用鎖靜态對象的方法把代碼塊給控制了,但是Reentrantlock 的lock執行個體根本沒有指定任何鎖定對象,那鎖定的到底是什麼。個人認為Reentrantlock根本沒有鎖定任何東西,因為這個架構底層都是基于CAS去實作的,在底層代碼裡面也沒有發現任何Reentrantlock鎖對象的内容。認為鎖定的是 Reentrantlock 内置的對象也沒關系,因為鎖定的内容完全可以不用關心。

 ② Reentrantlock為什麼能夠實作可中斷的線程響應?

根據上面synchronized關鍵字實作的中斷中,已經知道synchronized不能實作響應式中斷的原因是:不能知道線程能否擷取到鎖,即JDK沒有提供代碼給程式員使用說:"我的代碼擷取到鎖了嗎"這個條件。Reentrantlock 提供了一個擷取這個條件的方法:tryLock(),該方法可以測試,代碼到底能否擷取到鎖。個人猜測底層也是通過這個方法去實作響應式中斷線程的。

1.1.3 ReentrantLock實作多條件變量的控制  

    使用synchronized關鍵字控制線程間的通訊,基本通過wait和notify兩個方法把線程給阻塞以及喚醒,來協調兩個線程之間的通訊。當使用多個條件的時候,發現使用synchronized很難去實作。例如:生産者-消費者模式中,假如倉庫裡面的庫存已經沒法容納更多的産品,這時候應該調用notify方法把消費者線程喚醒,生産者線程進入休眠。但synchronized的方法沒辦法通過notify方法喚醒消費者。在調用notify的時候,喚醒的是所有等待鎖的線程對象;這時候等待鎖的可能是消費者也有可能是生産者,如果喚醒的是生産者,那麼生産者又進入了休眠。這樣将會導緻程式的執行效率比較低。如果有倉庫滿了,有方法喚醒消費者線程就好了。這時候,ReentrantLock的按對象喚醒就派上用場了;這個也是synchronized處理不了的。

 基于這個内容,先看下JDK API 所提供按條件喚醒、休眠的例子。

Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
1 package com.scl.thread.lock.condition;
 2 
 3 import java.util.concurrent.locks.Condition;
 4 import java.util.concurrent.locks.Lock;
 5 import java.util.concurrent.locks.ReentrantLock;
 6 
 7 class BoundedBuffer
 8 {
 9     final Lock lock = new ReentrantLock();
10     // 建立兩個不同歸屬的鎖對象
11     final Condition notFull = lock.newCondition();
12     final Condition notEmpty = lock.newCondition();
13     // 緩沖區
14     final Object[] items = new Object[100];
15     int putptr, takeptr, count;
16 
17     public void put(Object x) throws InterruptedException
18     {
19         lock.lock();
20         try
21         {
22             while (count == items.length)
23                 // 緩沖區已滿,阻塞“生産”線程,通知“消費”線程競争鎖
24                 notFull.await();
25             items[putptr] = x;
26             if (++putptr == items.length)
27                 putptr = 0;
28             ++count;
29             // 緩沖區非空,通知“消費”線程競争鎖,繼續消費
30             notEmpty.signal();
31         }
32         finally
33         {
34             lock.unlock();
35         }
36     }
37 
38     public Object take() throws InterruptedException
39     {
40         lock.lock();
41         try
42         {
43             while (count == 0)
44                 // 緩沖區為空,阻塞“消費”
45                 notEmpty.await();
46             Object x = items[takeptr];
47             if (++takeptr == items.length)
48                 takeptr = 0;
49             --count;
50             // 緩沖區未滿,通知“生産”線程繼續生産。
51             notFull.signal();
52             return x;
53         }
54         finally
55         {
56             lock.unlock();
57         }
58     }
59 }      

API condition 實作緩沖區例子

   API 中就是使用了condition實作按條件喚醒線程功能。使用一個condition同樣能夠實作功能,但效率可能不高。

  使用ReetrantLock及condition更改進出庫内的生産者-消費者模型。

Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
1 package com.scl.thread.lock.condition;
 2 
 3 import java.util.concurrent.ExecutorService;
 4 import java.util.concurrent.Executors;
 5 
 6 public class TestCarPark
 7 {
 8     public static void main(String[] args)
 9     {
10         // 假設車庫内有3個車輛使出者,5個使用的.模拟工作五分鐘的情況
11         ExecutorService driverInWorkers = Executors.newFixedThreadPool(5);
12         ExecutorService driverOutWorkers = Executors.newFixedThreadPool(3);
13         long startTime = System.currentTimeMillis();
14         CarPark carPark = new CarPark(10);
15 
16         while (true)
17         {
18 
19             long endTime = System.currentTimeMillis();
20             if (endTime - startTime > 2000)
21             {
22                 // 程式運作20秒後,不再加任務
23                 driverOutWorkers.shutdown();
24                 driverInWorkers.shutdown();
25                 break;
26             }
27             else
28             {
29                 try
30                 {
31                     Thread.sleep(50);
32                 }
33                 catch (InterruptedException e)
34                 {
35                     e.printStackTrace();
36                 }
37                 // 不斷地加減任務
38                 driverOutWorkers.submit(new Secute(carPark));
39                 driverInWorkers.submit(new CarOwner(carPark));
40             }
41         }
42         // 如果線程池任務都已經完成, 則退出線程池
43         if (driverInWorkers.isTerminated() && driverOutWorkers.isTerminated())
44         {
45             driverOutWorkers.shutdownNow();
46             driverInWorkers.shutdownNow();
47         }
48     }
49 }      
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
1 package com.scl.thread.lock.condition;
2 
3 //模拟車子類,設定成空
4 public class Car
5 {
6     public Car()
7     {
8     }
9 }      

模拟汽車類

Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
1 package com.scl.thread.lock.condition;
 2 
 3 public class CarOwner implements Runnable
 4 {
 5     private CarPark carPark;
 6 
 7     public CarOwner(CarPark carPark)
 8     {
 9         this.carPark = carPark;
10     }
11 
12     @Override
13     public void run()
14     {
15         carPark.driverIn();
16     }
17 }      

持車人 CarOwner

Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
1 package com.scl.thread.lock.condition;
 2 
 3 public class Secute implements Runnable
 4 {
 5     private CarPark carPark;
 6 
 7     public Secute(CarPark carPark)
 8     {
 9         this.carPark = carPark;
10     }
11 
12     @Override
13     public void run()
14     {
15         carPark.driverOut();
16     }
17 }      

車庫出車人 Secute

Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
1 package com.scl.thread.lock.condition;
  2 
  3 import java.util.ArrayList;
  4 import java.util.List;
  5 import java.util.concurrent.locks.Condition;
  6 import java.util.concurrent.locks.ReentrantLock;
  7 
  8 /**
  9  * 
 10  * @author scl
 11  * 
 12  * @fileName CarPark.java
 13  * 
 14  * @time 2016下午3:42:35
 15  * 
 16  *       declaration: 模拟車庫每次進出一輛車子
 17  */
 18 public class CarPark
 19 {
 20     protected int MaxCarNum;
 21     private volatile List<Car> carList = new ArrayList<Car>();
 22     private ReentrantLock reLock = new ReentrantLock();
 23     private Condition driverInCon = reLock.newCondition();
 24     private Condition driverOutCon = reLock.newCondition();
 25 
 26     public CarPark(int maxNum)
 27     {
 28         this.MaxCarNum = maxNum;
 29     }
 30 
 31     public void driverIn()
 32     {
 33         reLock.lock();
 34         try
 35         {
 36             // while (true)
 37             // {
 38             //
 39             // if (carList.size() + 1 > MaxCarNum)
 40             // {
 41             // System.out.println(Thread.currentThread().getName() +
 42             // " 目前車庫車輛數目:" + carList.size() + "車庫滿了,不能再入庫了");
 43             //
 44             // driverInCon.await();
 45             // }
 46             // else
 47             // {
 48             // carList.add(new Car());
 49             // Thread.sleep(300);
 50             // System.out.println(Thread.currentThread().getName() +
 51             // " 已入庫1輛汽車,目前車庫車輛數目: " + carList.size());
 52             // // 從這句代碼可以看出signal并不釋放鎖
 53             // driverOutCon.signal();
 54             // }
 55             // }
 56             while (carList.size() + 1 > MaxCarNum)
 57             {
 58                 System.out.println(Thread.currentThread().getName() + " 目前車庫車輛數目:" + carList.size() + "車庫滿了,不能再入庫了");
 59                 driverInCon.await();
 60             }
 61 
 62             carList.add(new Car());
 63             Thread.sleep(30);
 64             System.out.println(Thread.currentThread().getName() + " 已入庫1輛汽車,目前車庫車輛數目: " + carList.size());
 65             // signal不會 釋放鎖,也不會喚醒某個線程,隻是在condition隊列裡面把某條線程出列
 66             driverOutCon.signal();
 67 
 68         }
 69         catch (Exception e)
 70         {
 71             e.printStackTrace();
 72         }
 73         finally
 74         {
 75             reLock.unlock();
 76         }
 77 
 78     }
 79 
 80     public void driverOut()
 81     {
 82         reLock.lock();
 83         try
 84         {
 85             // while (true)
 86             // {
 87             //
 88             // if (carList.size() - 1 < 0)
 89             // {
 90             // System.out.println(Thread.currentThread().getName() +
 91             // " 車庫沒有車了,目前車庫車輛數目: " + carList.size());
 92             // driverOutCon.await();
 93             // }
 94             // else
 95             // {
 96             // // 使出一輛
 97             // carList.remove(0);
 98             // Thread.sleep(300);
 99             // System.out.println(Thread.currentThread().getName() +
100             // " 已經使出一輛車,目前車庫車輛數目:" + carList.size());
101             // // signal不會 釋放鎖
102             // driverInCon.signal();
103             // }
104             // }
105 
106             while (carList.size() - 1 < 0)
107             {
108                 System.out.println(Thread.currentThread().getName() + " 車庫沒有車了,目前車庫車輛數目: " + carList.size());
109                 driverOutCon.await();
110             }
111 
112             // 使出一輛
113             carList.remove(0);
114             Thread.sleep(30);
115             System.out.println(Thread.currentThread().getName() + " 已經使出一輛車,目前車庫車輛數目:" + carList.size());
116             // signal不會 釋放鎖,也不會喚醒某個線程,隻是在condition隊列裡面把某條線程出列
117             driverInCon.signal();
118 
119         }
120         catch (Exception e)
121         {
122             e.printStackTrace();
123         }
124         finally
125         {
126             reLock.unlock();
127         }
128     }
129 }      

停車場 CarPark

   在開始寫停車場類的時候,代碼無意中寫錯,發現一個問題:signal方法根本不能釋放對象鎖且不能喚醒任何線程,檢視JDK内的notify方法進行了一下比較。API内注明如下:

直到目前線程放棄此對象上的鎖定,才能繼續執行被喚醒的線程 ;後來搜尋了一下他人的部落格,發現Condition同樣維護着一個隊列,當調用await的時候,線程被扔進Condition的隊列内,直到調用signal且函數釋放了對象鎖,才會對線程進行喚醒。相關部落格位址:http://www.liuinsect.com/2014/01/27/how_to_understand_condition

同樣地,可以對停車場内寫錯的代碼進行使用進行驗證該觀點。

2. ReentrantReadWriteLock (讀寫鎖)

  在使用資料庫事務的時候,資料庫在處理事務時有兩個很重要的鎖。一個是讀鎖,一個是寫鎖。讀鎖共享,能跟其他讀鎖并存;寫鎖排它,寫鎖與讀鎖互斥、寫鎖與寫鎖互斥。JDK同樣地在控制多線程的時候有這讀、寫這兩把鎖。  

      首先要指出的是ReentrantLock與ReentrantLock沒有任何聯系。唯一有聯系的就是兩種不同的鎖都是基于AQS進行實作的。

     Hibernate 裡面有個叫延遲加載的功能,跟讀寫鎖很相似。讀的時候,在記憶體裡面查找。如果記憶體沒有,則查詢資料庫。

Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
1 package com.scl.thread.lock.condition;
 2 
 3 import java.util.HashMap;
 4 import java.util.Map;
 5 import java.util.concurrent.locks.ReentrantReadWriteLock;
 6 
 7 public class CacheDemo
 8 {
 9     // 模拟記憶體中緩存的資料
10     private Map<String, Object> map = new HashMap<String, Object>();
11     private ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
12 
13     // 擷取對象
14     private Object getObject(String id)
15     {
16         Object data = map.get(id);
17         // 寫鎖是共享的,可以多條線程進入通路
18         rwlock.readLock().lock();
19         try
20         {
21             // 如果記憶體裡面該值為空
22             if (data == null)
23             {
24                 // 釋放寫鎖
25                 rwlock.readLock().unlock();
26                 // 添加寫鎖
27                 rwlock.writeLock().lock();
28                 // 如果某一線程在特定時間點讀到資料則不再通路資料庫。防止線程重讀
29                 if (data == null)
30                 {
31                     try
32                     {
33                         data = readDataFromDB(id);
34                     }
35                     catch (Exception e)
36                     {
37                         e.printStackTrace();
38                     }
39                     finally
40                     {
41                         // 釋放寫鎖,重新上讀鎖
42                         rwlock.writeLock().unlock();
43                         rwlock.readLock().lock();
44                     }
45                 }
46             }
47         }
48         catch (Exception e)
49         {
50             e.printStackTrace();
51         }
52         finally
53         {
54             rwlock.readLock().unlock();
55         }
56         return data;
57     }
58 
59     private Object readDataFromDB(String id)
60     {
61         // 模拟從資料庫讀取資料
62         return new Object();
63     }
64 
65 }      

模拟緩存代碼

JDK Api中也有模拟緩存的例子,還用了鎖降級。不是很了解鎖降級的具體作用。後面具體查找下原因再補充。

Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
Java多線程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
1 package com.scl.thread.lock.condition;
 2 
 3 import java.util.concurrent.locks.ReentrantReadWriteLock;
 4 
 5 class CachedData
 6 {
 7     Object data;
 8     volatile boolean cacheValid;
 9     ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
10 
11     void processCachedData()
12     {
13         rwl.readLock().lock();
14         if (!cacheValid)
15         {
16             // Must release read lock before acquiring write lock
17             rwl.readLock().unlock();
18             rwl.writeLock().lock();
19             // Recheck state because another thread might have acquired
20             // write lock and changed state before we did.
21             if (!cacheValid)
22             {
23                 data = getDataFromDB();
24                 cacheValid = true;
25             }
26             // Downgrade by acquiring read lock before releasing write lock
27             rwl.readLock().lock();
28             rwl.writeLock().unlock(); // Unlock write, still hold read
29         }
30         // use data to do something
31         use(data);
32         rwl.readLock().unlock();
33     }
34 
35     // 模拟調用資料庫
36     private Object getDataFromDB()
37     {
38         return new Object();
39     }
40 
41     // 模拟使用資料
42     private void use(Object data)
43     {
44         System.out.println(data.toString());
45     }
46 }      

JDK操作緩存示例代碼

以上為本次對JDK5上面鎖的總結,如有問題,煩請指出糾正。

繼續閱讀