并发编程—如何使用一把锁保护多个资源?

目录

一、保护没有关联关系的多个资源

二、保护有关联关系的多个资源

三、正确使用锁的姿势

四、总结


上篇文章中,我们提到了受保护资源和锁之间合理的关系应该是N:1的关系,也就是说可以用一把锁来保护多个资源,而不能用多把锁来保护一个资源。那么如何使用一把锁保护多个资源呢?

一、保护没有关联关系的多个资源

比如如下所示的代码,在Account类中有余额 balance 和 密码 password 两个属性,而修改密码和取款两个是不相干的操作,在转账时,可以修改密码,在修改密码时也,也可以转账。当然可以使用同一把锁来同时保护 balance 和 password  那么就会导致这几个操作是串行的,就会影响性能。像这种不相干资源咱们可以使用不同的锁保护。用不同的锁对受保护资源进精细化管理,能够提升性能。这种锁就叫“细粒度锁

public class Account {

	private Object bLock = new Object();
	private Object pwLock = new Object();
	
	private double balance;
	
	private String password;
	
	//取款
	public  void withdraw(double atm) {
		synchronized ( bLock) {
			if(this.balance > atm) {
				this.balance -= atm;
			}
			
		}
	}
	
	//查询余额
	public double getBlance() {
		synchronized ( bLock) {
			return this.balance;
		}
	}
	
	//修改密码
	public void updatePassword(String newPwd) {
		synchronized (pwLock) {
			this.password = newPwd;
		}
	}

	
	//查询密码
	public String getPassWord() {
		synchronized (pwLock) {
		    return this.password;	
		}
	}
	
}

二、保护有关联关系的多个资源

如果多个资源有关联关系的,那这个问题就会复杂很多。例如银行里面的转账业务,账户 A 转账给 B 100元,那么这两个账户就有关联关系了,两个操作需要A的账户 减少100元,B的账户 增加100元。那么这种存在关联关系的操作如何解决呢 ?如下代码所示:

public class Account {

	private double balance;
	
	public void transfer(Account target, double amount) {
		
		if(this.balance > amount) {
			this.balance = this.balance - amount;
			target.balance += amount;
		}
		
	}
	
}

有人可能说给 transfer() 方法加一个 synchronized 关键字加锁就可以了啊!如下所示:

public class Account {

	private double balance;
	
	
	public synchronized void transfer(Account target, double amount) {
		
		if(this.balance > amount) {
			this.balance = this.balance - amount;
			target.balance += amount;
		}
		
	}
}

这样加上synchronized后真的会如你所愿吗?答案是否定的。因为临界区有两个资源,this.balance 和 target.balance,并且使用的锁是 this,符合我们前面说到的一个锁保护多个资源,看似正确,实时上却并非如此,问题就出现在 this 这把锁上,this 这把锁只能保护 this.balance,却保护不了target.balance,就像你不能用自家的锁保护别人家的财产。

那我们分析一下,假设 A、B、C账户上都是 200元,假设线程A执行 A 转账给 B,线程B  执行 B 转账给 C。那么这两个线程分别再两颗CPU上执行,那么这两个是互斥的吗?答案是否定的,因为A线程锁的是 A实例 (A.this),而线程B锁定的是 B实例 (B.this),显然练个线程持有的不是同一把锁,那么执完后,B账户上有多少钱呢,可能是 300,也可能是100。如下图所示:

并发转账示意图

 如果两个线程进入临界区后,读到 账户 B的余额都是 200,如果先执行 线程A,那么线程B的结果就会覆盖,线程A的结果,最终账户B的余额为100。

如果线程B先执行完,线程A 的结果就会覆盖 线程B的结果,那么 最终账户 B的余额就会为300。

三、正确使用锁的姿势

在上篇文章中,我们提到可以用一把锁来保护多个资源,那么如何实现呢?其实,很简单,只要我们的锁能够覆盖所有受保护的资源就可以了。在上面的例子中,我们可以是所有的转账操作都使用同一把锁,如下所示:

public class Account {
	
	private Object lock;

	private double balance;
	
	public Account(Object lock) {
		this.lock = lock;
	}
	
	public  void transfer(Account target, double amount) {
		synchronized (lock) {
			if(this.balance > amount) {
				this.balance = this.balance - amount;
				target.balance += amount;
			}
		}
		
	}
}

 这样实现必须要求,在创建 Account实例是,传入的 lock 必须是同一个对象,这样实现起来相对比较麻烦,也很难控制,我们可以通过如下方式实现,使用Account.class作为锁。

public class Account {

	private double balance;
	
	public  void transfer(Account target, double amount) {
		synchronized (Account.class) {
			if(this.balance > amount) {
				this.balance = this.balance - amount;
				target.balance += amount;
			}
		}
		
	}
}

四、总结

看完这篇文章后是否对如何保护多个资源有了心得呢,如何使用锁来保护资源,关键是要分析多个资源之间是否存在关系。

1、如果多个资源之间不存在关系,很好处理,就每个资源使用一把锁就行了,使用细粒度锁。

2、如果多个资源之间存在关系,那么就使用一个粒度更大的锁,这个锁应该能够覆盖所有的相关资源。

除此之外,还要舒立春有哪些访问路径,所有的访问路径都要设置合适的锁。

引申一下,关联关系如果用更具体、更专业的语言来描述的话,其实就是一种“原子性”特征。“原子性”的本质,其实就是不可分割,不可分割只是外在表现,其本质就是多个资源间有一致性的要求,操作的中间状态对外不可见。所以  解决原子性问题,是要保证中间状态对外不可见

 

参考资料:

    Java并发编程实战

并发编程—死锁了,怎么办?

并发编程—等待-通知

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值