在多线程应用中,两个或者两个以上的线程需要共享对同一个数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了修改该对象的方法,这种情况通常被称为竞争条件。竞争条件最容易理解的例子如下:比如电影院售卖电影票,电影票数量是一定的,但卖电影票的窗口到处都有,每个窗口就相当于一个线程。这么多的线程共用所有的电影票资源,如果不使用同步是无法保证其原子性的。在一个时间点上,两个线程同时使用电影票资源,那其取出的电影票是一样的(座位号一样),这样就会给顾客造成麻烦。解决方法如下:当一个线程要使用电影票这个资源时,我们就交给它一把锁,等它把事情做完后再把锁给另一个要用这个资源的线程。这样就不会出现上述情况了。
一、重入锁与条件对象
重入锁 ReentrantLock 就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。用 ReentrantLock 保护代码块的结构如下所示:
Lock lock = new ReentrantLock();
lock.lock();
try {
...
} finally {
lock.unlock();
}
这一结构确保任何时刻只有一个线程进入临界区,临界区就是在同一时刻只能有一个任务访问的代码区。一旦一个线程封锁了锁对象,其他任何线程都无法进入 Lock 语句。把解锁的操作放在 finally 中是十分必要的。如果在临界区发生了异常,锁是必须要释放的,否则其他线程将会永远被阻塞。进入临界区时,却发现在某一个条件满足之后,它才能执行。这时可以使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程,条件对象又被称作条件变量。通过下面的例子来说明为何需要条件对象。假设一个场景需要用银行转账。我们首先写了银行的类,它的构造方法需要传入银行账户的数量和每个账户的账户金额。
public class Bank {
private double[] accounts;
private Lock bankLock;
public Bank(int number, double money) {
accounts = new double[number];
bankLock = new ReentrantLock();
for (int i = 0; i < accounts.length; i++) {
accounts[i] = money;
}
}
}
接下来我们要转账,写一个转账的方法,from 是转账方,to 是接收方,amount 是转账金额,如下所示:
public void transfer(int from, int to, int amount) {
bankLock.lock();
try {
while (accounts[from] < amount) {
// wait
}
} finally {
bankLock.unlock();
}
}
结果我们发现转账方余额不足;如果有其他线程给这个转账方再转足够的钱,就可以转账成功了。但是这个线程已经获取了锁,它具有排他性,别的线程无法获取锁来进行存款操作,这就是我们需要引入条件对象的原因。一个锁对象拥有多个相关的条件对象,可以用 newCondition 方法获得一个条件对象,我们得到条件对象后调用 await 方法,当前线程就被阻塞了并放弃了锁。整理以上代码,加入条件对象,代码如下所示:
public class Bank {
private double[] accounts;
private Lock bankLock;
private Condition condition;
public Bank(int number, double money) {
accounts = new double[number];
bankLock = new ReentrantLock();
// 得到条件对象
condition = bankLock.newCondition();
for (int i = 0