/* * eXist Open Source Native XML Database * Copyright (C) 2005-2007 The eXist Project * http://exist-db.org * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * Original code is * * Copyright 2001-2004 The Apache Software Foundation. * * Licensed under the Apache License, Version 2.0 (the "License") * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $Id$ */ package org.exist.storage.lock; import org.apache.log4j.Logger; import org.exist.util.DeadlockException; import org.exist.util.LockException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; /** * A reentrant read/write lock, which allows multiple readers to acquire a lock. * Waiting writers are preferred. * <p/> * This is an adapted and bug-fixed version of code taken from Apache's Turbine * JCS. */ public class MultiReadReentrantLock implements Lock { private final static Logger LOG = Logger.getLogger(MultiReadReentrantLock.class); private Object id; /** * Number of threads waiting to read. */ private int waitingForReadLock = 0; /** * Number of threads reading. */ private List outstandingReadLocks = new ArrayList(4); /** * The thread that has the write lock or null. */ private Thread writeLockedThread; /** * The number of (nested) write locks that have been requested from * writeLockedThread. */ private int outstandingWriteLocks = 0; /** * Threads waiting to get a write lock are tracked in this ArrayList to * ensure that write locks are issued in the same order they are requested. */ private List waitingForWriteLock = null; private ReentrantLock lock = DeadlockDetection.getLock(); private Condition monitor = lock.newCondition(); private Condition writeMonitor = lock.newCondition(); /** * Default constructor. */ public MultiReadReentrantLock(Object id) { this.id = id; } public String getId() { return id.toString(); } /* @deprecated Use other method * @see org.exist.storage.lock.Lock#acquire() */ public boolean acquire() throws LockException { return acquire(Lock.READ_LOCK); } public boolean acquire(int mode) throws LockException { if (mode == Lock.NO_LOCK) { LOG.warn("acquired with no lock !"); return true; } switch (mode) { case Lock.WRITE_LOCK: return writeLock(true); default: return readLock(true); } } /* (non-Javadoc) * @see org.exist.util.Lock#attempt(int) */ public boolean attempt(int mode) { try { switch (mode) { case Lock.WRITE_LOCK: return writeLock(false); default: return readLock(false); } } catch (LockException e) { return false; } } /** * Issue a read lock if there is no outstanding write lock or threads * waiting to get a write lock. Caller of this method must be careful to * avoid synchronizing the calling code so as to avoid deadlock. * @param waitIfNecessary whether to wait if the lock is not available right away */ private boolean readLock(boolean waitIfNecessary) throws LockException { lock.lock(); try { final Thread thisThread = Thread.currentThread(); if (writeLockedThread == thisThread) { // add acquired lock to the current list of read locks outstandingReadLocks.add(new LockOwner(thisThread)); // LOG.debug("Thread already holds a write lock"); return true; } deadlockCheck(); waitingForReadLock++; if (writeLockedThread != null) { if (!waitIfNecessary) return false; WaitingThread waiter = new WaitingThread(thisThread, monitor, this, Lock.READ_LOCK); DeadlockDetection.addResourceWaiter(thisThread, waiter); while (writeLockedThread != null) { // LOG.debug("readLock wait by " + thisThread.getName() + " for " + getId()); waiter.doWait(); // LOG.debug("wake up from readLock wait"); } DeadlockDetection.clearResourceWaiter(thisThread); } // LOG.debug("readLock acquired by thread: " + Thread.currentThread().getName()); waitingForReadLock--; // add acquired lock to the current list of read locks outstandingReadLocks.add(new LockOwner(thisThread)); return true; } finally { lock.unlock(); } } /** * Issue a write lock if there are no outstanding read or write locks. * Caller of this method must be careful to avoid synchronizing the calling * code so as to avoid deadlock. * @param waitIfNecessary whether to wait if the lock is not available right away */ private boolean writeLock(boolean waitIfNecessary) throws LockException { Thread thisThread = Thread.currentThread(); WaitingThread waiter; lock.lock(); try { if (writeLockedThread == thisThread) { outstandingWriteLocks++; // LOG.debug("acquired additional write lock on " + getId()); return true; } if (writeLockedThread == null && grantWriteLock()) { writeLockedThread = thisThread; outstandingWriteLocks++; // LOG.debug( "writeLock on " + getId() + " acquired without waiting by " + writeLockedThread.getName()); return true; } if (!waitIfNecessary) return false; // if (writeLockedThread == thisThread) { // LOG.debug("nested write lock: " + outstandingWriteLocks); // } deadlockCheck(); if (waitingForWriteLock == null) waitingForWriteLock = new ArrayList(3); waiter = new WaitingThread(thisThread, writeMonitor, this, Lock.WRITE_LOCK); addWaitingWrite(waiter); DeadlockDetection.addResourceWaiter(thisThread, waiter); List deadlockedThreads = null; LockException exceptionCaught = null; if (thisThread != writeLockedThread) { while (thisThread != writeLockedThread && deadlockedThreads == null) { // LOG.debug("writeLock wait on " + getId() + ". held by " + (writeLockedThread == null ? "null" : writeLockedThread.getName()) // + ". outstanding: " + outstandingWriteLocks); if (LockOwner.DEBUG) { StringBuffer buf = new StringBuffer("Waiting for write: "); for (int i = 0; i < waitingForWriteLock.size(); i++) { buf.append(' '); buf.append(((WaitingThread) waitingForWriteLock.get(i)).getThread().getName()); } LOG.debug(buf.toString()); debugReadLocks("WAIT"); } deadlockedThreads = checkForDeadlock(thisThread); if (deadlockedThreads == null) { try { waiter.doWait(); } catch (LockException e) { // Don't throw the exception now, leave the synchronized block and clean up first exceptionCaught = e; break; } } } if (deadlockedThreads == null && exceptionCaught == null) outstandingWriteLocks++; //testing // LOG.debug( "writeLock on " + getId() + " acquired by " + writeLockedThread.getName()); } DeadlockDetection.clearResourceWaiter(thisThread); removeWaitingWrite(waiter); if (exceptionCaught != null) throw exceptionCaught; if (deadlockedThreads != null) { for (int i = 0; i < deadlockedThreads.size(); i++) { WaitingThread wt = (WaitingThread) deadlockedThreads.get(i); wt.signalDeadlock(); } throw new DeadlockException(); } } finally { lock.unlock(); } return true; } private void addWaitingWrite(WaitingThread waiter) { waitingForWriteLock.add(waiter); } private void removeWaitingWrite(WaitingThread waiter) { for (int i = 0; i < waitingForWriteLock.size(); i++) { WaitingThread next = (WaitingThread) waitingForWriteLock.get(i); if (next.getThread() == waiter.getThread()) { waitingForWriteLock.remove(i); break; } } } /* @deprecated : use other method * @see org.exist.storage.lock.Lock#release() */ public void release() { release(Lock.READ_LOCK); } public void release(int mode) { switch (mode) { case Lock.NO_LOCK: break; case Lock.WRITE_LOCK: releaseWrite(1); break; default: releaseRead(1); break; } } public void release(int mode, int count) { switch (mode) { case Lock.WRITE_LOCK: releaseWrite(count); break; default: releaseRead(count); break; } } private void releaseWrite(int count) { lock.lock(); try { if (Thread.currentThread() == writeLockedThread) { //log.info( "outstandingWriteLocks= " + outstandingWriteLocks ); if (outstandingWriteLocks > 0) outstandingWriteLocks -= count; // else { // LOG.info("extra lock release, writelocks are " + outstandingWriteLocks + "and done was called"); // } if (outstandingWriteLocks > 0) { // LOG.debug("writeLock released for a nested writeLock request: " + outstandingWriteLocks + // "; thread: " + writeLockedThread.getName()); return; } // if another thread is waiting for a write lock, we immediately pass control to it. // no further checks should be required here. if (grantWriteLockAfterRead()) { WaitingThread waiter = (WaitingThread) waitingForWriteLock.get(0); removeWaitingWrite(waiter); DeadlockDetection.clearResourceWaiter(waiter.getThread()); writeLockedThread = waiter.getThread(); // if (LOG.isDebugEnabled()) { // LOG.debug("writeLock released and before notifying a write lock waiting thread " + writeLockedThread); // } writeMonitor.signalAll(); // if (LOG.isDebugEnabled()) { // LOG.debug("writeLock released by " + Thread.currentThread().getName() + // " after notifying a write lock waiting thread " + writeLockedThread.getName()); // } } else { writeLockedThread = null; if (waitingForReadLock > 0) { // LOG.debug("writeLock " + Thread.currentThread().getName() + " released, notified waiting readers"); // wake up pending read locks monitor.signalAll(); } // } else { // LOG.debug("writeLock released, no readers waiting"); // } } } else { LOG.warn("Possible lock problem: a thread released a write lock it didn't hold. Either the " + "thread was interrupted or it never acquired the lock.", new Throwable()); } // LOG.debug("writeLock released: " + getId() + "; outstanding: " + outstandingWriteLocks + // "; thread: " + Thread.currentThread().getName() + " suspended: " + suspendedThreads.size()); } finally { lock.unlock(); } } /** * Threads call this method to relinquish a lock that they previously got * from this object. * * @throws IllegalStateException if called when there are no outstanding locks or there is a * write lock issued to a different thread. */ private void releaseRead(int count) { lock.lock(); try { if (!outstandingReadLocks.isEmpty()) { removeReadLock(count); // if (LOG.isDebugEnabled()) { // LOG.debug("readLock on " + getId() + " released by " + Thread.currentThread().getName()); // LOG.debug("remaining read locks: " + listReadLocks()); // } if (writeLockedThread == null && grantWriteLockAfterRead()) { WaitingThread waiter = (WaitingThread) waitingForWriteLock .get(0); removeWaitingWrite(waiter); DeadlockDetection.clearResourceWaiter(waiter.getThread()); writeLockedThread = waiter.getThread(); // if (LOG.isDebugEnabled()) { // LOG.debug("readLock released and before notifying a write lock waiting thread " + writeLockedThread); // LOG.debug("remaining read locks: " + outstandingReadLocks.size()); // } writeMonitor.signalAll(); // if (LOG.isDebugEnabled()) { // LOG.debug("readLock released and after notifying a write lock waiting thread " + writeLockedThread); // } } return; } else { LOG.warn( "Possible lock problem: thread " + Thread.currentThread().getName() + " released a read lock it didn't hold. Either the " + "thread was interrupted or it never acquired the lock. " + "Write lock: " + (writeLockedThread != null ? writeLockedThread .getName() : "null"), new Throwable()); if (LockOwner.DEBUG) debugReadLocks("ILLEGAL RELEASE"); } } finally { lock.unlock(); } } public boolean isLockedForWrite() { lock.lock(); try { return writeLockedThread != null || (waitingForWriteLock != null && waitingForWriteLock .size() > 0); } finally { lock.unlock(); } } public boolean hasLock() { lock.lock(); try { return !outstandingReadLocks.isEmpty() || isLockedForWrite(); } finally { lock.unlock(); } } public boolean isLockedForRead(Thread owner) { lock.lock(); try { for (int i = outstandingReadLocks.size() - 1; i > -1; i--) { if (((LockOwner) outstandingReadLocks.get(i)).getOwner() == owner) return true; } return false; } finally { lock.unlock(); } } private void removeReadLock(int count) { Object owner = Thread.currentThread(); for (int i = outstandingReadLocks.size() - 1; i > -1 && count > 0; i--) { LockOwner current = (LockOwner) outstandingReadLocks.get(i); if (current.getOwner() == owner) { outstandingReadLocks.remove(i); --count; } } } private void deadlockCheck() throws DeadlockException { // if (writeLockedThread != null) { // Lock lock = DeadlockDetection.isWaitingFor(writeLockedThread); // if (lock != null && lock.hasLock()) // throw new DeadlockException(); // } final int size = outstandingReadLocks.size(); LockOwner next; for (int i = 0; i < size; i++) { next = (LockOwner) outstandingReadLocks.get(i); Lock lock = DeadlockDetection.isWaitingFor(next.getOwner()); if (lock != null) { // LOG.debug("Checking for deadlock..."); lock.wakeUp(); } } } /** * Detect circular wait on different resources: thread A has a write lock on * resource R1; thread B has a write lock on resource R2; thread A tries to * acquire lock on R2; thread B now tries to acquire lock on R1. Solution: * suspend existing write lock of thread A and grant it to B. * * @return true if the write lock should be granted to the current thread */ private List checkForDeadlock(Thread waiter) { ArrayList waiters = new ArrayList(10); if (DeadlockDetection.wouldDeadlock(waiter, writeLockedThread, waiters)) { LOG.warn("Potential deadlock detected on lock " + getId() + "; killing threads: " + waiters.size()); // for (int i = 0; i < waiters.size(); i++) { // WaitingThread wt = (WaitingThread) waiters.get(i); // LOG.debug("Waiter: " + wt.getThread().getName() + " -> " + wt.getLock().getId()); // } return waiters.size() > 0 ? waiters : null; } return null; } /** * Check if a write lock can be granted, either because there are no * read locks, the read lock belongs to the current thread and can be * upgraded or the thread which holds the lock is blocked by another * lock held by the current thread. * * @return true if the write lock can be granted */ private boolean grantWriteLock() { Thread waiter = Thread.currentThread(); final int size = outstandingReadLocks.size(); if (size == 0) { return true; } LockOwner next; // walk through outstanding read locks for (int i = 0; i < size; i++) { next = (LockOwner) outstandingReadLocks.get(i); // if the read lock is owned by the current thread, all is ok and we continue if (next.getOwner() != waiter) { // otherwise, check if the lock belongs to a thread which is currently blocked // by a lock owned by the current thread. if yes, it will be safe to grant the // write lock: the other thread will be blocked anyway. if (!DeadlockDetection.isBlockedBy(waiter, next.getOwner())) { return false; } } } return true; } /** * Check if a write lock can be granted, either because there are no * read locks or the read lock belongs to the current thread and can be * upgraded. This method is called whenever a lock is released. * * @return true if the write lock can be granted */ private boolean grantWriteLockAfterRead() { // waiting write locks? if (waitingForWriteLock != null && waitingForWriteLock.size() > 0) { // yes, check read locks final int size = outstandingReadLocks.size(); if (size > 0) { // grant lock if all read locks are held by the write thread WaitingThread waiter = (WaitingThread) waitingForWriteLock.get(0); return isCompatible(waiter.getThread()); } else return true; } return false; } /** * Check if the specified thread has a read lock on the resource. * * @param owner the thread * @return true if owner has a read lock */ private boolean hasReadLock(Thread owner) { LockOwner next; for (int i = 0; i < outstandingReadLocks.size(); i++) { next = (LockOwner) outstandingReadLocks.get(i); if (next.getOwner() == owner) return true; } return false; } public Thread getWriteLockedThread() { return writeLockedThread; } /** * Check if the specified thread holds either a write or a read lock * on the resource. * * @param owner the thread * @return true if owner has a lock */ public boolean hasLock(Thread owner) { lock.lock(); try { if (writeLockedThread == owner) return true; return isLockedForRead(owner); } finally { lock.unlock(); } } public void wakeUp() { } /** * Check if the pending request for a write lock is compatible * with existing read locks and other write requests. A lock request is * compatible with another lock request if: (a) it belongs to the same thread, * (b) it belongs to a different thread, but this thread is also waiting for a write lock. * * @param waiting * @return true if the lock request is compatible with all other requests and the * lock can be granted. */ private boolean isCompatible(Thread waiting) { LockOwner next; for (int i = 0; i < outstandingReadLocks.size(); i++) { next = (LockOwner) outstandingReadLocks.get(i); // if the read lock is owned by the current thread, all is ok and we continue if (next.getOwner() != waiting) { // otherwise, check if the lock belongs to a thread which is currently blocked // by a lock owned by the current thread. if yes, it will be safe to grant the // write lock: the other thread will be blocked anyway. if (!DeadlockDetection.isBlockedBy(waiting, next.getOwner())) { return false; } } } return true; } public LockInfo getLockInfo() { lock.lock(); try { LockInfo info; String[] readers = new String[0]; if (outstandingReadLocks != null) { readers = new String[outstandingReadLocks.size()]; for (int i = 0; i < outstandingReadLocks.size(); i++) { LockOwner owner = (LockOwner) outstandingReadLocks.get(i); readers[i] = owner.getOwner().getName(); } } if (writeLockedThread != null) { info = new LockInfo(LockInfo.RESOURCE_LOCK, LockInfo.WRITE_LOCK, getId(), new String[] { writeLockedThread.getName() }); info.setReadLocks(readers); } else { info = new LockInfo(LockInfo.RESOURCE_LOCK, LockInfo.READ_LOCK, getId(), readers); } if (waitingForWriteLock != null) { String waitingForWrite[] = new String[waitingForWriteLock .size()]; for (int i = 0; i < waitingForWriteLock.size(); i++) { waitingForWrite[i] = ((WaitingThread) waitingForWriteLock .get(i)).getThread().getName(); } info.setWaitingForWrite(waitingForWrite); } return info; } finally { lock.unlock(); } } private void debugReadLocks(String msg) { for (int i = 0; i < outstandingReadLocks.size(); i++) { LockOwner owner = (LockOwner) outstandingReadLocks.get(i); LOG.debug(msg + ": " + owner.getOwner(), owner.getStack()); } } private String listReadLocks() { StringBuffer buf = new StringBuffer(); for (int i = 0; i < outstandingReadLocks.size(); i++) { LockOwner owner = (LockOwner) outstandingReadLocks.get(i); buf.append(' '); buf.append(((Thread) owner.getOwner()).getName()); } return buf.toString(); } }