锁的粒度
今天拿取款来说说事儿
假设我们有Acount用户这个实体,有两个字段,一个用户名,一个余额,都是资源。
class Acount{
private Integer balance;
private String name;
public void updateName(String name){
this.name = name;
}
public void withdraw(Integer amt){
if(this.balance > amt){
this.balance -= amt;
}
}
}
一把锁锁一个资源
讲道理,如果我们现在想给用户名、余额分别加锁,我们需要这样做
class Acount{
private Integer balance;
private final Object balanceLock=new Object();
private String name;
private final Object nameLock=new Object();
public void updateName(String name){
synchronized(nameLock){
this.name = name;
}
}
public void withdraw(Integer amt){
synchronized(balanceLock){
if(this.balance > amt){
this.balance -= amt;
}
}
}
}
这样分别给每个用户对象,分配了一把锁,多线程下修改同一用户信息,不会出现A、B线程都拿到资源在不同的CPU运行,出现不可见问题
一把锁锁多个资源
synchronized可以用在变量、方法、静态方法上,分别对应锁的粒度为变量、当前对象this、当前类
当需要锁定当前对象多个方法时
class Acount{
private Integer balance;
private String name;
synchronized public void updateName(String name){
this.name = name;
}
synchronized public void withdraw(Integer amt){
if(this.balance > amt){
this.balance -= amt;
}
}
}
这样就针对某个用户对象,同一时间updateName()、withdraw()只能有一个执行
如果现在需要转账,a给b转账,b给c转账,单独锁一个对象肯定是不够玩了,因为锁是针对资源锁定的,加在方法上只能锁定当前用户对象
这样做会有什么问题呢(代码在下面)?
我模拟一个场景,a\b\c各有100块钱,a给b转100,b给c转100,A、B线程同时执行。
A获取到,a对象的锁
B获取到,b对象的锁
但是他们没有锁定对方账户,那同时执行的后果,可能B线程b=0写完,A线程的b=200也写完了
最终结果a=0、b=200、c=200,与我们预想的a=0、b=100、c=200,凭空多了100块
class Acount{
private Integer balance;
private String name;
synchronized public void transfer(Acount target,Integer amt){
if(this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
这种情况可以加static修饰transfer方法,这样锁的资源对象就是Acount.class了,但是这样的后果就是整个系统的转账串行化,效率低下!
我们再进阶一步优化锁
我们需要将目标对象也锁了
class Acount{
private Integer balance;
private String name;
synchronized public void transfer(Acount target,Integer amt){
synchronized(target){
if(this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
}
这次,a->b ,b->c不会再出问题了,因为操作成了原子性的,只有同时获取到两把锁,才能执行业务,这保障了原子性
但是,如果a->b,b->a转账,就赶寸了,A线程拿到a锁,B拿到b锁,互相等对方手中的资源
没错,死锁了~
避免死锁
对于死锁的发生,我们要参考卡夫曼大佬的总结,先了解死锁的形成。
- 互斥,同一时间只有一个线程可以拿到两把锁
- 占有且等待,获取到部分资源,楞等别的资源
- 不可抢占资源,别的线程不能抢走当前线程已经获取的部分资源
- 循环等待,我也等、你也等
只要破坏其一,就可以解决死锁的问题,第一点,互斥,本身我们就是想让两个有关系的资源a、b账户对象绑一起,线程间互斥,所以pass第一个
占有且等待
一次让他拿到所有的资源,就可以解决,我们创建一个Allocator类,他管理所有的资源,开放apply()方法获取所有资源,free()释放资源
class Allocator{
private List<Acount> pool = new ArrayList(){};
synchronized public boolean apply(Acount from,Acount to){
if(pool.contains(from)||pool.contains(to)){
return false;
}
pool.add(from);
pool.add(to);
return true;
}
synchronized public void free(Acount from,Acount to){
pool.remove(from);
pool.remove(to);
}
}
class Acount{
private Integer balance;
private Allocator allocator;
public void transfer(Acount target,Integer amt){
while(!allocator.apply(this,target));
try{
synchronized(this){
synchronized(target){
if(this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
} finally{
allocator.free(this,target);
}
}
}
这种方式可以保证执行可以多线程,但是获取资源仍然是串行的,部分可以高效
不可抢占
synchronized目前做不到主动释放已占有资源,因为获取到资源,线程就进入阻塞了,啥都干不了。
需要用到concurrent包下的Lock可以搞定
资源循环
将我们需要锁的资源,按相同顺序排序,再上锁,破坏循环规则
class Acount{
private Integer balance;
public void transfer(Acount target,Integer amt){
Acount left=this,right=target;
if(this.id>target){
right = this;
left = target;
}
synchronized(this){
synchronized(target){
if(this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
}
}
还有可以整个丢到mq里,让他获取锁,获取失败就重试,保证最终一致性