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核心技术卷》厚厚一本,而我进度如蜗牛!还有不少事要作,特别忙,真是心急如焚。这是个很是蛋疼的问题,然而不积跬步无以致千里,这是做为一个优秀程序猿的必经之路,望君加油!

相关文章
相关标签/搜索