ReentrantReadWriteLock死锁分析

4 篇文章 0 订阅

背景

服务底层核心逻辑使用ReentrantReadWriteLock控制缓存,出现死锁后,缓存读写全部阻塞。排查线程dump发现,等待的对象既没有被读锁获取,也没有被写锁获取。

相关概念

1. 写锁的获取和释放

写锁 WriteLock是支持重进入的排他锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取读锁时,读锁已经被获取或者该线程不是已获取写锁的线程,则当前线程进入等待状态。读写锁确保写锁的操作对读锁可见。写锁释放每次减少写状态,当前写状态为0时表示写锁已被释放。

2.读锁的获取与释放

读锁 ReadLock是支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(写状态为0)时,读锁总是能够被成功地获取,而所做的也只是增加读状态(线程安全)。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已经被获取,则进入等待状态。

3. 锁降级与锁升级

锁降级:是写锁降级成为读锁。锁降级是指把持住当前拥有的写锁的同时,再获取到读锁,随后释放写锁的过程。
锁升级:是读锁升级为写锁。锁升级至指把持当前拥有的读锁的同时,再获取写锁。锁升级一般是通过

锁为什么能够降级,却不能够升级?

读写锁是互斥的,读的时候不能写。但是针对缓存操作先读后写其实是很常见的操作,此处只是java当前版本未支持锁升级。.NET对于锁升级可以使用ReaderWriterLockSlim
Java版本的当前

可能原因

1. 锁升级

加入服务先获取了读锁,然后又尝试获取写锁,就会发生锁升级。服务JDK版本为1.8,当前版本 ReentrantReadWriteLock并不支持锁升级操作

线程dump如下所示,对象0x000000076abe9300

"Thread-readLock-1" #11 prio=5 os_prio=31 tid=0x00007ffd5b092000 nid=0xa803 waiting on condition [0x0000700002118000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x000000076abe9300> (a java.util.concurrent.locks.ReentrantReadWriteLock$NonfairSync)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireShared(AbstractQueuedSynchronizer.java:967)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireShared(AbstractQueuedSynchronizer.java:1283)
	at java.util.concurrent.locks.ReentrantReadWriteLock$ReadLock.lock(ReentrantReadWriteLock.java:727)
	at com.example.test.TestReentrant.lambda$main$1(TestReentrant.java:47)
	at com.example.test.TestReentrant$$Lambda$2/142257191.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
	- None


"Thread-writeLock" #13 prio=5 os_prio=31 tid=0x00007ffd5b091000 nid=0xa703 waiting on condition [0x000070000221b000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x000000076abe9300> (a java.util.concurrent.locks.ReentrantReadWriteLock$NonfairSync)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
	at java.util.concurrent.locks.ReentrantReadWriteLock$WriteLock.lock(ReentrantReadWriteLock.java:943)
	at com.example.test.TestReentrant.lambda$main$3(TestReentrant.java:86)
	at com.example.test.TestReentrant$$Lambda$4/1826771953.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
	- None

堆转储分析对象0x000000076abe9300未被释放
堆转储

Java Doc中示例写法

  1. Lock downgrading 锁降级 (用于减少写锁耗时;保证其他线程写锁被阻塞,数据不被改变)

Sample usages. Here is a code sketch showing how to perform lock downgrading after updating a cache (exception handling is particularly tricky when handling multiple locks in a non-nested fashion):

class CachedData {
  Object data;
  volatile boolean cacheValid;
  final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

  void processCachedData() {
    rwl.readLock().lock();
    if (!cacheValid) {
      // Must release read lock before acquiring write lock
      rwl.readLock().unlock();
      rwl.writeLock().lock();
      try {
        // Recheck state because another thread might have
        // acquired write lock and changed state before we did.
        if (!cacheValid) {
          data = ...
          cacheValid = true;
        }
        // Downgrade by acquiring read lock before releasing write lock
        rwl.readLock().lock();
      } finally {
        rwl.writeLock().unlock(); // Unlock write, still hold read
      }
    }

    try {
      use(data);
    } finally {
      rwl.readLock().unlock();
    }
  }
}

声明了volatile类型的cacheValid变量,保证可见性。先获取读锁,如果cache不可用,则释放读锁,获取写锁,在更改数据之前,再检查一次cacheValid的值(防止别的写操作已经改变了缓存状态),然后修改数据,将cacheValid置为true,然后在释放写锁前获取读锁;此时,cache中数据可用,处理cache中数据,最后释放读锁。这个过程就是一个完整的锁降级的过程,目的是保证数据可见性,如果当前的线程C在修改完cache中的数据后,没有获取读锁而是直接释放了写锁,那么假设此时另一个线程T获取了写锁并修改了数据,那么C线程无法感知到数据已被修改,则数据出现错误。如果遵循锁降级的步骤,线程C在释放写锁之前获取读锁,那么线程T在获取写锁时将被阻塞,直到线程C完成数据处理过程,释放读锁。

  1. 读多写少,并且远超同步读取的时

ReentrantReadWriteLocks can be used to improve concurrency in some uses of some kinds of Collections. This is typically worthwhile only when the collections are expected to be large, accessed by more reader threads than writer threads, and entail operations with overhead that outweighs synchronization overhead. For example, here is a class using a TreeMap that is expected to be large and concurrently accessed.


class RWDictionary {
  private final Map<String, Data> m = new TreeMap<String, Data>();
  private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
  private final Lock r = rwl.readLock();
  private final Lock w = rwl.writeLock();

  public Data get(String key) {
    r.lock();
    try { return m.get(key); }
    finally { r.unlock(); }
  }
  public String[] allKeys() {
    r.lock();
    try { return m.keySet().toArray(); }
    finally { r.unlock(); }
  }
  public Data put(String key, Data value) {
    w.lock();
    try { return m.put(key, value); }
    finally { w.unlock(); }
  }
  public void clear() {
    w.lock();
    try { m.clear(); }
    finally { w.unlock(); }
  }
}

死锁

1. 锁升级造成的死锁
final ReadWriteLock lock = new ReentrantReadWriteLock();
lock.getReadLock().lock();

// In real code we would go call other methods that end up calling back and
// thus locking again
lock.getReadLock().lock();

// Now we do some stuff and realise we need to write so try to escalate the
// lock as per the Javadocs and the above description
lock.getReadLock().unlock(); // Does not actually release the lock
lock.getWriteLock().lock();  // Blocks as some thread (this one!) holds read lock

System.out.println("Will never get here");
2. Stack Overflow造成死锁

finally其实不难保证锁绝对被释放。如果deepCall中存在StackOverflowError,JVM在执行finally的unlock方法时又触发另外一个StackOverflowError,那么该锁将永远无法释放

Although JEP 270 makes lock and unlock methods somewhat atomic, it does not guarantee the invocation of these methods will always succeed. Unfortunately, the simplest lock-unlock pattern remains fragile:

  lock.lock();
  try {
      deepCall();
  } finally {
      lock.unlock();
  }

If StackOverflowError happens here inside deepCall, JVM will attempt to execute the finally block, but a call to unlock method may result in another StackOverflowError, leaving the object locked forever. For some reason ReentrantLock.unlock() method itself is not annotated with @ReservedStackAccess 😞

参考文档

1. Oracle Doc ReentrantReadWriteLock
2. 读写锁ReentrantReadWriteLock之锁降级
3. Deadlock: Who Owns the Lock?
4. Stack Overflow handling in HotSpot JVM
5. ReentrantLock、ReentrantReadWriteLock、StampedLock讲解
6. Java ReentrantReadWriteLocks - how to safely acquire write lock when in a read lock?
7. What is “Locked ownable synchronizers” in thread dump?
8. 并发 - ReentrantReadWriteLock
9. https://lotabout.me/books/Java-Concurrency/Source-Lock/ReentrantLock.html
10. Upgrading and Downgrading Locks

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值