同步机制和ReentrantLock类

通过上一篇文章我们已经知道了在并发操作时,对相同数据进行存取会导致了数据的不一致问题,那么导致这样的问题的原因是什么呢?怎么避免这个问题呢?

并发下数据不一致问题的原因

造成并发操作下数据不一致问题的原因主要在于:各线程对数据的存取时机冲突造成的。

每个线程都有自己的工作空间,各线程会将共享变量从主存拷贝到各自的工作内存,线程在工作内存中进行操作后再写入主存。如下图:

在这里插入图片描述

同步机制

为了解决并发带来问题,必须进行并发控制,其中一种方式就是同步机制,当多个线程访问同一个资源时,它们需要以某种顺序来确保资源在某一时刻只能被一个线程使用。

要实现同步操作,必须要获得一个对象锁,并对临界区(访问互斥资源的代码块)进行加锁和解锁操作,可以保证在同一刻只有一个线程能够进入临界区,并且在这个锁被释放之前,其他线程就不能在进入这个临界区。

如果还有其他线程想要获得该锁,只能进入阻塞队列等待,只有当拥有该锁的线程退出临界区时,锁才会被释放,线程调度器会重新选择线程获得该锁进入临界区。
在这里插入图片描述
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的前不翼而飞了。

猜你喜欢

转载自blog.csdn.net/is_Javaer/article/details/84724169
今日推荐