[Java] StackOverflowError and threads waiting for ReentrantReadWriteLock

原文はこちら。
https://blogs.oracle.com/poonam/stackoverflowerror-and-threads-waiting-for-reentrantreadwritelock

「Stuck Threads」や「Hang」という言葉になじみがあって怖いと感じる場合には、ぜひお読みください。 Weblogic Server(WLS)がいくつかのスレッドで「スタックしている(Stuck)」または「進捗していない(not making any progress)」と報告している状況に出くわすことがありますが、そうしたスレッドのスタックトレースやプロセス内の他のスレッドは絶対に正常に見えます。 それらのスレッドがスタックする理由と様子を示すものが何も現れません。
最近このような状況を分析しなければならなくなりました。アプリケーションのスレッドはスタックしているとのレポートで、ReentrantReadWriteLockオブジェクトを待っていました。 WriteLockを取得しようとしていたスレッドを除き、その他すべてのスレッドは、ReentrantReadWriteLockオブジェクトのReadLockの取得を待機していました。
Class ReentrantReadWriteLock
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html
ReadLockを待っているスレッドのスタックトレースは以下のようでした。
"[STUCK] ExecuteThread: '20' for queue: 'weblogic.kernel.Default (self-tuning)'" #127 daemon prio=1 os_prio=0 tid=0x00007f9c01e1c800 nid=0xb92 waiting on condition [0x00007f9b66fe9000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x00000006c1e34768> (a java.util.concurrent.locks.ReentrantReadWriteLock$FairSync)
    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.sun.jmx.mbeanserver.Repository.retrieve(Repository.java:487)
    at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.getMBean(DefaultMBeanServerInterceptor.java:1088)
    at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.getClassLoaderFor(DefaultMBeanServerInterceptor.java:1444)
    at com.sun.jmx.mbeanserver.JmxMBeanServer.getClassLoaderFor(JmxMBeanServer.java:1324)
    .... 

    Locked ownable synchronizers:
    - None  
以下はWriteLockを待機しているスレッドのスタックトレースです。
"[STUCK] ExecuteThread: '19' for queue: 'weblogic.kernel.Default (self-tuning)'" #126 daemon prio=1 os_prio=0 tid=0x00007f9c01e1b000 nid=0xb91 waiting on condition [0x00007f9b670ea000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x00000006c1e34768> (a java.util.concurrent.locks.ReentrantReadWriteLock$FairSync)
    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.sun.jmx.mbeanserver.Repository.addMBean(Repository.java:416)
    at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.registerWithRepository(DefaultMBeanServerInterceptor.java:1898)
    ... 

   Locked ownable synchronizers:
    - None 
スレッドのダンプ内のどのスレッドも、このReentrantReadWriteLockロック(0x00000006c1e34768)を保持していると報告されていませんでした。では、何が間違っていたのでしょう。これら全てのスレッドが待機していた理由は何だったのでしょう。
幸いにも、この状況が発生したときにヒープダンプを収集しました。ヒープダンプから、このReentrantReadWriteLockオブジェクトは以下のような状況であることがわかりました。
Java Platform, Standard Edition Troubleshooting Guide Release 8
Diagnose Leaks in Java Language Code - Create a Heap Dump
https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/memleaks004.html#CIHJIIBA
 0x6c1e34768: ReentrantReadWriteLock$FairSync

Type   |     Name      |      Value
-----------------------------------------------
int |firstReaderHoldCount|1
ref |firstReader         |[STUCK] ExecuteThread: '19' for queue: 'weblogic.kernel.Default (self-tuning)'
ref |cachedHoldCounter   |java.util.concurrent.locks.ReentrantReadWriteLock$Sync$HoldCounter @ 0x7bbd32b70
ref |readHolds           |java.util.concurrent.locks.ReentrantReadWriteLock$Sync$ThreadLocalHoldCounter @ 0x6c1e34798
int |state               |65536
ref |tail                |java.util.concurrent.locks.AbstractQueuedSynchronizer$Node @ 0x781c5f9f0
ref |head                |java.util.concurrent.locks.AbstractQueuedSynchronizer$Node @ 0x7bb77d640
ref |exclusiveOwnerThread|null
---------------------------------------------
ヒープダンプから、このconcurrent lock上のReadLockがすでに保持されていて、ReadLockを保持しているスレッドが実際にスレッド '19'自身であって、WriteLockを取得するのを待っていたことがわかります。 ReentrantReadWriteLockの設計によれば、ReentrantReadWriteLockオブジェクト上のReadLockが現在のスレッドまたは他のスレッドによって既に保持されている場合、そのスレッドはReentrantReadWriteLockオブジェクトに対して書き込みロックを取得できません。これこそがデッドロックの原因でした。スレッド19は、そのReentrantReadWriteLockオブジェクト上に保持されているReadLockが解放されるまでWriteLockを待機し続けます。
しかしどのようにして発生したのでしょう。com.sun.jmx.mbeanserver.Repositoryクラスファイルのコードを見ると、ReadLockを取得するすべてのメソッドがfinally節でそれを怠りなく解放します。
http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/com/sun/jmx/mbeanserver/Repository.java#l344
そのため、スレッド '19'が、ReadLockを取得するRepositoryのメソッドの1つを呼び出すと、取得されたReadLockはその呼び出しの最後にロックが解放されていなければなりません。もちろん、finally節でReadLockのロック解放するコードがスキップされなければ、という前提です。これは、finallyブロック内のReadLockでunlock()を実行する前に、そのスレッドがStackOverflowErrorを検出した場合にのみ発生する可能性があります。
また、ReadLock.lock()に至るコントロールパスではなく、アプリケーションの実行中にStackOverflowErrorsが発生したというアプリケーションログの証拠もあります。
 <java.lang.StackOverflowError>
 <at java.security.AccessController.doPrivileged(Native Method)>
 <at java.net.URLClassLoader$3.next(URLClassLoader.java:598)>
 <at java.net.URLClassLoader$3.hasMoreElements(URLClassLoader.java:623)>
 [...] 
ということで、StackOverflowErrorが発生し、WriteLockを待っていたのが同じスレッド(スレッド19)だったので、このスレッドはそのStackOverflowErrorを飲み込んだ可能性があることがわかりましたが、スタックトレースに現れるJDKフレームのコードを調べた後、Error/Throwableをキャッチして無視している箇所を見つけることはできませんでした。そのため、アプリケーションが問題のスレッドの呼び出しスタックのどこかでStackOverflowErrorを無視し、飲み込んでいる可能性が高いようです。
したがって、-Xssオプションを使用してスレッドスタックサイズを増やしてアプリケーションを実行し、コードを調べてStackOverflowErrorをどこかでもみ消しているかどうか確認することを提案しました。スタックサイズが増やしてこのアプリケーションを数日間テストしましたが、この手のハングアップはもう発生しませんでした。
今回の作業で以下の3点の学びがありました。
  1. ReentrantReadWriteLockでReadLockを保持している間にスレッドがWriteLockを取得しようとしないように、常に確認してください。ReentrantReadWriteLock#getReadHoldCount()を呼び出せば、WriteLockを取得する前に現在のスレッドのReadLockカウントを取得できます。
  2. StackOverflowErrorを抑制・隠蔽する可能性があるため、Error/Throwableをキャッチして無視しないようにしてください。無視していると、concurrent lockオブジェクトが矛盾した状態になる可能性があります。
  3. アプリケーションスレッドで発生する可能性があるStackOverflowErrorsを回避するために、-Xss JVMオプションを使用してスレッドのスタックサイズを増やすという回避策があります。
お役に立てると幸いです、それではまた!

0 件のコメント:

コメントを投稿