Lock & Condition
序言里面已经提到,Java 使用 Lock 和 Condition 来实现管程,Lock 解决互斥问题,Condition 解决同步问题。
Lock
为什么需要 Lock
在讲死锁时,提出破坏不可抢占条件方案,但这个方案 synchronized 没法解决。
synchronized 不能实现上述方案的原因是:synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。
所以若要锁的实现能够解决上述问题,可以有如下方案:
能够响应中断。如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。
支持超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。
非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。
这也是为什么 JDK 重新实现了套管程:
Lock 怎么保证可见性
我们知道 Java 的 Happens-Before 原则有一条关于 synchronized 的,所以 synchronized 能够保证可见性。
但是 Lock 怎么保证可见性呢?Lock 内部持有一个 volatile 变量 state,获取和释放锁的时候都会读写 state,所以利用 顺序性规则、volatile 变量规则、传递性,Lock 就可以保证可见性。
ReentrantLock
Lock 有一个实现类 ReentrantLock
,即可重入锁:线程可以重复获取同一把锁。
底层基于 AQS,每当同一个线程获得一次这把锁,state 变量加1,释放一次 state 变量减1,最终当线程不持有锁时,state 为0。
公平锁与非公平锁
ReentrantLock
有一个有参构造函数,传入 fair 表示公平策略。
如果一个线程没有获得锁,就会进入等待队列,当有线程释放锁的时候,就需要从等待队列中唤醒一个等待的线程。公平锁:谁等待的时间长,就唤醒谁;非公平锁:有可能等待时间短的线程反而先被唤醒。
用锁的最佳实践
永远只在更新对象的成员变量时加锁
永远只在访问可变的成员变量时加锁
永远不在调用其他对象的方法时加锁
Condition
Condition 实现了管程模型里面的条件变量。一个 Lock 可以有多个条件变量,这个是与 synchronized 一个很大的区别。
利用两个条件变量实现阻塞队列
由此可见:
Condition.await()
对应Object.wait()
Condition.signal()
对应Object.notify()
Condition.signalAll()
对应Object.notifyAll()
ReadWriteLock
在生产中经常有读多写少的场景,比如缓存。针对这种情况,Java 提供了一个接口ReadWriteLock
。
允许多个线程同时读共享变量。
只允许一个线程写共享变量。
当一个线程在写共享变量时,不允许其它线程读和写操作。
缓存实现
缓存数据按需加载
缓存中的数据是需要初始化的,若数据量小,可以直接一次性全部加载;若数据量大,则要按需加载(懒加载),即当查询的时候再加载。
读写锁升级与降级
把上面的代码改成如下是否可行?
答案是不行,会导致写锁永远等待。
锁的升级:先获取了读锁,再获取写锁。这是不允许的。
锁的降级:先获取了写锁,再获取读锁。这是允许的。
总结
读写锁也支持公平和非公平模式。
只有写锁支持条件变量,读锁不支持。
StampedLock
Java 1.8 提供,性能比 ReadWriteLock 更好。支持三种模式:
写锁:与 ReadWriteLock的写锁类似,只允许一个线程获取。获取时返回一个 stamp,解锁时需要传入此 stamp。
悲观读锁:与 ReadWriteLock 读锁类似,允许多个线程同时获取,与写锁互斥。获取时返回一个 stamp,解锁时需要传入此 stamp。
乐观读:注意没加锁字,因为乐观读是无锁的。允许一个线程获取写锁。
上述代码首先乐观读,但是 x、y 可能被修改,因此需要验证一下,若验证不通过,升级为读锁。若不升级为读锁,需要在循环里面反复乐观读,浪费 CPU,所以最佳实践是升级为读锁。
数据库的乐观锁
先从数据库读一条带 version 字段的记录。
然后在程序中对数据做业务修改。
最后写入数据库时采用
update set version = version + where version = XX and id = XX
的语句,若返回1,则说明更新成功,期间没有人修改这条数据;若返回0,则更新失败,说明有人修改过此数据。
可见,数据库乐观锁的 version 字段与 StampedLock 的 stamp 是同样的意义。
结论
StampedLock 的功能是 ReadWriteLock 的子集。
StampedLock 不支持可重入;
StampedLock 的悲观读锁和写锁都不支持条件变量;
若线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,一定不要调用中断操作;如果需要支持中断功能,一定使用可中断的悲观读锁
readLockInterruptibly()
和写锁writeLockInterruptibly()
。StampedLock 支持锁降级
tryConvertToReadLock()
与升级tryConvertToWriteLock()
。
StampedLock 读模板:
StampedLock 写模板:
Last updated