04.互斥锁(下):如何用一把锁保护多个资源?

受保护资源和锁之间合理的关联关系应该是 N:1 的关系,也就是说可以用一把锁来保护多个资源,但是不能用多把锁来保护一个资源.

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

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

在现实世界里,球场的座位和电影院的座位就是没有关联关系的.这种场景非常容易理解,球赛有球赛的门票,电影院有电影院的门票,各自管理各自的.

同样这对应到编程领域,也很容易解决.例如.银行业务中有针对账户余额(资源)的取款操作,也有针对账户密码(资源)的更改操作,我们可以为账户余额和账户密码分配不同的锁来解决并发问题,这个还是很简单的

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


也可以用一把互斥锁保护多个资源,可以用this这一把锁管理账户类里所有的资源.在所有方法加上synchronized关键字就可以了.

但是,用一把锁有个问题,就是性能太差,会导致取款,查看余额,修改密码,查看密码这些操作是串行的.而用两把锁,取款和修改密码是可以并行的.用不同的锁对受保护资源进行精细化管理,能够提升性能。这种锁还有个名字,叫细粒度锁。

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

如果多个资源是有关联关系的,那这个问题就有点复杂了。例如银行业务里面的转账操作,账户 A 减少 100 元,账户 B 增加 100 元。这两个账户就是有关联关系的。

声明了个账户类:Account,该类有一个成员变量余额:balance,还有一个用于转账的方法:transfer(),然后怎么保证转账操作 transfer() 没有并发问题呢?


直接在transfer()方法前加上synchronized?

然而在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读到的值都是200,导致最终账户B的余额可能是300或100.就是不可能是200

使用锁的正确姿势

同一把锁来保护多个资源,只要我们的锁能覆盖所有受保护资源就可以了.

可以在账户类中定义一个变量,在创建账户类时要求传入这个变量,这样在创建Account对象时传入相同的变量,就会共享这个锁了.但是这要求传入的变量必须时同一个.

还有一种方案就是用Account.class作为共享的锁.

总结

要分析多个资源之间的关系。如果资源之间没有关系,很好处理,每个资源一把锁就可以了。如果资源之间有关联关系,就要选择一个粒度更大的锁,这个锁应该能够覆盖所有相关的资源。

“原子性”的本质是什么?其实不是不可分割,不可分割只是外在表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。例如,在 32 位的机器上写 long 型变量有中间状态(只写了 64 位中的 32 位),在银行转账的操作中也有中间状态(账户 A减少了 100,账户 B 还没来得及发生变化)。所以解决原子性问题,是要保证中间状态对外不可见。

你可能感兴趣的:(04.互斥锁(下):如何用一把锁保护多个资源?)