Java 读写锁

总结了一下Java的读写锁实现

  1. ReadWriteLock

最简单的就是【ReentrantReadWriteLock】,但是有一个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种【悲观锁】。

  1. 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);
    }
}
  1. 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来监听票号

  1. CLHLock

为了减少缓存一致性带来的开销,CLHLock被发明了。
CLH锁的核心思想是:1. 竞争线程排队 2. 监听变量拆分
我的理解是将多个监听变为一个队列里的前节点的监听
CLH:当前节点监听前驱节点是否释放锁
MCS:前驱节点释放锁后修改下一个节点的锁标识,当前节点监听当前节点中的锁标识

  1. 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;
      }
  }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值