在 synchronized 互斥锁 中,对于 如何用一个锁 来保护 多个有关联的资源中,我们用 Account.class 作为锁,解决转账问题。是的A B账户没有并发问题。虽然这个方案解决了并发问题,但是 所有的账户 的转账操作都变成了串行的。例如 账户A转入B,账户C转入 D,在这个方案中只能串行执行,性能太差,而现实世界中这两个操作是可以并行的。
所以我们可以采用细粒度锁,用不同的锁来保护不同的资源。对于这个转账操作来说,就是使用两把锁,一个锁定转出账户this,一个锁定转入账户 target。只有两个锁都加锁成功,则进行转账操作。这种情况下,账户A转入B,账户C转入 D的两个操作就可以并行了。代码如下:
使用细粒度锁,可以提高并行度,是性能优化的重要手段,而这个方式也有代价,那就是可能会导致死锁。
对于以上代码情景:如果现在有两个线程,分别执行账户A给B转100元,账户B给A转一百。那么两个线程会在第一步给this加锁,也就是线程A给账户A加锁,线程B给账户B加上了锁,然后准备去获取target锁的时候,线程A发现账户B已经被加锁,所以账户A只能等待,同时持有账户B的锁的线程B也在等待账户A的锁,因为两个线程都只能等待下去,无法进行。这也就是我们说的死锁。
在发生死锁的时候,一般没有什么特别好的办法,只能重启应用。因而,解决死锁最好的办法就是规避死锁。
死锁:一组相互竞争资源你的线程相互等待,导致“永久”阻塞的现象。
发生死锁的四个条件 (必须一下四个条件同时发生才会死锁):
互斥:共享资源X 和 Y 同一时刻只能被一个线程占用。
占有且等待:线程1 已经取得了 共享资源X,在等待 共享资源Y 的时候,不释放 共享资源X。
不可抢占:其他线程不能强行抢占线程1占有的资源
循环等待:线程1 等待 线程2 占有的资源,线程2 等待 线程1占有的资源,就是循环等待。
对于以上四个条件,我们只要破坏一个就可以避免死锁。
互斥是不可以破坏的,因为锁就是互斥的。
对于“占用且等待”,我们可以一次性申请所有资源,这样就不用等待了。
对于“不可抢占”,占用部分的资源的线程,在进一步申请其他资源的时候,申请不到就释放自己战友的资源。
对于“循环等待”,可以按序申请资源,也就是给资源排个序,先申请前边的在申请后边的。就不会循环了。
1、破坏占用且等待资源的办法 (一次性申请所有的资源)
如一下代码,定义一个Allocator类,用于同时申请和同时释放我们所需要的资源。
但是上面代码中的while循环,如果在apply()耗时很长或者并发量大的情况下是很消耗CPU的。解决办法
while(!actr.apply(this, target))
2、破坏不可抢占条件
但是synchronized是做不到了,因为当synchronized申请资源申请不到的时候,下城就进入了阻塞状态啥也做不了。也就释放不了资源了。java语言层级解决不了这个问题,但是SDK层面还是解决了的。
3、破坏循环等待条件
给所需申请的资源排序,按序申请。这里需要保护的资源是账户,所以我们给账户增加一个属性id,申请时候按照从小到大申请
在现实操作中,我们需要根据操作成本,去选择一个成本较低的条件进行破坏