通过上一篇文章我们已经知道了在并发操作时,对相同数据进行存取会导致了数据的不一致问题,那么导致这样的问题的原因是什么呢?怎么避免这个问题呢?
并发下数据不一致问题的原因
造成并发操作下数据不一致问题的原因主要在于:各线程对数据的存取时机冲突造成的。
每个线程都有自己的工作空间,各线程会将共享变量从主存拷贝到各自的工作内存,线程在工作内存中进行操作后再写入主存。如下图:
同步机制
为了解决并发带来问题,必须进行并发控制,其中一种方式就是同步机制,当多个线程访问同一个资源时,它们需要以某种顺序来确保资源在某一时刻只能被一个线程使用。
要实现同步操作,必须要获得一个对象锁,并对临界区(访问互斥资源的代码块)进行加锁和解锁操作,可以保证在同一刻只有一个线程能够进入临界区,并且在这个锁被释放之前,其他线程就不能在进入这个临界区。
如果还有其他线程想要获得该锁,只能进入阻塞队列等待,只有当拥有该锁的线程退出临界区时,锁才会被释放,线程调度器会重新选择线程获得该锁进入临界区。
Java语言在同步机制中提供了语言级的支持,可以通过synchronized关键字来实现同步,并且在Java SE 5.0引入了ReentrantLock类。
但是需要注意的是,同步机制是以很大的系统开销作为代价的,有时候甚至可能造成死锁,所以,同步控制并非越多越好,要尽量避免无谓的同步控制。
ReentrantLock类
ReentrantLock实现了java.util.concurrent.locks包下的Lock接口,实现该接口的类还有:ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock。
ReentrantLock是一个可重入的互斥锁Lock,它具有与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
推荐使用方式:
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。
注意:必须把解锁操作放在finally语句块中,如果临界区的代码抛出异常,锁必须被释放,否则,其他线程将永远阻塞。
我们还是以银行转账的例子来说明ReentrantLock的使用。
模拟账户–Account类:
public class Account {
private String name;//名字
private double money;//余额
//构造方法
public Account (String name,double money) {
this.name = name;
this.money = money;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
模拟银行–Bank类:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Bank {
/**
* 转账
* @param fromAccount 转出账户
* @param toAccount 转入账户
* @param money 转账金额
* @return
*/
private Lock banklock = new ReentrantLock();
public boolean transfer(Account fromAccount,Account toAccount,double money) {
banklock.lock();
try {
if (fromAccount.getMoney() >= money) {
fromAccount.setMoney(fromAccount.getMoney() - money);
toAccount.setMoney(toAccount.getMoney() + money);
System.out.println(fromAccount.getName() + "向" + toAccount.getName() + "转账" + money + "元");
return true;
} else {
System.out.println(fromAccount.getName() + "余额不足,转账失败");
return false;
}
}
finally {
banklock.unlock();
}
}
/**
* 打印余额
* @param account 账户
*/
public void display(Account account) {
banklock.lock();
try {
System.out.println(account.getName() + ":" + account.getMoney() + "元");
}
finally {
banklock.unlock();
}
}
}
转账线程–TransferRunnable类:
/**
* 转账线程
* @author 朋
*
*/
public class TransferRunnable implements Runnable{
Bank bank;
private Account fromAccount;
private Account toAccount;
private double money;
private final int DELAY = 10;
@Override
public void run() {
// TODO Auto-generated method stub
bank.transfer(fromAccount, toAccount, money);
try {
Thread.sleep((long) (DELAY * Math.random()));//模拟延迟
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public TransferRunnable (Bank bank,Account fromAccount,Account toAccount,double money) {
this.bank = bank;
this.fromAccount = fromAccount;
this.toAccount = toAccount;
this.money = money;
}
}
打印余额线程–DisplayRunnable:
/**
* 打印余额线程
* @author 朋
*
*/
public class DisplayRunnable implements Runnable {
Bank bank;
private Account account;
private final int DELAY = 10;
public DisplayRunnable(Bank bank,Account account) {
this.bank = bank;
this.account = account;
}
@Override
public void run() {
// TODO Auto-generated method stub
bank.display(account);
try {
Thread.sleep((long) (DELAY * Math.random()));//模拟延迟
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
测试–Test类:
public class Test {
public static void main(String[] args) {
Bank bank = new Bank();//创建银行对象
Account zhangsan = new Account("zhangsan",100);//创建账户对象
Account lisi = new Account("lisi",100);//创建账户对象
//打印输出
System.out.println(zhangsan.getName() + ":" + zhangsan.getMoney() + "元");
System.out.println(lisi.getName() + ":" + lisi.getMoney() + "元");
//模拟并发
for (int i = 0;i < 10;i++) {
new Thread(new TransferRunnable(bank, zhangsan, lisi, 50)).start();
new Thread(new TransferRunnable(bank, lisi, zhangsan, 100)).start();
new Thread(new DisplayRunnable(bank, zhangsan)).start();
new Thread(new DisplayRunnable(bank, lisi)).start();
}
}
}
这个例子的重点在于,Bank类中的两个方法transfer()和display()方法,使用了ReentrantLock对临界区代码进行加锁,保证了在任何一个时刻只有一个线程能够进入临界区,从而不会出现数据不一致的问题。
运行结果(部分):
从结果可以看到,程序没有出现数据的不一致问题。
每一个Bank对象有自己的ReentrantLock对象,如果两个线程试图访问同一个Bank对象,那么锁将以串行地方式提供服务。
重入锁
锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数器来跟踪lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁,由于这一特性,被一个锁保护的代码可以调用另外一个使用相同的锁的方法。
例如:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Bank {
/**
* 转账
* @param fromAccount 转出账户
* @param toAccount 转入账户
* @param money 转账金额
* @return
*/
private Lock banklock = new ReentrantLock();
public boolean transfer(Account fromAccount,Account toAccount,double money) {
banklock.lock();
try {
if (fromAccount.getMoney() >= money) {
fromAccount.setMoney(fromAccount.getMoney() - money);
toAccount.setMoney(toAccount.getMoney() + money);
System.out.println(fromAccount.getName() + "向" + toAccount.getName() + "转账" + money + "元");
display(fromAccount);//可以调用另外一个使用相同的锁的方法
return true;
} else {
System.out.println(fromAccount.getName() + "余额不足,转账失败" );
display(fromAccount);//可以调用另外一个使用相同的锁的方法
return false;
}
}
finally {
banklock.unlock();
}
}
/**
* 打印余额
* @param account 账户
*/
public void display(Account account) {
banklock.lock();
try {
System.out.println(account.getName() + ":" + account.getMoney() + "元");
}
finally {
banklock.unlock();
}
}
}
transfer()方法与display()方法使用了相同的锁banklock,所以可以在transfer()方法中调用banklock()方法。
当一个线程获取banklock对象,进入transfer()方法临界区时,banklock对象的持有计数为1,当执行到进入transfer()方法中调用的display()方法时,banklock对象的持有计数变为2。当display()方法退出时,持有计数变回1,transfer()方法退出时,释放锁,持有计数变为0。
我们可以使用ReentrantLock中的成员方法public int getHoldCount() 来查看当前线程保持此锁的次数。
临界区异常
要留心临界区中的代码,不要因为异常的抛出而跳出了临界区。如果在临界区代码结束之前抛出了异常,finally子句将释放锁,到会使对象可能处于一种受损状态。
我们还是以上面的银行转账的例子来说明这个问题,我们将Bank类中的transfer()方法稍微改造下,在临界区中故意制造一个运行时异常。
import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.ReentrantLock;
/**
* 转账
*/
public class Bank {
private ReentrantLock banklock = new ReentrantLock();
public boolean transfer(Account fromAccount,Account toAccount,double money) {
banklock.lock();
try {
if (fromAccount.getMoney() >= money) {
fromAccount.setMoney(fromAccount.getMoney() - money);
System.out.println(1/0);//制造异常
toAccount.setMoney(toAccount.getMoney() + money);
System.out.println(fromAccount.getName() + "向" + toAccount.getName() + "转账" + money + "元");
return true;
} else {
System.out.println(fromAccount.getName() + "余额不足,转账失败" );
return false;
}
}
finally {
banklock.unlock();
}
}
/**
* 打印余额
* @param account 账户
*/
public void display(Account account) {
banklock.lock();
try {
System.out.println(account.getName() + ":" + account.getMoney() + "元");
}
finally {
banklock.unlock();
}
}
}
我们在测试类中创建一个线程来执行转账操作,再创建两个线程分别来显示zhangsan和lisi的余额。
public class Test {
public static void main(String[] args) {
Bank bank = new Bank();//创建银行对象
Account zhangsan = new Account("zhangsan",100);//创建账户对象
Account lisi = new Account("lisi",100);//创建账户对象
//打印输出
System.out.println(zhangsan.getName() + ":" + zhangsan.getMoney() + "元");
System.out.println(lisi.getName() + ":" + lisi.getMoney() + "元");
new Thread(new TransferRunnable(bank, zhangsan, lisi, 50)).start();
new Thread(new DisplayRunnable(bank, zhangsan)).start();
new Thread(new DisplayRunnable(bank, lisi)).start();
}
}
运行结果:
转账线程中抛出异常,语句fromAccount.setMoney(fromAccount.getMoney() - money);已经执行,而其后面的语句toAccount.setMoney(toAccount.getMoney() + money);切没有被执行,造成zhangsan的前不翼而飞了。