返回 登录
0

Java多线程——同步

学习Java的同学注意了!!!
学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群,群号码:492139965 我们一起学Java!

1.竞争条件

首先,我们回顾一下《Java核心技术卷》里讲到的多线程的“竞争条件”。由于各线程访问数据的次序,可能会产生讹误的现象,这样一个情况通常称为“竞争条件”。

那么,讹误具体是怎么产生的呢?本质上,是由于操作的非原子性。比如,假定两个线程同时执行指令 account[to] += amount;该指令可能会被处理如下:

1)将account[to]加载到寄存器。

2)增加amount[to]。

3)将结果写回account[to]。

现在,假定第一个线程执行步骤1和2,然后,它被剥夺了运行权。假定第二个线程被唤醒并修改了accounts数组中的同一项。然后,第1个线程被唤醒并完成第3步。这样,这一动作擦去了第二个线程所做的更新。于是,总金额不再正确。

———————————————我是分割线———————————————————————————————

好,我们再从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(" .2f from %d to %d", amount, from, to);
41       accounts[to] += amount;
42       System.out.printf(" Total Balance: .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

学习Java的同学注意了!!!
学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群,群号码:492139965 我们一起学Java!

评论