Java多線程之「同步」

好習慣要堅持,這是我第二篇博文,任務略重,可是要堅持努力!!!html

1.競爭條件java

首先,咱們回顧一下《Java核心技術卷》裏講到的多線程的「競爭條件」。因爲各線程訪問數據的次序,可能會產生訛誤的現象,這樣一個狀況一般稱爲「競爭條件」。編程

那麼,訛誤具體是怎麼產生的呢?本質上,是因爲操做的非原子性。好比,假定兩個線程同時執行指令 account[to] += amount;該指令可能會被處理以下:數組

1)將account[to]加載到寄存器。緩存

2)增長amount[to]。多線程

3)將結果寫回account[to]。併發

如今,假定第一個線程執行步驟1和2,而後,它被剝奪了運行權。假定第二個線程被喚醒並修改了accounts數組中的同一項。而後,第1個線程被喚醒並完成第3步。這樣,這一動做擦去了第二個線程所作的更新。因而,總金額再也不正確。dom

---------------------------------------------我是分割線---------------------------------------------------------------------------------------------ide

好,咱們再從java的內存模型來深層次講講「訛誤」,這裏有個概念叫作「緩存一致性」。測試

你們都知道,計算機在執行程序時,每條指令都是在CPU中執行的,而執行指令過程當中,勢必涉及到數據的讀取和寫入。因爲程序運行過程當中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,因爲CPU執行速度很快,而從內存讀取數據和向內存寫入數據的過程跟CPU執行指令的速度比起來要慢的多,所以若是任什麼時候候對數據的操做都要經過和內存的交互來進行,會大大下降指令執行的速度。所以在CPU裏面就有了高速緩存。

也就是,當程序在運行過程當中,會將運算須要的數據從主存複製一份到CPU的高速緩存當中,那麼CPU進行計算時就能夠直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束以後,再將高速緩存中的數據刷新到主存當中。舉個簡單的例子,好比下面的這段代碼:

i = i + 1;

 當線程執行這個語句時,會先從主存當中讀取i的值,而後複製一份到高速緩存當中,而後CPU執行指令對i進行加1操做,而後將數據寫入高速緩存,最後將高速緩存中i最新的值刷新到主存當中。這個代碼在單線程中運行是沒有任何問題的,可是在多線程中運行就會有問題了。在多核CPU中,每條線程可能運行於不一樣的CPU中,所以每一個線程運行時有本身的高速緩存(對單核CPU來講,其實也會出現這種問題,只不過是以線程調度的形式來分別執行的)。本文咱們以多核CPU爲例。好比同時有2個線程執行這段代碼,假如初始時i的值爲0,那麼咱們但願兩個線程執行完以後i的值變爲2。可是事實會是這樣嗎?

可能存在下面一種狀況:初始時,兩個線程分別讀取i的值存入各自所在的CPU的高速緩存當中,而後線程1進行加1操做,而後把i的最新值1寫入到內存。此時線程2的高速緩存當中i的值仍是0,進行加1操做以後,i的值爲1,而後線程2把i的值寫入內存。最終結果i的值是1,而不是2。這就是著名的緩存一致性問題。一般稱這種被多個線程訪問的變量爲共享變量。也就是說,若是一個變量在多個CPU中都存在緩存(通常在多線程編程時纔會出現),那麼就可能存在緩存不一致的問題。

  爲了解決緩存不一致性問題,一般來講有如下2種解決方法:

  1)經過在總線加LOCK#鎖的方式

  2)經過緩存一致性協議

  這2種方式都是硬件層面上提供的方式。

  在早期的CPU當中,是經過在總線上加LOCK#鎖的形式來解決緩存不一致的問題。由於CPU和其餘部件進行通訊都是經過總線來進行的,若是對總線加LOCK#鎖的話,也就是說阻塞了其餘CPU對其餘部件訪問(如內存),從而使得只能有一個CPU能使用這個變量的內存。好比上面例子中 若是一個線程在執行 i = i +1,若是在執行這段代碼的過程當中,在總線上發出了LCOK#鎖的信號,那麼只有等待這段代碼徹底執行完畢以後,其餘CPU才能從變量i所在的內存讀取變量,而後進行相應的操做。這樣就解決了緩存不一致的問題。

可是上面的方式會有一個問題,因爲在鎖住總線期間,其餘CPU沒法訪問內存,致使效率低下。

因此就出現了緩存一致性協議。最出名的就是Intel 的MESI協議,MESI協議保證了每一個緩存中使用的共享變量的副本是一致的。它核心的思想是:當CPU寫數據時,若是發現操做的變量是共享變量,即在其餘CPU中也存在該變量的副本,會發出信號通知其餘CPU將該變量的緩存行置爲無效狀態,所以當其餘CPU須要讀取這個變量時,發現本身緩存中緩存該變量的緩存行是無效的,那麼它就會從內存從新讀取。

關於java內存模型,我有空會單獨寫一篇文章進行總結,這裏僅僅淺談一下。下面,咱們再來談談鎖對象條件對象synchronized關鍵字

2.鎖對象

有兩種機制防止代碼塊受併發訪問的干擾。Java語言提供了一個synchronized關鍵字達到這一目的,而且JavaSE 5.0引入了ReentrantLock類。

咱們先看看ReentrantLock:

java.util.concurrent.locks.ReentrantLock  5.0 已實現的接口:Serializable, Lock

咱們再來看看Lock接口: java.util.concurrent.locks.Lock 5.0,該接口下有2個方法:

(1) void lock() 獲取這個鎖:若是鎖同時被另外一個線程擁有則發生阻塞。

(2)void unlock() 釋放這個鎖。

讓咱們使用一個鎖來保護Bank類的transfer方法。下面咱們來看看3個類:

 1 package unsynch;
 2 
 3 import java.util.concurrent.locks.Lock;
 4 import java.util.concurrent.locks.ReentrantLock;
 5 
 6 /**
 7  * A bank with a number of bank accounts.
 8  * @version 1.30 2004-08-01
 9  * @author Cay Horstmann
10  */
11 public class Bank
12 {
13    private final double[] accounts;
14    private Lock bankLock = new ReentrantLock();
15    /**
16     * Constructs the bank.
17     * @param n the number of accounts
18     * @param initialBalance the initial balance for each account
19     */
20    public Bank(int n, double initialBalance)
21    {
22       accounts = new double[n];
23       for (int i = 0; i < accounts.length; i++)
24          accounts[i] = initialBalance;
25    }
26 
27    /**
28     * Transfers money from one account to another.
29     * @param from the account to transfer from
30     * @param to the account to transfer to
31     * @param amount the amount to transfer
32     */
33    public void transfer(int from, int to, double amount)
34    {
35       bankLock.lock();
36       try{
37       if (accounts[from] < amount) return;
38       System.out.print(Thread.currentThread());
39       accounts[from] -= amount;
40       System.out.printf(" %10.2f from %d to %d", amount, from, to);
41       accounts[to] += amount;
42       System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
43       }
44       finally{
45           bankLock.unlock();
46       }
47       
48    }
49 
50    /**
51     * Gets the sum of all account balances.
52     * @return the total balance
53     */
54    public double getTotalBalance()
55    {
56       double sum = 0;
57 
58       for (double a : accounts)
59          sum += a;
60 
61       return sum;
62    }
63 
64    /**
65     * Gets the number of accounts in the bank.
66     * @return the number of accounts
67     */
68    public int size()
69    {
70       return accounts.length;
71    }
72 }
View Code
 1 package unsynch;
 2 
 3 /**
 4  * A runnable that transfers money from an account to other accounts in a bank.
 5  * @version 1.30 2004-08-01
 6  * @author Cay Horstmann
 7  */
 8 public class TransferRunnable implements Runnable
 9 {
10    private Bank bank;
11    private int fromAccount;
12    private double maxAmount;
13    private int DELAY = 10;
14 
15    /**
16     * Constructs a transfer runnable.
17     * @param b the bank between whose account money is transferred
18     * @param from the account to transfer money from
19     * @param max the maximum amount of money in each transfer
20     */
21    public TransferRunnable(Bank b, int from, double max)
22    {
23       bank = b;
24       fromAccount = from;
25       maxAmount = max;
26    }
27 
28    public void run()
29    {
30       try
31       {
32          while (true)
33          {
34             int toAccount = (int) (bank.size() * Math.random());
35             double amount = maxAmount * Math.random();
36             bank.transfer(fromAccount, toAccount, amount);
37             Thread.sleep((int) (DELAY * Math.random()));
38          }
39       }
40       catch (InterruptedException e)
41       {
42       }
43    }
44 }
View Code
 1 package unsynch;
 2 
 3 /**
 4  * This program shows data corruption when multiple threads access a data structure.
 5  * @version 1.30 2004-08-01
 6  * @author Cay Horstmann
 7  */
 8 public class UnsynchBankTest
 9 {
10    public static final int NACCOUNTS = 100;
11    public static final double INITIAL_BALANCE = 1000;
12 
13    public static void main(String[] args)
14    {
15       Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);
16       int i;
17       for (i = 0; i < NACCOUNTS; i++)
18       {
19          TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);
20          Thread t = new Thread(r);
21          t.start();
22       }
23    }
24 }
View Code

 這段程序模擬一個有若干帳戶的銀行。隨機地生成在這些帳戶之間轉移錢款的交易。每個帳戶有一個線程。每一筆交易中,會從線程所服務的帳戶中隨機轉移必定數目的錢款到另外一個隨機帳戶。嘗試一下,添加加鎖代碼到transfer方法而且再次運行程序,你永遠能夠運行它,而銀行的餘額不會出現訛誤。

假定一個線程調用transfer,在執行結束前被剝奪了運行權。假定第二個線程也調用transfer,因爲第二個線程不能得到鎖,將在調用lock方法時被阻塞。他必須等待第一個線程完成transfer方法的執行以後才能再度被激活。當第一個線程釋放鎖時,那麼第二個線程才能開始運行。

注意每個Bank對象有本身的ReentrantLock對象。若是兩個線程試圖訪問同一個Bank對象,那麼鎖以串行方式提供服務。可是,若是兩個線程訪問不一樣的Bank對象,每個線程獲得不一樣的鎖對象,兩個線程都不會發生阻塞。本該如此,由於線程在操縱不一樣的Bank實例的時候,線程之間不會互相影響。

鎖是可重入的,由於線程能夠重複地得到已經持有的鎖。鎖保持一個持有計數(hhldcount)來跟蹤對lock方法的嵌套調用。線程在每一次調用lock都要調用unlock來釋放鎖。因爲這一特性,被一個鎖保護的代碼能夠調用另外一個使用相同鎖的方法。例如,transfer方法調用getTotalBalance方法,這也會封鎖bankLock對象,此時bankLock對象的持有計數爲2。當getTotalBalance方法退出的時候,持有計數變回1。當transfer方法退出的時候,持有計數變爲0。線程釋放鎖。

警告:把解鎖操做括在finally子句以內是相當重要的。若是臨界區的代碼拋出異常,鎖必須被釋放。不然,其它線程將永遠被阻塞。

3.條件對象

條件對象常常被稱爲條件變量。一個鎖對象能夠有一個或多個相關的條件對象。你能夠用newCondition方法得到一個條件對象。

下面咱們再來看看Java API 中的Conditon接口的定義:java.util.concurrent.locks.Condition 5.0,它有幾個方法:

void await() 將該線程放到條件的等待集中。

void signalAll() 解除該條件的等待集中的全部線程的阻塞狀態(經過競爭)

void signal()  從該條件的等待集中隨機地選擇一個線程,解除其阻塞狀態。

下面咱們經過一個代碼的示例來講明這個條件對象如何使用:

 1 package synch;
 2 
 3 import java.util.concurrent.locks.*;
 4 
 5 /**
 6  * A bank with a number of bank accounts that uses locks for serializing access.
 7  * @version 1.30 2004-08-01
 8  * @author Cay Horstmann
 9  */
10 public class Bank
11 {
12    private final double[] accounts;
13    private Lock bankLock;
14    private Condition sufficientFunds;
15 
16    /**
17     * Constructs the bank.
18     * @param n the number of accounts
19     * @param initialBalance the initial balance for each account
20     */
21    public Bank(int n, double initialBalance)
22    {
23       accounts = new double[n];
24       for (int i = 0; i < accounts.length; i++)
25          accounts[i] = initialBalance;
26       bankLock = new ReentrantLock();
27       sufficientFunds = bankLock.newCondition();
28    }
29 
30    /**
31     * Transfers money from one account to another.
32     * @param from the account to transfer from
33     * @param to the account to transfer to
34     * @param amount the amount to transfer
35     */
36    public void transfer(int from, int to, double amount) throws InterruptedException
37    {
38       bankLock.lock();
39       try
40       {
41          while (accounts[from] < amount)
42             sufficientFunds.await();
43          System.out.print(Thread.currentThread());
44          accounts[from] -= amount;
45          System.out.printf(" %10.2f from %d to %d", amount, from, to);
46          accounts[to] += amount;
47          System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
48          sufficientFunds.signalAll();
49       }
50       finally
51       {
52          bankLock.unlock();
53       }
54    }
55 
56    /**
57     * Gets the sum of all account balances.
58     * @return the total balance
59     */
60    public double getTotalBalance()
61    {
62       bankLock.lock();
63       try
64       {
65          double sum = 0;
66 
67          for (double a : accounts)
68             sum += a;
69 
70          return sum;
71       }
72       finally
73       {
74          bankLock.unlock();
75       }
76    }
77 
78    /**
79     * Gets the number of accounts in the bank.
80     * @return the number of accounts
81     */
82    public int size()
83    {
84       return accounts.length;
85    }
86 }
View Code
 1 package synch;
 2 
 3 /**
 4  * This program shows how multiple threads can safely access a data structure.
 5  * @version 1.30 2004-08-01
 6  * @author Cay Horstmann
 7  */
 8 public class SynchBankTest
 9 {
10    public static final int NACCOUNTS = 100;
11    public static final double INITIAL_BALANCE = 1000;
12 
13    public static void main(String[] args)
14    {
15       Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);
16       int i;
17       for (i = 0; i < NACCOUNTS; i++)
18       {
19          TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);
20          Thread t = new Thread(r);
21          t.start();
22       }
23    }
24 }
View Code
 1 package synch;
 2 
 3 /**
 4  * A runnable that transfers money from an account to other accounts in a bank.
 5  * @version 1.30 2004-08-01
 6  * @author Cay Horstmann
 7  */
 8 public class TransferRunnable implements Runnable
 9 {
10    private Bank bank;
11    private int fromAccount;
12    private double maxAmount;
13    private int DELAY = 10;
14 
15    /**
16     * Constructs a transfer runnable.
17     * @param b the bank between whose account money is transferred
18     * @param from the account to transfer money from
19     * @param max the maximum amount of money in each transfer
20     */
21    public TransferRunnable(Bank b, int from, double max)
22    {
23       bank = b;
24       fromAccount = from;
25       maxAmount = max;
26    }
27 
28    public void run()
29    {
30       try
31       {
32          while (true)
33          {
34             int toAccount = (int) (bank.size() * Math.random());
35             double amount = maxAmount * Math.random();
36             bank.transfer(fromAccount, toAccount, amount);
37             Thread.sleep((int) (DELAY * Math.random()));
38          }
39       }
40       catch (InterruptedException e)
41       {
42       }
43    }
44 }
View Code

這段代碼顯然比上段代碼多了一些東西,爲何要多這些東西呢?咱們這麼作是爲了細化銀行的模擬程序。咱們避免選擇沒有足夠資金的帳戶做爲轉出帳戶。
若是transfer方法發現餘額不足,它調用sufficientFunds.await();當前線程如今被阻塞了,並放棄了鎖。一旦一個線程調用await方法,它進入該條件的等待集。當鎖可用時,該線程不能立刻解除阻塞。相反,它處於阻塞狀態,直到另外一個線程調用同一條件上的signalAll方法時爲止。

當另外一個線程轉帳是,它應該調用sufficientFunds.signalAll();

這一調用從新激活由於這一條件而等待的全部線程。當這些線程從等待集當中移出時,它們再次成爲可行的,調度器將再次激活它們。同時,它們將試圖從新進入該對象。一旦鎖成爲可用的,它們中的某個將從await調用返回,得到該鎖並從被阻塞的地方繼續執行。

此時,線程應該再次測試該條件。因爲沒法確保該條件被知足——signalAll方法僅僅是通知正在等待的線程:此時有可能已經知足條件,值得再次去檢測該條件。

 最後,有一點須要注意:當一個線程調用await()時,它沒有辦法從新激活自身。它寄但願於其餘線程。若是沒有其餘線程來從新激活等待的線程,它就永遠再也不運行了。這將致使使人不快的死鎖(deadlock)現象。總結一下:每一個條件對象管理那些已經進入被保護的代碼段但還不能運行的線程。

4.synchronized關鍵字

大多數狀況下,咱們並不須要Lock和Condition接口爲程序設計人員提供的高度的鎖定控制。從1.0版本開始,java中的每個對象都有一個內部鎖。若是一個方法用synchronized關鍵字聲明,那麼對象的鎖將保護整個方法。也就是說,要調用該方法,線程必須得到內部的對象鎖。

換句話說,

public synchronized void method()       等價於    public void method()

{                                                                   {

method body                                                       this.intrinsicLock.lock();

}                                                                        try

                                                                               {

                                                                                     method body

                                                                                }

                                                                               finally{this.intrinsicLock.unlock();}

                                                                       }

例如,能夠簡單地聲明Bank類的transfer方法爲synchronized,而不是使用一個顯式的鎖。

一樣的,下面咱們經過一段代碼來理解synchronized關鍵字。

 1 package synch2;
 2 
 3 /**
 4  * A bank with a number of bank accounts that uses synchronization primitives.
 5  * @version 1.30 2004-08-01
 6  * @author Cay Horstmann
 7  */
 8 public class Bank
 9 {
10    private final double[] accounts;
11 
12    /**
13     * Constructs the bank.
14     * @param n the number of accounts
15     * @param initialBalance the initial balance for each account
16     */
17    public Bank(int n, double initialBalance)
18    {
19       accounts = new double[n];
20       for (int i = 0; i < accounts.length; i++)
21          accounts[i] = initialBalance;
22    }
23 
24    /**
25     * Transfers money from one account to another.
26     * @param from the account to transfer from
27     * @param to the account to transfer to
28     * @param amount the amount to transfer
29     */
30    public synchronized void transfer(int from, int to, double amount) throws InterruptedException
31    {
32       while (accounts[from] < amount)
33          wait();
34       System.out.print(Thread.currentThread());
35       accounts[from] -= amount;
36       System.out.printf(" %10.2f from %d to %d", amount, from, to);
37       accounts[to] += amount;
38       System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
39       notifyAll();
40    }
41 
42    /**
43     * Gets the sum of all account balances.
44     * @return the total balance
45     */
46    public synchronized double getTotalBalance()
47    {
48       double sum = 0;
49 
50       for (double a : accounts)
51          sum += a;
52 
53       return sum;
54    }
55 
56    /**
57     * Gets the number of accounts in the bank.
58     * @return the number of accounts
59     */
60    public int size()
61    {
62       return accounts.length;
63    }
64 }
View Code

尤爲須要注意的是,內部對象鎖只有一個相關條件,多是不夠的!在代碼中應該使用哪種?Lock和Condition對象仍是同步方法?下面是一些建議。
1)最好既不使用Lock/Condition也不使用synchronized關鍵字。在許多狀況下你可使用java.util.concurrent包中的一種機制,它會爲你處理全部的加鎖。

2)若是synchronized關鍵字適合你的程序,那麼請儘可能使用它,這樣能夠減小編碼的代碼量,減小出錯的概率。

3)若是特別須要Lock/Condition結構提供的獨有特性時,才使用Lock/Condition。

如上所述,內部對象只有一個相關條件。wait方法添加一個線程到等待集中,notifyAll/notify方法解除等待線程的阻塞狀態。換句話說,調用wait或notifyAll等價於

intrinsicCondition.await();

intrinsicCondition.signalAll();

註釋:wait,notifyAll以及notify方法是Object類的final方法。Condition方法必須被命名爲await,signalAll和signal以便它們不會與那些方法發生衝突。

下面,咱們再來看看java.lang.Object內的幾個相關方法:

  • void notifyAll()

解除那些在該對象上調用wait方法的線程的阻塞狀態。該方法只能在同步方法或同步塊內部調用。若是當前線程不是對象鎖的持有者,該方法拋出一IllegalMonitorStateException異常。

  • void notify()

隨機選擇一個在該對象上調用wait方法的線程,解除其阻塞狀態。該方法只能在一個同步方法或同步塊中調用,若是當前線程不是對象鎖的持有者,該方法拋出一個IllgalMonitorStateException異常。

  • void wait()

致使線程進入等待狀態只到它被通知。該方法只能在一個同步方法中調用。若是當前線程不是對象鎖的持有者,該方法拋出一個IllegalMonitorStateException異常。

  • void wait(long millis)
  • void wait(long millis,int nanos)

須要尤爲注意以上紅色字體部分。這裏有2層意思:(1)這意味着,使用wait(),notifyAll(),notify()時,必須使用synchronized關鍵字!(2)然而,使用synchronized時,未必會用wait()等方法。synchronized方法只是讓線程排隊,就是同步代碼塊,可是排隊後一個線程得到內部鎖後,未必就知足繼續執行下去的條件!因此,考慮到餘額不足時要阻塞,就必須使用wait(),若是要考慮多個條件,則要考慮使用Lock/Conditon了。

5.同步阻塞

每個java對象有一個鎖,線程能夠經過調用同步方法得到鎖,還有一種機制能夠得到鎖,經過進入一個同步阻塞,即同步塊!咱們有時會遇到以下「特殊的」鎖,例如:

public class Bank

{

    private double [] accounts;

    private Object lock = new Object();

    ...

    public void transfer (int from,int to,int amount)

     {

        synchronized(lock)

         {

            accounts[from] -=amount;

            accounts[to] += amount;

         }

      System.out.println(...);

     }

}

在此,lock對象被建立僅僅是用來使用每一個java對象持有的鎖。程序猿使用一個對象的鎖來實現額外的原子操做,實際上成爲客戶端鎖定

客戶端鎖定是很是脆弱的,一般不推薦使用。

----------------------------------------------我是分割線-------------------------------------------------

到這裏,同步第一部分講完了。寫這一部分整整用了我三個晚上!確實,寫博客是個慢工夫,可是印象深入,脈絡清晰。看着《java核心技術卷》厚厚一本,而我進度如蝸牛!還有不少事要作,特別忙,真是心急如焚。這是個很是蛋疼的問題,然而不積跬步無以致千里,這是做爲一個優秀程序猿的必經之路,望君加油!

相關文章
相關標籤/搜索