什么是线程同步?
当使用多个线程来访问同一个数据时,非常容易出现线程安全问题(比如多个线程都在操作同一数据导致数据不一致),所以我们用同步机制来解决这些问题。
锁和条件的关键之处:
-
锁用来保护代码片段,任意时刻只能有一个线程执行被保护的代码。
-
锁可以管理试图进入保护代码片段的线程
-
锁可以拥有一个或者多个相关的条件对象
-
每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
锁对象
在Java SE5.0引入ReentrantLock类。Lock是
Java
.util.concurrent.locks包下的接口,
Lock
实现提供了比使用
synchronized
方法和语句可获得的更广泛的锁定操作。
用ReentrantLock保护代码块的基本结构如下:
myLock.lock(); //a ReentrantLock object
try
{
critical section
}
finally
{
myLock.unlock();//确保代码抛出异常锁必须被释放
}
这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,他们被阻塞,直到第一个线程释放锁对象。把解锁操作放在finally子句之内是至关重要的。如果在临界区的代码抛出异常,锁必须释放。否则,其他线程将永远阻塞。
如果使用锁,就不能使用带资源的try语句。首先解锁方法名不是close。不过即使将他重命名,带资源的try语句也无法正常工作。他的首部希望声明一个新变量。但如果使用一个锁,你可能想使用多个线程共享的那个变量,而不是新变量。
public class Bank
{
private Lock bankLock <span style="font-size:18px;"><span class="FrameTitleFont" size="+1"></span></span>= new ReentrantLock();//<span style="font-size:18px;"><span class="FrameTitleFont" size="+1"></span></span>ReentrantLock 实现了Lock接口
........
public void transfer(int from, int to, double amount)
{
bankLock.lock();
try
{
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
}
finally
{
bankLock.unlock();
}
}
public double getTotalBalance()
{
bankLock.lock();
try
{
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
finally
{
bankLock.unlock();
}
}
}
假定一个线程调用transfer,在执行结束前被剥夺运行权。假定第二个线程也调用了transfer,由于第二个线程并不能获得锁,将在调用lock方法时被阻塞。他必须等待第一个线程完成transfer方法之后才能再度被激活。当第一个线程释放锁时,第二个线程才能开始运行。这样余额就不会出错了。
每个Bank对象有自己的ReentrantLock对象,如果两个线程试图访问同一个Bank对象,那么锁以串行方式提供服务。但是,如果两个线程访问不同的Bank对象,每个线程得到不同的锁对象,两个线程都不会发生阻塞。两个线程在操作不同的Bank实例的时候,线程之间不会相互影响。
锁是
可重入的
,因为线程可以重复的获得已经持有的锁。锁保持一个持有计数来跟踪对Lock方法的嵌套调用。线程每一次调用都用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同锁的方法。例如,transfer方法调用getTotalBalance方法,这也会封锁bankLock对象,此时bankLock对象的持有计数为2,当
getTotalBalance方法
退出时,持有计数变为1,当transfer方法退出时,持有计数变为0。线程释放锁。通常,可能想要保护需若干个操作来更新或者检查共享对象的代码块。要确保这些操作完成后,另一个线程才能使用相同的对象。
要留心临界区的代码,不要因为异常的抛出二跳出了临界区。如果在临界区代码结束之前抛出了异常,finally字句释放锁,但会使对象可能出于一种受损状态。
条件对象
通常,线程进入临界区,却发现在某一条件满足后才能执行。要使用一个条件对象来管理那些已经获得了一个锁,但是不能做有用工作的线程。(条件对象经常被称为条件变量)
分析上文中的银行模拟程序,
if(bank.getBalance(from)>=amount)
transfer(from, to, amount);
如果当前程序通过if条件判断,且在调用transfer之前被中断,在线程再次运行前,账户余额可能已经低于提款金额。必须确保没有其他线程在本检查余额与转账活动之间修改余额。通过使用锁来保护检查与转账动作来做到这一点:
public void transfer(int from, int to, double amount)
{
bankLock.lock();
try
{
while (accounts[from] < amount)
{
//wait()
}
//transfer funds
........
finally
{
bankLock.unlock();
}
}
现在,当账户中没有足够的余额时,等待直到另一个线程向账户中注入资金。但是,这一线程刚刚获得了bankLock的排他性访问,因此别的线程没有进行存款操作的机会,这就是为什么需要用条件对象的原因。
一个锁对象可以有一个或者多个相关的条件对象。可以用newCondition方法获得一个条件对象。习惯的给每一个条件对象命名为可以反应它所表达条件的名字。如sufficientFunds = bankLock.newCondition();
如果transfer方法发现余额不足,它调用sufficientFunds.await();当前线程被他阻塞了,并放弃锁。我们希望这样可以等待另一个线程进行增加账户余额的操作。
等待获得锁的线程和调用await方法的线程在本质上存在不同。一旦一个线程调用await方法,他进入该条件的等待集。当该锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的signalAll方法时为止。
当另一个线程转账时,它应该调用sufficientFunds.signalAll();这一调用重新激活因为这一条件等待的所有线程。当这些线程从等待集当中移除时,他们再次成为可运行的,调度器将再次激活它们。同时,他们试图重新进入该对象。一旦锁成为可用,他们将从await调用返回,获得该锁并从被阻塞的地方继续执行。
此时,线程应该再次测试该条件。由于无法确保该条件被满足,signalAll方法仅仅通知正在等待的线程:此时有可能已经满足条件,值得再次去检测条件。
最关重要的是最终需要某个其他线程调用signalAll方法。当一个线程调用await时,他没有办法自己激活自身,它寄希望于其他线程。如果没有其他线程重新来激活等待的线程,他就永远不再运行。导致死锁。如果所有其他线程被阻塞,最后一个线程再解除其他阻塞线程之前就调用await,那么它也被阻塞。没有线程解除其他阻塞线程,那么该程序就挂起。
应该何时调用signalAll呢,在本例中,当一个账户的余额发生改变时,等待的线程就有机会检查余额。调用signalAll不会立即激活一个等待线程。它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程同步推出后,通过竞争实现对对象的访问。当一个线程拥有某个条件的锁时,它仅仅可以在该条件上调用await,signalAll和signal方法。
以下为完整的例子:
synch/Bank.java
package synch;
import java.util.concurrent.locks.*;
public class Bank
{
private final double[] accounts;
private Lock bankLock;
private Condition sufficientFunds;
public Bank(int n, double initialBalance)
{
accounts = new double[n];
for (int i = 0; i < accounts.length; i++)
accounts[i] = initialBalance;
bankLock = new ReentrantLock();
sufficientFunds = bankLock.newCondition();
}
public void transfer(int from, int to, double amount) throws InterruptedException
{
bankLock.lock();
try
{
while (accounts[from] < amount)
sufficientFunds.await();
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
sufficientFunds.signalAll();
}
finally
{
bankLock.unlock();
}
}
public double getTotalBalance()
{
bankLock.lock();
try
{
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
finally
{
bankLock.unlock();
}
}
public int size()
{
return accounts.length;
}
}
synch/TransferRunnable.java
package synch;
public class TransferRunnable implements Runnable
{
private Bank bank;
private int fromAccount;
private double maxAmount;
private int DELAY = 10;
public TransferRunnable(Bank b, int from, double max)
{
bank = b;
fromAccount = from;
maxAmount = max;
}
public void run()
{
try
{
while (true)
{
int toAccount = (int) (bank.size() * Math.random());
double amount = maxAmount * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
}
}
catch (InterruptedException e)
{
}
}
}
synch/SynchBankTest
package synch;
public class SynchBankTest
{
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static void main(String[] args)
{
Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);
int i;
for (i = 0; i < NACCOUNTS; i++)
{
TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);
Thread t = new Thread(r);
t.start();
}
}
}