Java的多线程之同步篇一:锁对象、条件对象

时间:2022-04-20 13:05:10

一、多线程的同步

根据各线程访问数据的次序,可能会产生讹误的对象。这样一种情况通常称为竞争条件。

1>同步存取(以银行转账为例:)

模拟一个有若干个账户的银行,随机地生成在这些账户之间转换钱款的交易。每个账户有一个线程。每笔交易中,会从线程所服务的账户中随机转移一定数目的钱款到另一个随机账户。

提示:银行内部的转账交易,银行的总金额应保持不变。

代码如下:

package com.thread.bank;

//银行类提供各种基础的服务
public class Bank {

private final double[] accounts;

public Bank(int n, double initialBalance) {
accounts = new double[n];
for (int i = 0; i < accounts.length; i++) {
accounts[i] = initialBalance;
}
}

public void transfer(int from, int to, double amount) {
if (accounts[from] < amount)
return;
System.out.println(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());
}

public double getTotalBalance() {
double sum = 0;
for (double a : accounts) {
sum += a;
}
return sum;
}

public int size(){
return accounts.length;
}
}
package com.thread.bank;//业务类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;}@Overridepublic 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) {System.out.println("服务中断");}}}
package com.thread.bank;public class UnsynchBankTest {private static final int NACCOUNTS = 100;private static final double INITIAL_BALANCE = 1000;public static void main(String[] args) {Bank b = new Bank(NACCOUNTS,INITIAL_BALANCE);for (int j = 0; j < NACCOUNTS; j++) {TransferRunnable r = new TransferRunnable(b, j, INITIAL_BALANCE);Thread t = new Thread(r);t.start();}}}
现状:总金额不断变化。因为数据之间存在竞争条件,所以必须实现同步存取。

当两个线程试图同时更新同一个账户的时候,就会出现以上问题。假定两个线程同时执行指令

accounts[to]+=amount;

问题在于这不是原子操作。该指令可能被处理如下:

a>将accounts[to]加载到寄存器

b>增加amount

c>将结果写回accounts[to]

现在假定第一个线程执行步骤1和2,然后被剥夺了运行权。假定第二个线程被唤醒修改了accounts数组中的同一项。然后,第一个线程被唤醒并完成其第3步。

这样,这一动作擦去了第二线程所做的更新。于是,总金额不在正确。

Java的多线程之同步篇一:锁对象、条件对象

2>锁对象

有两种机制防止代码块受并发访问的干扰。JAVA语言提供一个synchronized关键字达到这一目的,并且Java SE 5.0引入了ReentrantLock类。java.util.concurrent框架为这些基础机制提供独立的类。

a>ReentrantLock保护代码块的基本结构如下:(可重入锁)

myLock.lock();
try{
critical section
}
finally
{
myLock.unlock();
}
这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程*了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。

警告:把解锁操作括在finally子句之内是至关重要的。如果在临界区代码抛出异常,锁必须被释放。否则,其他线程将永远阻塞。

注:如果使用锁,就不能使用带资源的try语句。首先,解锁方法名不是close。不过,即使将它重命名,带资源的try语句也无法正常工作。它的首部希望声明一个新变量。但是如果使用一个锁,你可能想使用多个线程共享的那个变量(而不是新变量)。

实例如下:

package com.thread.bank;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

//银行类提供各种基础的服务
public class Bank {
private Lock bankLock = new ReentrantLock();
...

public void transfer(int from, int to, double amount) {
bankLock.lock();
try {
System.out.println(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();
}
}
}
假定一个线程调用transfer,在执行结束前被剥夺了运行权。假定第二个线程也调用transfer,由于第二个线程不能获得锁,将在调用lock方法时被阻塞。它必须等待第一个线程完成transfer方法的执行之后才能再度被激活。当第一个线程释放锁时,那么第二个线程才能运行。

注:每一个Bank对象有自己的ReentrantLock对象。如果两个线程试图访问同一个Bank对象,那么锁以串行方式提供服务。但是,如果两个线程访问不同的Bank对象,每一个线程得到不同的锁对象,两个线程都不会发生阻塞。

锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数(holdcount)来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。

例如,transfer方法调用getTotalBalance方法,这也会*bankLock对象,此时bankLock对象的持有计数为2。当getTotalBalance方法退出的时候,持有计数变回1。当transfer方法退出的时候,持有计数变为0。线程释放锁。

通常,可能想要保护需若干个操作来更新或检查共享对象的代码块。要确保这些操作完成后,另一个线程才能使用相同的对象。

常用方法:lock()          获得这个锁,如果锁同时被另一个线程拥有则发生阻塞。

                 unlock()      释放这个锁。

                 ReentrantLock() 构建一个可以被用来保护临界区的可重入锁。

3>条件对象

通常,线程进入临界区,却发现在某一个条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。这就是Java库中条件对象的实现。(由于历史的原因,条件对象经常被称为条件变量)

还是以银行的模拟程序为例:

避免选择没有足够资金的账户作为转出账户。注意不能使用下面这样的代码:

if(bank.getBalance(from)>=account)
bank.transfer(from,to,amount);
因为当前线程完全有可能在完成测试、调用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方法获得一个条件对象。习惯上给每一个条件对象命名为可以反映它所表达的条件的名字。例如,在此设置一个条件对象来表达“余额充足”条件。

class Bank{
private Condition sufficientFunds;
...
public Bank(){
...
sufficientFunds = bankLock.newCondition();
}
}
如果transfer方法发现余额不足,它调用sufficientFunds.await();

       当前线程现在被阻塞了,并放弃了锁。我们希望这样可以使得另一个线程可以进行增加账户余额的操作。

       等待获得锁的线程和调用await方法的线程存在本质上的不同。一旦一个线程调用await方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的signalAll方法时为止。

       当另一个线程转账时,它应该调用sufficientFunds.signalAll();

       这一调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集当中移出时,它们再次成为可运行的,调度器将再次激活它们。同时,它们将试图重新进入该对象。一旦锁成为可用,它们中的某个将从await调用返回,获得该锁并从阻塞的地方继续执行。

       此时,线程应该再次测试该条件。由于无法确保该条件被满足--------signalAll方法仅仅是通知正在等待的线程:此时有可能已经满足条件,值得再次去检测该条件。
注:通常,对await的调用应该在如下形式的循环体中

while(!(ok to proceed))
condition.await();

a>死锁

       至关重要的是最终需要某个其他线程调用signalAll方法。当一个线程调用await方法时,它没办法重新激活自身。它寄希望与其他线程。如果没有其他线程来重新激活等待的线程,它将永远不再运行了。这将导致死锁(deadlock)现象。

注:signalAll方法不会立即激活一个等待线程,它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问。

       当一个线程拥有某个条件的锁时,它仅仅可以在该条件上调用await、signalAll或signal方法。

修改后的代码如下:

package com.thread.bank;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

//银行类提供各种基础的服务
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.println(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;
}

}
在getTotalBalance使用锁的原因:当在transfer方法之外调用getTotalBalance方法时,如果进行途中被剥夺了运行权,而由于未使用锁,导致其他的线程可以修改accounts中的数据。当未被计数的数据被修改,从而导致最后的计数结构可能变化,但实际accounts中的数据总和还是不变的。
常用方法: Condition newCondition() 返回一个与该锁相关的条件对象

                  void await()  将该线程放到条件的等待集中

                  void signalAll() 解除该条件的等待集中的所有的线程的阻塞状态

总结:锁和条件的关键之处:

a>锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。

b>锁可以管理试图进入被保护代码段的线程。

c>锁可以拥有一个或多个相关的条件对象。

d>每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。