受保护资源和锁之间合理的关联关系应该是N:1的关系,可以用一把锁来保护多个资源,但是不能用多把锁来保护一个资源(起不到互斥作用)。
当要保护多个资源时,首先要区分这些资源是否存在关联关系。
保护没有关联关系的多个资源
在现实世界里,球场的座位电影院的座位是没有关联关系的,这种场景非常容易解决,球场有球赛的门票,电影院有电影院的门票,各自管理各自的。
对应到编程领域,也很容易解决。银行业务中有针对账户余额(余额是一种资源)的取款操作,也有针对账户密码(密码是一种资源)的更改操作,可以为账户余额和账户密码分配不同的锁来解决并发问题。
账户类Account有两个成员变量,分别是账户余额balance和账户密码password。取款withdraw()和查看余额getBalance()操作会访问账户balance,创建一个final对象balLock作为锁(类比球赛门票);更改密码updatePassword()和查看密码getPassword()操作会修改账户密码password,创建一个final对象pwLock作为锁(类比电影票)。不同的资源用不同的锁保护,各自管各自的。
1 | //final最佳实践,防止一不小心改变了互斥锁的对象 |
不能用可变对象做锁。(this.balance&this.password等)修改后锁对象变了,当前线程持有旧锁,其他线程可以获取新锁,相当于多个锁保护一种资源的情况,无法保证互斥。
也可以用一把互斥锁来保护多个资源,如用this这一把锁来管理账户类里所有的资源:账户余额和用户密码。(所有方法都增加同步关键字synchronized)
但是用一把锁的性能太差,会导致取款、查看余额、修改密码、查看密码等操作都是串行的。用两把锁,取款和修改密码是可以并行的。用不同的锁对受保护的资源进行精细化管理,能够提升性能-细粒度锁。
保护有关联关系的多个资源
银行业务里的转账操作,账户A减少100元,账户B增加100元。这两个账户就是由关联关系的。
声明账户类:Account,该类有一个成员变量余额:balance,还有一个用于转账的方法:transfer()。
1 | class Account { |
仅仅用synchronized关键字修饰transfer()方法不行。临界区内有两个资源,分别是转出账户的余额this.balance和转入账户的余额target.balance,并且用的是一把锁this。this这把锁可以保护自己的余额this.balance,却保护不了别人的余额target.balance,不能用自家的锁来保护别人家的资产。
假设有A、B、C三个账户,余额都是200元,用两个线程分别执行两个转账操作:账户A转给账户B100元,账户B转给账户C100元,最后期望的结果应该是账户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,导致最终账户B的余额可能是300(线程1后于线程2写B.balance,线程2写B.balance值被线程1覆盖),可能是100(线程1先于线程2写B.balance,线程1写B.balance值被线程2覆盖),就是不可能是200。
使用锁的正确姿势
用一把锁来保护多个资源,也就是现实世界的包场,只要锁能覆盖所有受保护资源就可以了。this是对象级别的锁,A对象和B对象都有自己的锁,需要让A对象和B对象共享一把锁。
方案一
让所有对象都持有一个唯一性的对象,这个对象在创建Account时传入。
把Account默认构造函数变为private,同时增加一个带Object lock参数的构造函数,创建Account对象时,传入相同的lock,这样所有的Account对象都会共享这个lock。
1 | class Account { |
该方法要求在创建Account对象的时候必须传入同一个对象,如果创建Account对象时,传入的lock不是同一个对象,会出现锁自家门来保护他家财产的情况。在真实的项目场景中,创建Account对象的代码很可能分散在多个工程中,传入共享的lock很难。
方案二
用Account.class作为共享的锁。
Account.class是所有Account对象共享的(所有操作串行),而且这个对象是Java虚拟机在加载Account类的时候创建的,不用担心它的唯一性。使用Account.class作为共享的锁,无须在创建Account对象时传入。
1 | class Account { |
总结
保护多个资源的关键是要分析多个资源之间的关系。如果资源之间没有关系,每个资源一把锁就可以了。如果资源之间有关系,就要选择一个粒度更大的锁,这个锁应该能够覆盖所有相关的资源。除此之外,还要梳理出有哪些访问路径,所有的访问路径都要设置合适的锁。
关联关系如果用更具体、更专业的语言来描述,其实是一种“原子性”特性。
原子性的本质,其实是不可分割,不可分割只是外在表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。(在32位机器上写long型变量有中间状态(只写了64位中的32位),在银行转账的操作中也有中间状态(账户A减少了100,账户B还没来得及发生变化))。解决原子性问题,是要保证中间状态对外不可见。