总结了一下Java的读写锁实现
- ReadWriteLock
最简单的就是【ReentrantReadWriteLock】,但是有一个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种【悲观锁】。
- StampedLock
为了提高效率,jdk1.8引入了新的读写锁【StampedLock】:读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种【乐观锁】。是不可重入锁。验证和获取乐观锁都是针对修改版本号。
public class StampedLockTest {
private final StampedLock stampedLock = new StampedLock();
private double x;
private double y;
public double distanceFromOrigin() {
// 获得一个乐观读锁
// 返回版本号
long stamp = stampedLock.tryOptimisticRead();
// 注意下面两行代码不是原子操作
// 假设x,y = (100,200)
double currentX = x;
// 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
double currentY = y;
// 此处已读取到y,如果没有写入,读取是正确的(100,200)
// 如果有写入,读取是错误的(100,400)
// 检查乐观读锁后是否有其他写锁发生,验证版本号
if (!stampedLock.validate(stamp)) {
// 获取一个悲观读锁
stamp = stampedLock.readLock();
try {
currentX = x;
currentY = y;
} finally {
// 释放悲观读锁
stampedLock.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
- TicketLock
TicketLock不是现成的锁,而是一种实现读写锁的方法:线程想要竞争某个锁,需要先领一张ticket,然后监听flag,发现flag被更新为手上的ticket的值了,才能去占领锁,就像是在医院看病一样,医生就是临界区,病人就是线程,病人挂了号领一张单子,单子上写了一个独一无二的号码,病人等的时候就看屏幕,屏幕上显示到自己的号码了,才能进去找医生。
实现公平锁
public class TicketLock {
AtomicInteger ticket = new AtomicInteger(0);
volatile int flag = 0;
void lock() {
//发号必须是一个原子操作,不能多个线程拿到同一个ticket
int my_ticket = ticket.getAndIncrement();
while (my_ticket != flag) {
// 自旋
}
}
void unlock() {
flag++;
}
}
这个锁开心比较大,因为通过volatile来监听票号
- CLHLock
为了减少缓存一致性带来的开销,CLHLock被发明了。
CLH锁的核心思想是:1. 竞争线程排队 2. 监听变量拆分
我的理解是将多个监听变为一个队列里的前节点的监听
CLH:当前节点监听前驱节点是否释放锁
MCS:前驱节点释放锁后修改下一个节点的锁标识,当前节点监听当前节点中的锁标识
- MCSLock
MCS与CLH最大的不同在于:CLH是在前驱节点的locked域上自旋,MCS是在自己节点上的locked域上自旋。
具体的实现是,前驱节点在释放锁之后,会主动将后继节点的locked域更新。
也就是把多次对远端内存的监听 + 一次对本地内存的更新,简化成了多次对本地内存的监听 + 一次对远端内存的更新。
public class MCSLock {
volatile Node head, tail;//waitingList
public MCSLock() {
head = tail = null;
}
public Node lock() {
//lock-free的将node添加到waitingList的尾部
Node node = new Node(true, null);
Node oldTail = tail;
while (!cas(tail, oldTail, node)) {
oldTail = tail;
}
if (null == oldTail) {//如果等待列表为空,那么获取锁成功,直接返回
return node;
}
oldTail.setNext(node);
while (node.isLocked()) {//监听当前节点的locked变量
}
return node;
}
public void unlock(Node node) {
if (node.getNext() == null) {
if (cas(tail, node, null)) {//即使当前节点的后继为null,也要用cas看一下队列是否真的为空
return;
}
while (node.getNext() != null) {//cas失败,说明有后继节点,只是还没更新前驱节点的next域,等前驱节点看到后继节点后,即可安全更新后继节点的locked域
}
}
node.getNext().setLocked(false);
}
static class Node {
public Node(boolean locked, Node next) {
this.locked = locked;
this.next = next;
}
volatile boolean locked;//true:当前线程正在试图占有锁或者已经占有锁,false:当前线程已经释放锁,下一个线程可以占有锁了
Node next;//后继节点
public boolean isLocked() {
return locked;
}
public void setLocked(boolean locked) {
this.locked = locked;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
}