Java併發程式設計之執行緒安全、執行緒通訊

leoliu168發表於2018-11-07

Java多執行緒開發中最重要的一點就是執行緒安全的實現了。所謂Java執行緒安全,可以簡單理解為當多個執行緒訪問同一個共享資源時產生的資料不一致問題。為此,Java提供了一系列方法來解決執行緒安全問題。

synchronized

synchronized用於同步多執行緒對共享資源的訪問,在實現中分為同步程式碼塊和同步方法兩種。

同步程式碼塊

 1 public class DrawThread extends Thread {
 2     
 3     private Account account;
 4     private double drawAmount;
 5     public DrawThread(String name, Account account, double drawAmount) {
 6         super(name);
 7         this.account = account;
 8         this.drawAmount = drawAmount;
 9     }
10     @Override
11     public void run() {
12         //使用account作為同步程式碼塊的鎖物件
13         synchronized(account) {
14             if (account.getBalance() >= drawAmount) {
15                 System.out.println(getName() + "取款成功, 取出:" + drawAmount);
16                 try {
17                     TimeUnit.MILLISECONDS.sleep(1);
18                 } catch (InterruptedException e) {
19                     e.printStackTrace();
20                 }
21                 account.setBalance(account.getBalance() - drawAmount);
22                 System.out.println("餘額為: " + account.getBalance());
23             } else {
24                 System.out.println(getName() + "取款失敗!餘額不足!");
25             }
26         }
27     }
28 }

同步方法

使用同步方法,即使用synchronized關鍵字修飾類的例項方法或類方法,可以實現執行緒安全類,即該類在多執行緒訪問中,可以保證可變成員的資料一致性。

同步方法中,隱式的鎖物件由鎖的是例項方法還是類方法確定,分別為該類物件或類的Class物件。

 1 public class SyncAccount {
 2     private String accountNo;
 3     private double balance;
 4     //省略構造器、getter setter方法
 5     //在一個簡單的賬戶取款例子中, 通過新增synchronized的draw方法, 把Account類變為一個執行緒安全類
 6     public synchronized void draw(double drawAmount) {
 7         if (balance >= drawAmount) {
 8             System.out.println(Thread.currentThread().getName() + "取款成功, 取出:" + drawAmount);
 9             try {
10                 TimeUnit.MILLISECONDS.sleep(1);
11             } catch (InterruptedException e) {
12                 e.printStackTrace();
13             }
14             balance -= drawAmount;
15             System.out.println("餘額為: " + balance);
16         } else {
17             System.out.println(Thread.currentThread().getName() + "取款失敗!餘額不足!");
18         }
19     }
20     //省略HashCode和equals方法
21 }

同步鎖(Lock、ReentrantLock)

Java5新增了兩個用於執行緒同步的介面Lock和ReadWriteLock,並且分別提供了兩個實現類ReentrantLock(可重入鎖)和ReentrantReadWriteLock(可重入讀寫鎖)。

相比較synchronized,ReentrantLock的一些優勢功能:

1. 等待可中斷:指持有鎖的執行緒長期不釋放鎖的時候,正在等待的執行緒可以選擇放棄等待。

2. 公平鎖:多個執行緒等待同一個鎖時,必須按照申請鎖的時間順序依次獲取。synchronized是非公平鎖,ReentrantLock可以通過引數設定為公平鎖

3. 多條件鎖:ReentrantLock可通過Condition類獲取多個條件關聯

Java 1.6以後,synchronized效能提升較大,因此一般的開發中依然建議使用語法層面上的synchronized加鎖。

Java8新增了更為強大的可重入讀寫鎖StampedLock類。

比較常用的是ReentrantLock類,可以顯示地加鎖、釋放鎖。下面使用ReentrantLock重構上面的SyncAccount類。

 1 public class RLAccount {
 2     //定義鎖物件
 3     private final ReentrantLock lock = new ReentrantLock();
 4     private String accountNo;
 5     private double balance;
 6     //省略構造方法和getter setter
 7     public void draw(double drawAmount) {
 8         //加鎖
 9         lock.lock();
10         try {
11             if (balance >= drawAmount) {
12                 System.out.println(Thread.currentThread().getName() + "取款成功, 取出:" + drawAmount);
13                 try {
14                     TimeUnit.MILLISECONDS.sleep(1);
15                 } catch (InterruptedException e) {
16                     e.printStackTrace();
17                 }
18                 balance -= drawAmount;
19                 System.out.println("餘額為: " + balance);
20             } else {
21                 System.out.println(Thread.currentThread().getName() + "取款失敗!餘額不足!");
22             } 
23         } finally {
24             //通過finally塊保證釋放鎖
25             lock.unlock();
26         }
27     }
28 }

死鎖

當兩個執行緒相互等待地方釋放鎖的時候,就會產生死鎖。關於死鎖和執行緒安全的深入分析,將另文介紹。

執行緒通訊方式之wait、notify、notifyAll

Object類提供了三個用於執行緒通訊的方法,分別是wait、notify和notifyAll。這三個方法必須由同步鎖物件來呼叫,具體來說:

1. 同步方法:因為同步方法預設使用所在類的例項作為鎖,即this,可以在方法中直接呼叫。

2. 同步程式碼塊:必須由鎖來呼叫。

wait():導致當前執行緒等待,直到其它執行緒呼叫鎖的notify方法或notifyAll方法來喚醒該執行緒。呼叫wait的執行緒會釋放鎖。

notify():喚醒任意一個在等待的執行緒

notifyAll():喚醒所有在等待的執行緒

 1 /*
 2  * 通過一個生產者-消費者佇列來說明執行緒通訊的基本使用方法
 3  * 注意: 假如這裡的判斷條件為if語句,喚醒方法為notify, 那麼如果分別有多個執行緒操作入隊出隊, 會導致執行緒不安全.
 4  */
 5 public class EventQueue {
 6     
 7     private final int max;
 8     
 9     static class Event{
10         
11     }
12     //定義一個不可改的連結串列集合, 作為佇列載體
13     private final LinkedList<Event> eventQueue = new LinkedList<>();
14     
15     private final static int DEFAULT_MAX_EVENT = 10;
16     
17     public EventQueue(int max) {
18         this.max = max;
19     }
20     
21     public EventQueue() {
22         this(DEFAULT_MAX_EVENT);
23     }
24     
25     private void console(String message) {
26         System.out.printf("%s:%s
",Thread.currentThread().getName(), message);
27     }
28     //定義入隊方法
29     public void offer(Event event) {
30         //使用連結串列物件作為鎖
31         synchronized(eventQueue) {
32             //在迴圈中判斷如果佇列已滿, 則呼叫鎖的wait方法, 使執行緒阻塞
33             while(eventQueue.size() >= max) {
34                 try {
35                     console(" the queue is full");
36                     eventQueue.wait();
37                 } catch (InterruptedException e) {
38                     e.printStackTrace();
39                 }
40             }
41             console(" the new event is submitted");
42             eventQueue.addLast(event);
43             this.eventQueue.notifyAll();
44         }
45     }
46     //定義出隊方法
47     public Event take() {
48         //使用連結串列物件作為鎖
49         synchronized(eventQueue) {
50             //在迴圈中判斷如果佇列已空, 則呼叫鎖的wait方法, 使執行緒阻塞
51             while(eventQueue.isEmpty()) {
52                 try {
53                     console(" the queue is empty.");
54                     eventQueue.wait();
55                 } catch (InterruptedException e) {
56                     e.printStackTrace();
57                 }
58             }
59             Event event = eventQueue.removeFirst();
60             this.eventQueue.notifyAll();
61             console(" the event " + event + " is handled/taked.");
62             return event;
63         }
64     }
65 }

執行緒通訊方式之Condition

如果使用的是Lock介面實現類來同步執行緒,就需要使用Condition類的三個方法實現通訊,分別是await、signal和signalAll,使用上與Object類的通訊方法基本一致。

 1 /*
 2  * 使用Lock介面和Condition來實現生產者-消費者佇列的通訊
 3  */
 4 public class ConditionEventQueue {
 5     //顯示定義Lock物件
 6     private final Lock lock = new ReentrantLock();
 7     //通過newCondition方法獲取指定Lock物件的Condition例項
 8     private final Condition cond = lock.newCondition();
 9     private final int max;
10     static class Event{ }
11     //定義一個不可改的連結串列集合, 作為佇列載體
12     private final LinkedList<Event> eventQueue = new LinkedList<>();
13     private final static int DEFAULT_MAX_EVENT = 10;
14     public ConditionEventQueue(int max) {
15         this.max = max;
16     }
17     
18     public ConditionEventQueue() {
19         this(DEFAULT_MAX_EVENT);
20     }
21     
22     private void console(String message) {
23         System.out.printf("%s:%s
",Thread.currentThread().getName(), message);
24     }
25     //定義入隊方法
26     public void offer(Event event) {
27             lock.lock();
28             try {
29                 //在迴圈中判斷如果佇列已滿, 則呼叫cond的wait方法, 使執行緒阻塞
30                 while (eventQueue.size() >= max) {
31                     try {
32                         console(" the queue is full");
33                         cond.await();
34                     } catch (InterruptedException e) {
35                         e.printStackTrace();
36                     }
37                 }
38                 console(" the new event is submitted");
39                 eventQueue.addLast(event);
40                 cond.signalAll();;
41             } finally {
42                 lock.unlock();
43             }
44         
45     }
46     //定義出隊方法
47     public Event take() {
48             lock.lock();
49             try {
50                 //在迴圈中判斷如果佇列已空, 則呼叫cond的wait方法, 使執行緒阻塞
51                 while (eventQueue.isEmpty()) {
52                     try {
53                         console(" the queue is empty.");
54                         cond.wait();
55                     } catch (InterruptedException e) {
56                         e.printStackTrace();
57                     }
58                 }
59                 Event event = eventQueue.removeFirst();
60                 cond.signalAll();
61                 console(" the event " + event + " is handled/taked.");
62                 return event;
63             } finally {
64                 lock.unlock();
65             }
66     }
67 }

Java 1.5開始就提供了BlockingQueue介面,來實現如上所述的生產者-消費者執行緒同步工具。具體介紹將另文說明。

 


相關文章