华为应用锁退出立即锁_读写锁应用与原理

目录:

  • 概念

  • 应用

  • 原理

  • StampedLock

一.概念

独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁

共享锁:指该锁可以被多个线程锁持有

对ReentrantReadWriteLock其读锁是共享,其写锁是独占

写的时候只能一个人写,但是读的时候,可以多个人同时读

为什么会有写锁和读锁

原来我们使用ReentrantLock创建锁的时候,是独占锁,也就是说一次只能一个线程访问,但是有一个读写分离场景,读的时候想同时进行,因此原来独占锁的并发性就没这么好了,因为读锁并不会造成数据不一致的问题,因此可以多个人共享读

多个线程 同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行,但是如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写

  • 读-读:能共存

  • 读-写:不能共存

  • 写-写:不能共存

ReentrantReadWriteLock

当读操作远远高于写操作时,这时候使用 读写锁 让 读-读 可以并发,提高性能。类似于数据库中的 select ... from ... lock in share mode

提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法

代码

class DataContainer {  private Object data;  private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();   private ReentrantReadWriteLock.ReadLock r = rw.readLock(); private ReentrantReadWriteLock.WriteLock w = rw.writeLock();  public Object read() {     log.debug("获取读锁...");     r.lock();    try {      log.debug("读取"); sleep(1);      return data;    } finally {       log.debug("释放读锁...");      r.unlock();     }  }  public void write() {     log.debug("获取写锁...");     w.lock();    try {      log.debug("写入");      sleep(1); } finally {      log.debug("释放写锁...");      w.unlock();     }  } }

测试 读锁-读锁 可以并发

DataContainer dataContainer = newDataContainer(); newThread(() -> {  dataContainer.read(); }, "t1").start();new Thread(() -> {   dataContainer.read();}, "t2").start();

输出结果,从这里可以看到 Thread-0 锁定期间,Thread-1 的读操作不受影响

14:05:14.341 c.DataContainer [t2] - 获取读锁... 14:05:14.341 c.DataContainer [t1] - 获取读锁... 14:05:14.345 c.DataContainer [t1] - 读取 14:05:14.345 c.DataContainer [t2] - 读取 14:05:15.365 c.DataContainer [t2] - 释放读锁... 14:05:15.386 c.DataContainer [t1] - 释放读锁...

测试 读锁-写锁 相互阻塞

DataContainer dataContainer = newDataContainer(); newThread(() -> {  dataContainer.read(); }, "t1").start();Thread.sleep(100); new Thread(() -> {  dataContainer.write(); }, "t2").start();

输出结果

14:04:21.838 c.DataContainer [t1] - 获取读锁... 14:04:21.838 c.DataContainer [t2] - 获取写锁... 14:04:21.841 c.DataContainer [t2] - 写入 14:04:22.843 c.DataContainer [t2] - 释放写锁... 14:04:22.843 c.DataContainer [t1] - 读取 14:04:23.843 c.DataContainer [t1] - 释放读锁...

写锁-写锁 也是相互阻塞的,这里就不测试了

注意事项

  • 读锁不支持条件变量

  • 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待

r.lock(); try {  // ...  w.lock();   try {    // ...  } finally{     w.unlock();  }} finally{  r.unlock();}
  • 重入时降级支持:即持有写锁的情况下去获取读锁

class CachedData {   Object data;  // 是否有效,如果失效,需要重新计算 data  volatile boolean cacheValid;  final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();   void processCachedData() {    rwl.readLock().lock();     if (!cacheValid) {    // 获取写锁前必须释放读锁     rwl.readLock().unlock();     rwl.writeLock().lock();       try {        // 判断是否有其它线程已经获取了写锁、更新了缓存, 避免重复更新         if (!cacheValid) {          data = ...          cacheValid = true;         }        // 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存        rwl.readLock().lock();       } finally {       rwl.writeLock().unlock();       }    }    // 自己用完数据, 释放读锁     try {      use(data);     } finally {      rwl.readLock().unlock();     }  } }

二.读写锁应用

1. 缓存更新策略更新时,是先清缓存还是先更新数据库 

先清缓存

d5981bc218fad422eba45942346d3b77.png

先更新数据库

2e681d72f4b3f1f34f041d7946d1f0c2.png

补充一种情况,假设查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询

3146444b97b2aa18e28a8cb1dacbd281.png

这种情况的出现几率非常小,见 facebook 论文

2. 读写锁实现一致性缓存

使用读写锁实现一个简单的按需加载缓存

class GenericCachedDao {  // HashMap 作为缓存非线程安全, 需要保护   HashMap map = new HashMap<>();  ReentrantReadWriteLock lock = new ReentrantReadWriteLock();   GenericDao genericDao = new GenericDao();  public int update(String sql, Object... params) {     SqlPair key = new SqlPair(sql, params);    // 加写锁, 防止其它线程对缓存读取和更改    lock.writeLock().lock();     try {      int rows = genericDao.update(sql, params);       map.clear();      return rows;    } finally {       lock.writeLock().unlock();    }   }    public T queryOne(Class beanClass, String sql, Object... params) {     SqlPair key = new SqlPair(sql, params);    // 加读锁, 防止其它线程对缓存更改    lock.readLock().lock();    try {      T value = map.get(key);       if (value != null) {        return value;       }    } finally {       lock.readLock().unlock();    }    // 加写锁, 防止其它线程对缓存读取和更改     lock.writeLock().lock();    try {      // get 方法上面部分是可能多个线程进来的, 可能已经向缓存填充了数据       // 为防止重复查询数据库, 再次验证      T value = map.get(key);      if (value == null) {        // 如果没有, 查询数据库        value = genericDao.queryOne(beanClass, sql, params);         map.put(key, value);      }      return value;     } finally {      lock.writeLock().unlock();     }  }    // 作为 key 保证其是不可变的   class SqlPair {    private String sql;     private Object[] params;    public SqlPair(String sql, Object[] params) {       this.sql = sql;      this.params = params;     }    @Override    public boolean equals(Object o) {      if (this == o) {        return true      }      if (o == null || getClass() != o.getClass()) {         return false;      }      SqlPair sqlPair = (SqlPair) o;       return sql.equals(sqlPair.sql) &&            Arrays.equals(params, sqlPair.params);    }    @Override    public int hashCode() {      int result = Objects.hash(sql);      result = 31 * result + Arrays.hashCode(params);       return result;    }   }}

注意

  • 以上实现体现的是读写锁的应用,保证缓存和数据库的一致性,但有下面的问题没有考虑

    • 适合读多写少,如果写操作比较频繁,以上实现性能低 没有考虑缓存容量 

    • 没有考虑缓存过期

    • 只适合单机

    • 并发性还是低,目前只会用一把锁

    • 更新方法太过简单粗暴,清空了所有 key(考虑按类型分区或重新设计 key)

  • 乐观锁实现:用 CAS 去更新

三.读写锁原理

1. 图解流程

读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个

t1 w.lock,t2 r.lock

1) t1 成功上锁,流程与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁 使用的是 state 的高 16 位

c19510efd0173427efdca8194f30bb9f.png

2) t2 执行 r.lock,这时进入读锁的 sync.acquireShared(1) 流程,首先会进入 tryAcquireShared 流程。如果有写 锁占据,那么 tryAcquireShared 返回 -1 表示失败

tryAcquireShared 返回值表示

  • -1 表示失败

  • 0 表示成功,但后继节点不会继续唤醒

  • 正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回 1

c3f35b73fbc8082cffe388260aa4619b.png

3) 这时会进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态

4)t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁

5)如果没有成功,在 doAcquireShared 内 for (;;) 循环一次,把前驱节点的 waitStatus 改为 -1,再 for (;;) 循环一 次尝试 tryAcquireShared(1) 如果还不成功,那么在 parkAndCheckInterrupt() 处 park

4950d9f88fd7abd84801704c01af076e.png

t3 r.lock,t4 w.lock
这种状态下,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子

8ead2502d692df3e5f8ec610eeb45b64.png

t1 w.unlock
这时会走到写锁的 sync.release(1) 流程,调用 sync.tryRelease(1) 成功,变成下面的样子

7264dadfef5f9271c7f15dd6b60b747a.png

接下来执行唤醒流程 sync.unparkSuccessor,即让老二恢复运行,这时 t2 在 doAcquireShared 内parkAndCheckInterrupt() 处恢复运行

这回再来一次 for (;;) 执行 tryAcquireShared 成功则让读锁计数加一

c5f87681e0b9d5022be2f35769678836.png

这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点

e001cce24031e5ab6fb0aca6117ec023.png

事情还没完,在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared,如果是则调用doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒老二,这时 t3 在 doAcquireShared 内parkAndCheckInterrupt() 处恢复运行

edf35941e7d1c3786efedd78485cf675.png

这回再来一次 for (;;) 执行 tryAcquireShared 成功则让读锁计数加一

bb27770d8214c03ffaa161593910b23c.png

这时 t3 已经恢复运行,接下来 t3 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点

ae0ae36dd7ff07727bc528e360d715e6.png

下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点t2 r.unlock,t3 r.unlock
t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但由于计数还不为零

31f9440f27c69a3517bc461bb30d0613.png

t3 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,这回计数为零了,进入doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二,即

91bdb15ab11a1dd0424ad33e8ecf287d.png

之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;;) 这次自己是老二,并且没有其他 竞争,tryAcquire(1) 成功,修改头结点,流程结束

21b868941def11666a141b05a906c051.png

2. 源码分析

写锁上锁流程

static final class NonfairSync extends Sync {  // ... 省略无关代码  // 外部类 WriteLock 方法, 方便阅读, 放在此处   public void lock() {    sync.acquire(1);   }  // AQS 继承过来的方法, 方便阅读, 放在此处   public final void acquire(int arg) {    if (      // 尝试获得写锁失败      !tryAcquire(arg) &&      // 将当前线程关联到一个 Node 对象上, 模式为独占模式       // 进入 AQS 队列阻塞       acquireQueued(addWaiter(Node.EXCLUSIVE), arg)    ){       selfInterrupt();    }   }    // Sync 继承过来的方法, 方便阅读, 放在此处  protected final boolean tryAcquire(int acquires) {    // 获得低 16 位, 代表写锁的 state 计数     Thread current = Thread.currentThread();    int c = getState();     int w = exclusiveCount(c);    if (c != 0) {       if (        // c != 0 and w == 0 表示有读锁, 或者         w == 0 ||        // 如果 exclusiveOwnerThread 不是自己         current != getExclusiveOwnerThread()      ){        // 获得锁失败        return false;      }      // 写锁计数超过低 16 位, 报异常      if (w + exclusiveCount(acquires) > MAX_COUNT)        throw new Error("Maximum lock count exceeded");       // 写锁重入, 获得锁成功      setState(c + acquires);      return true;    }     if (      // 判断写锁是否该阻塞, 或者      writerShouldBlock() ||      // 尝试更改计数失败       !compareAndSetState(c, c + acquires)    ){      // 获得锁失败      return false;     }    // 获得锁成功     setExclusiveOwnerThread(current);     return true;  }  // 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞   final boolean writerShouldBlock() {    return false; }  }

写锁释放流程

static final class NonfairSync extends Sync {   // ... 省略无关代码  // WriteLock 方法, 方便阅读, 放在此处   public void unlock() {    sync.release(1);   }  // AQS 继承过来的方法, 方便阅读, 放在此处   public final boolean release(int arg) {    // 尝试释放写锁成功    if (tryRelease(arg)) {      // unpark AQS 中等待的线程      Node h = head;      if (h != null && h.waitStatus != 0)        unparkSuccessor(h);       return true;    }    return false;   }    // Sync 继承过来的方法, 方便阅读, 放在此处  protected final boolean tryRelease(int releases) {    if (!isHeldExclusively())      throw new IllegalMonitorStateException();    int nextc = getState() - releases;    // 因为可重入的原因, 写锁计数为 0, 才算释放成功     boolean free = exclusiveCount(nextc) == 0;     if (free) {      setExclusiveOwnerThread(null);     }    setState(nextc);    return free;   }}

读锁上锁流程

static final class NonfairSync extends Sync {  // ReadLock 方法, 方便阅读, 放在此处   public void lock() {    sync.acquireShared(1);     }    // AQS 继承过来的方法, 方便阅读, 放在此处     public final void acquireShared(int arg) {      // tryAcquireShared 返回负数, 表示获取读锁失败       if (tryAcquireShared(arg) < 0) {        doAcquireShared(arg);       }    }  // Sync 继承过来的方法, 方便阅读, 放在此处  protected final int tryAcquireShared(int unused) {    Thread current = Thread.currentThread();     int c = getState();    // 如果是其它线程持有写锁, 获取读锁失败    if (      exclusiveCount(c) != 0 &&      getExclusiveOwnerThread() != current     ){      return -1;    }    int r = sharedCount(c);     if (      // 读锁不该阻塞(如果老二是写锁,读锁该阻塞), 并且       !readerShouldBlock() &&      // 小于读锁计数, 并且      r < MAX_COUNT &&      // 尝试增加计数成功      compareAndSetState(c, c + SHARED_UNIT)     ){      // ... 省略不重要的代码      return 1;     }    return fullTryAcquireShared(current);   }  // 非公平锁 readerShouldBlock 看 AQS 队列中第一个节点是否是写锁 // true 则该阻塞, false 则不阻塞  final boolean readerShouldBlock() {    return apparentlyFirstQueuedIsExclusive();   }  // AQS 继承过来的方法, 方便阅读, 放在此处  // 与 tryAcquireShared 功能类似, 但会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞   final int fullTryAcquireShared(Thread current) {    HoldCounter rh = null;     for (;;) {      int c = getState();      if (exclusiveCount(c) != 0) {        if (getExclusiveOwnerThread() != current)           return -1;      }       else if (readerShouldBlock()) {         // ... 省略不重要的代码      }      if (sharedCount(c) == MAX_COUNT)          throw new Error("Maximum lock count exceeded");       if (compareAndSetState(c, c + SHARED_UNIT)) {        // ... 省略不重要的代码        return 1;       }    }   }  // AQS 继承过来的方法, 方便阅读, 放在此处   private void doAcquireShared(int arg) {    // 将当前线程关联到一个 Node 对象上, 模式为共享模式 final Node node = addWaiter(Node.SHARED); boolean failed = true;    try {      boolean interrupted = false;       for (;;) {        final Node p = node.predecessor();         if (p == head) {             // 再一次尝试获取读锁          int r = tryAcquireShared(arg);           // 成功          if (r >= 0) {            // (一)            // r 表示可用资源数, 在这里总是 1 允许传播             //(唤醒 AQS 中下一个 Share 节点)             setHeadAndPropagate(node, r);            p.next = null; // help GC            if (interrupted)              selfInterrupt(); failed = false;            return;           }        } if (          // 是否在获取读锁失败时阻塞(前一个阶段 waitStatus == Node.SIGNAL)           shouldParkAfterFailedAcquire(p, node) &&          // park 当前线程          parkAndCheckInterrupt()        ){          interrupted = true;        }       }    } finally {      if (failed)        cancelAcquire(node);    }   }  // (一) AQS 继承过来的方法, 方便阅读, 放在此处  private void setHeadAndPropagate(Node node, int propagate) {    Node h = head; // Record old head for check below     // 设置自己为 head    setHead(node);    // propagate 表示有共享资源(例如共享读锁或信号量)    // 原 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE     // 现在 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE     if (propagate > 0 || h == null || h.waitStatus < 0 ||        (h = head) == null || h.waitStatus < 0) {       Node s = node.next;      // 如果是最后一个节点或者是等待共享读锁的节点       if (s == null || s.isShared()) {        // 进入 (二)        doReleaseShared();       }    }   }  // (二) AQS 继承过来的方法, 方便阅读, 放在此处   private void doReleaseShared() {    // 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark    // 如果 head.waitStatus == 0 ==> Node.PROPAGATE, 为了解决 bug, 见后面分析     for (;;) {      Node h = head;      // 队列还有节点      if (h != null && h != tail) {        int ws = h.waitStatus;         if (ws == Node.SIGNAL) {          if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))             continue; // loop to recheck cases          // 下一个节点 unpark 如果成功获取读锁          // 并且下下个节点还是 shared, 继续 doReleaseShared           unparkSuccessor(h);      }      else if (ws == 0 &&          !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))        continue; // loop on failed CAS    }    if (h == head)  // loop if head changed      break;     }  } }

读锁释放流程

static final class NonfairSync extends Sync {  // ReadLock 方法, 方便阅读, 放在此处   public void unlock() {    sync.releaseShared(1);   }  // AQS 继承过来的方法, 方便阅读, 放在此处  public final boolean releaseShared(int arg) {    if (tryReleaseShared(arg)) {       doReleaseShared();       return true;    }    return false;   }  // Sync 继承过来的方法, 方便阅读, 放在此处  protected final boolean tryReleaseShared(int unused) {    // ... 省略不重要的代码     for (;;) {      int c = getState();      int nextc = c - SHARED_UNIT;      if (compareAndSetState(c, nextc)) {        // 读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程         // 计数为 0 才是真正释放        return nextc == 0;      }    }   }  // AQS 继承过来的方法, 方便阅读, 放在此处  private void doReleaseShared() {    // 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark     // 如果 head.waitStatus == 0 ==> Node.PROPAGATE    for (;;) {      Node h = head;      if (h != null && h != tail) {        int ws = h.waitStatus;        // 如果有其它线程也在释放读锁,那么需要将 waitStatus 先改为 0         // 防止 unparkSuccessor 被多次执行        if (ws == Node.SIGNAL) {          if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))             continue; // loop to recheck cases          unparkSuccessor(h);         }        // 如果已经是 0 了,改为 -3,用来解决传播性,见后文信号量 bug 分析        else if (ws == 0 &&            !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))          continue;      }      if (h == head)        break;     }  } }

StampedLock
该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用

加解读锁

 long stamp = lock.readLock();  lock.unlockRead(stamp);

加解写锁

 long stamp = lock.writeLock();  lock.unlockWrite(stamp);

乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通 过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。

 long stamp = lock.tryOptimisticRead(); // 验戳if(!lock.validate(stamp)){// 锁升级 }

提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法

classDataContainerStamped {   privateint data;  privatefinal StampedLock lock = new StampedLock();  public DataContainerStamped(int data) {     this.data = data;  }  public int read(int readTime) {    long stamp = lock.tryOptimisticRead();     log.debug("optimistic read locking...{}", stamp);     sleep(readTime);    if (lock.validate(stamp)) {      log.debug("read finish...{}, data:{}", stamp, data);      return data;     }    // 锁升级 - 读锁    log.debug("updating to read lock... {}", stamp);     try {      stamp = lock.readLock();      log.debug("read lock {}", stamp);      sleep(readTime);      log.debug("read finish...{}, data:{}", stamp, data);       return data;    } finally {      log.debug("read unlock {}", stamp);       lock.unlockRead(stamp);    }   }  public void write(int newData) {    long stamp = lock.writeLock();     log.debug("write lock {}", stamp);     try {      sleep(2);      this.data = newData;     } finally {      log.debug("write unlock {}", stamp);      lock.unlockWrite(stamp);     }  } }

测试 读-读 可以优化

public static void main(String[] args) {  DataContainerStamped dataContainer = new DataContainerStamped(1);   new Thread(() -> {    dataContainer.read(1);   }, "t1").start(); sleep(0.5);    new Thread(() -> {    dataContainer.read(0);   }, "t2").start();}

输出结果,可以看到实际没有加读锁

 15:58:50.217 c.DataContainerStamped [t1] - optimistic read locking...256  15:58:50.717 c.DataContainerStamped [t2] - optimistic read locking...256  15:58:50.717 c.DataContainerStamped [t2] - read finish...256, data:1  15:58:51.220 c.DataContainerStamped [t1] - read finish...256, data:1

测试 读-写 时优化读补加读锁

publicstatic void main(String[] args) {  DataContainerStamped dataContainer = new DataContainerStamped(1);   new Thread(() -> {    dataContainer.read(1);   }, "t1").start();     sleep(0.5);    new Thread(() -> {    dataContainer.write(100);   }, "t2").start();}

输出结果

15:57:00.219c.DataContainerStamped [t1] - optimistic read locking...256 15:57:00.717c.DataContainerStamped [t2] - write lock 38415:57:01.225 c.DataContainerStamped [t1] - updating to read lock... 256 15:57:02.719 c.DataContainerStamped [t2] - write unlock 384 15:57:02.719 c.DataContainerStamped [t1] - read lock 51315:57:03.719 c.DataContainerStamped [t1] - read finish...513, data:1000 15:57:03.719 c.DataContainerStamped [t1] - read unlock 513

注意StampedLock 不支持条件变量

StampedLock 不支持可重入

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值