当前位置:   article > 正文

互斥锁(下):如何用一把锁保护多个资源?_不同变量保护 互斥锁

不同变量保护 互斥锁

上一篇文章中提到一把锁可以保护多个资源,受保护资源和锁之间合理的关联关系应该是N:1的关系,阐述了如何正确保护一个资源,但是如何正确保护多个资源没说。最后说到两把锁保护两个资源,一个this一个所属类,但是不互斥会造成并发问题。。。

产生问题最主要的原因是this对象和所属类存在必然关联关系。

当我们要保护多个资源时,首先要区分这些资源是否存在关联关系。。。

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

现实世界中球场的座位和电影院的座位就是没有关联关系的,球赛的门票只能关联球赛的座位而不是电影院的座位。。。

同样这对应到编程领域,也很容易解决。例如从银行取款就会扣钱,更改账户密码就会改变,这是两个没有关系的资源。。。

相关的示例代码如下,账户类Account有两个成员变量,分别是账户余额balance和账户密码password。取款withdraw()和查看余额getBalance()操作会访问账户余额balance,创建一个final对象balLock作为锁(类比球赛门票);更改密码updatePassword()和查看密码getPassword()操作会修改账户密码password,创建一个final对象pwLock作为锁(类比电影票)。不同的资源用不同的锁保护,各自管各自的,很简单。

class	Account	{		
	//	锁:保护账⼾余额		
	private final Object balLock = new Object();		
	//	账⼾余额				
	private	Integer	balance;		
	//	锁:保护账⼾密码		
	private	final Object pwLock = new Object();		
	//	账⼾密码		
	private String password;		
	//	取款		
	void withdraw(Integer amt)	{				
		synchronized(balLock) {						
			if	(this.balance > amt){								
				this.balance -= amt;						
			}				
		}		
	}			
	//	查看余额		
	Integer	getBalance() {				
		synchronized(balLock) {						
			return	balance;				
		}		
	}		
	//	更改密码		
	void updatePassword(String pw){				
		synchronized(pwLock) {						
		this.password = pw;				
		}
	}			
	//	查看密码		
	String	getPassword() {				
		synchronized(pwLock) {						
			return password;				
		}		
	} 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

当然也可以用一把互斥锁来保护多个资源,例如我们用this这一把锁来管理账户类里所有的资源;账户余额和用户密码。具体实现很简单,示例程序中所有的方法都增加同步关键字synchronized就可以了,这里不一一展示了。。。

但是缺点也是存在的就是性能太差,像取款和修改密码是可以并行的,最好使用两把锁。。

用不同的锁对受保护资源进行精细化管理,能够提升性能。。 这样锁还叫细粒度锁

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

上面说的问题是保护没有关联的多个资源,现在的问题有点复杂了那就是保护多个有关联关系的资源。。例如银行的转账操作:账户A减少100元,账户B增加100元。这两个账户是有关联关系的。先把这个问题代码化:声明账户类Account,该类有一个成员变量余额:balance,还有一个用于转账的办法:transfer(),然后怎么保证转账操作transfer()没有并发问题呢???

class Account {		
	private int balance;		
	//	转账		
	void transfer(Account target, int amt){				
			if	(this.balance > amt) {						
				this.balance -= amt;						
				target.balance += amt;				
			}		
	}	
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

相信你的直觉会告诉你这样的解决方案:用户synchronized关键字修饰一下transfer()方法就可以了,于是很快就完成了相关的代码,如下所示:

class Account {		
	private int balance;		
	//	转账		
	synchronized void transfer(Account target, int amt){				
			if	(this.balance > amt) {						
				this.balance -= amt;						
				target.balance += amt;				
			}		
	}	
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这段代码中,临界区有两个资源,分别是转出账户的余额this.balance和转入账户的余额target.balance,并且用的是一把锁this,符合前面提到的,多个资源可以用一把锁来保护,看上去是没有问题的,但是问题就是出现在这里。。。

this这把锁,可以保护自己的余额this.balance但是无法保护target.balance
在这里插入图片描述
下面具体分析一下,假设有A、B、C三个账户,余额都是200元,用两个线程分别执行两个转账操作:账户A转给账户B 100元,账户B转给账户C 100元,最后期望的结果是账户A余额是100元,账户B余额是200元,账户C余额是300元。。。

假设线程1执行账户A转账户B的操作,线程2执行账户B转账户C的操作。这两个线程分别在两颗CPU上同时执行,不是互斥的。因为线程1锁定的是账户A的实例(A.this),线程2锁定是账户B的实例(B.this),所以这两个线程可以同时进入临界区transfer()。同时进入临界区的结果是线程1和线程2都会读到账户B的余额为200,导致出错。。

有可能线程1后于线程2写B.balance,线程2写的B.balance被线程1覆盖,也有可能是100,那就是相反过来执行,就是不可能是200。
在这里插入图片描述

使用锁的正确姿势

上一篇文章中,提到用同一把锁来保护多个资源,实际上只是我们的锁能够把资源覆盖起来,就能保护了。。上面的例子中,this是对象级别的锁,所以A对象和B对象都有自己的锁,如何让A对象和B对象共享一把锁呢???

方案还是挺多的,比如可以让所有对象都持有一个唯一性的对象,这个对象在创建Account时传入。方案有了,完成代码就简单了。。。

示例代码如下:把Account默认构造函数变为private,同时增加一个带Object lock参数的构造函数,创建Account对象时传入相同的lock,这样所有的Account对象都会共享这个lock了。。

class Account {		
	private	Object lock;		
	private int balance;		
	private Account();		
	//	创建Account时传⼊同⼀个lock对象		
	public Account(Object lock) {				
		this.lock = lock;		
	}			
	//	转账		
	void transfer(Account target, int amt){				
		//	此处检查所有对象共享的锁				
		synchronized(lock) {						
			if	(this.balance > amt) {								
				this.balance -= amt;								
				target.balance += amt;						
			}				
		}		
	} 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

这个办法确实能解决问题但是有点小瑕疵,要求在创建Account对象的时候必须传入同一个对象,如果创建Account对象时,传入的lock不是同一个对象会出现锁自家门来保护他家资产的事,真实项目场景中,创建Account对象的代码很可能分散在多个工程中,传入共享的lock真的很难。。

更好的方案是:用Account.class作为共享的锁。Account.class是所有Account对象共享的,而这个对象也是JVM在加载Account时候创建的,所以不用担心唯一性,代码更简单。

class Account {		
	private	int	balance;		
	//	转账		
	void transfer(Account target,	int	amt){				
		synchronized(Account.class)	{						
			if	(this.balance > amt){								
				this.balance -= amt;								
				target.balance += amt;						
			}				
		}		
	}	
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

下面的图很直观的展示了我们是如何使用共享的锁Account.class来保护不同对象的临界区的。。
在这里插入图片描述

总结

原子性本质是什么?” 其实是不可分割,不可分割只是外在表现,其本质是多个资源键有一致性要求,操作的中间状态对外不可见。例如在32位机器上写long型变量有中间状态(只写了64位中的32位),所以解决原子性问题是要保证中间状态对外不可见。。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/574590
推荐阅读
相关标签
  

闽ICP备14008679号