/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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. */ package org.apache.jackrabbit.core.state; import static org.apache.jackrabbit.data.core.TransactionContext.getCurrentThreadId; import static org.apache.jackrabbit.data.core.TransactionContext.isSameThreadId; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import org.apache.jackrabbit.core.id.ItemId; import org.apache.jackrabbit.core.id.NodeId; import org.apache.jackrabbit.core.id.PropertyId; import EDU.oswego.cs.dl.util.concurrent.Latch; import EDU.oswego.cs.dl.util.concurrent.ReadWriteLock; import EDU.oswego.cs.dl.util.concurrent.Sync; import EDU.oswego.cs.dl.util.concurrent.WriterPreferenceReadWriteLock; /** * <code>FineGrainedISMLocking</code>... */ public class FineGrainedISMLocking implements ISMLocking { /** * Avoid creating commonly used Integer instances. */ private static final Integer ONE = new Integer(1); /** * An anonymous read lock without an id assigned. */ private final ReadLock anonymousReadLock = new ReadLockImpl(); /** * The active writer or <code>null</code> if there is none. */ private WriteLockImpl activeWriter; private volatile Object activeWriterId; private ReadWriteLock writerStateRWLock = new WriterPreferenceReadWriteLock(); /** * Map that contains the read locks. */ private final LockMap readLockMap = new LockMap(); /** * Number of current readers. */ private final AtomicInteger readerCount = new AtomicInteger(0); /** * List of waiting readers that are blocked because they conflict with * the current writer. */ private List<Sync> waitingReaders = Collections.synchronizedList(new LinkedList<Sync>()); /** * List of waiting writers that are blocked because there is already a * current writer or one of the current reads conflicts with the change log * of the blocked writer. */ private List<Sync> waitingWriters = new LinkedList<Sync>(); /** * {@inheritDoc} */ public ReadLock acquireReadLock(ItemId id) throws InterruptedException { if (isSameThreadId(activeWriterId, getCurrentThreadId())) { // we hold the write lock readerCount.incrementAndGet(); readLockMap.addLock(id); return new ReadLockImpl(id); } // if we get here the following is true: // - the current thread does not hold a write lock for (;;) { Sync signal; // make sure writer state does not change Sync shared = writerStateRWLock.readLock(); shared.acquire(); try { if (activeWriter == null || !hasDependency(activeWriter.changes, id)) { readerCount.incrementAndGet(); readLockMap.addLock(id); return new ReadLockImpl(id); } else { signal = new Latch(); waitingReaders.add(signal); } } finally { shared.release(); } // if we get here there was an active writer with // a dependency to the current id. // wait for the writer until it is done, then try again signal.acquire(); } } /** * {@inheritDoc} */ public WriteLock acquireWriteLock(ChangeLog changeLog) throws InterruptedException { for (;;) { Sync signal; // we want to become the current writer Sync exclusive = writerStateRWLock.writeLock(); exclusive.acquire(); Object currentId = getCurrentThreadId(); try { if (activeWriter == null && !readLockMap.hasDependency(changeLog)) { activeWriter = new WriteLockImpl(changeLog); activeWriterId = currentId; return activeWriter; } else { if (isSameThreadId(activeWriterId, currentId) && !readLockMap.hasDependency(changeLog)) { return activeWriter; } else { signal = new Latch(); waitingWriters.add(signal); } } } finally { exclusive.release(); } // if we get here there is an active writer or there is a read // lock that conflicts with the change log signal.acquire(); } } //----------------------------< internal >---------------------------------- private final class WriteLockImpl implements WriteLock { private final ChangeLog changes; WriteLockImpl(ChangeLog changes) { this.changes = changes; } public void release() { Sync exclusive = writerStateRWLock.writeLock(); for (;;) { try { exclusive.acquire(); break; } catch (InterruptedException e) { // try again Thread.interrupted(); } } try { activeWriter = null; activeWriterId = null; notifyWaitingReaders(); notifyWaitingWriters(); } finally { exclusive.release(); } } public ReadLock downgrade() { readerCount.incrementAndGet(); readLockMap.addLock(null); Sync exclusive = writerStateRWLock.writeLock(); for (;;) { try { exclusive.acquire(); break; } catch (InterruptedException e) { // try again Thread.interrupted(); } } try { activeWriter = null; // only notify waiting readers since we still hold a down // graded lock, which is kind of exclusiv with respect to // other writers notifyWaitingReaders(); } finally { exclusive.release(); } return anonymousReadLock; } } private final class ReadLockImpl implements ReadLock { private final ItemId id; public ReadLockImpl() { this(null); } ReadLockImpl(ItemId id) { this.id = id; } public void release() { Sync shared = writerStateRWLock.readLock(); for (;;) { try { shared.acquire(); break; } catch (InterruptedException e) { // try again Thread.interrupted(); } } try { readLockMap.removeLock(id); if (readerCount.decrementAndGet() == 0 && activeWriter == null) { activeWriterId = null; } if (!isSameThreadId(activeWriterId, getCurrentThreadId())) { // only notify waiting writers if we do *not* hold a write // lock at the same time. that would be a waste of cpu time. notifyWaitingWriters(); } } finally { shared.release(); } } } private static boolean hasDependency(ChangeLog changeLog, ItemId id) { try { if (changeLog.get(id) == null) { if (!id.denotesNode() || changeLog.getReferencesTo((NodeId) id) == null) { // change log does not contain the item return false; } } } catch (NoSuchItemStateException e) { // is deleted } return true; } /** * This method is not thread-safe and calling threads must ensure that * only one thread calls this method at a time. */ private void notifyWaitingReaders() { Iterator<Sync> it = waitingReaders.iterator(); while (it.hasNext()) { it.next().release(); it.remove(); } } /** * This method may be called concurrently by multiple threads. */ private void notifyWaitingWriters() { synchronized (waitingWriters) { if (waitingWriters.isEmpty()) { return; } Iterator<Sync> it = waitingWriters.iterator(); while (it.hasNext()) { it.next().release(); it.remove(); } } } private static final class LockMap { /** * 16 slots */ @SuppressWarnings("unchecked") private final Map<ItemId, Integer>[] slots = new Map[0x10]; /** * Flag that indicates if the entire map is locked. */ private volatile boolean global = false; public LockMap() { for (int i = 0; i < slots.length; i++) { slots[i] = new HashMap<ItemId, Integer>(); } } /** * This method must be called while holding the reader sync of the * {@link FineGrainedISMLocking#writerStateRWLock}! * * @param id the item id. */ public void addLock(ItemId id) { if (id == null) { if (global) { throw new IllegalStateException( "Map already globally locked"); } global = true; return; } Map<ItemId, Integer> locks = slots[slotIndex(id)]; synchronized (locks) { Integer i = (Integer) locks.get(id); if (i == null) { i = ONE; } else { i = new Integer(i.intValue() + 1); } locks.put(id, i); } } /** * This method must be called while holding the reader sync of the * {@link FineGrainedISMLocking#writerStateRWLock}! * * @param id the item id. */ public void removeLock(ItemId id) { if (id == null) { if (!global) { throw new IllegalStateException( "Map not globally locked"); } global = false; return; } Map<ItemId, Integer> locks = slots[slotIndex(id)]; synchronized (locks) { Integer i = (Integer) locks.get(id); if (i != null) { if (i.intValue() == 1) { locks.remove(id); } else { locks.put(id, new Integer(i.intValue() - 1)); } } else { throw new IllegalStateException( "No lock present for id: " + id); } } } /** * This method must be called while holding the write sync of {@link * FineGrainedISMLocking#writerStateRWLock} to make sure no additional * read locks are added to or removed from the map! * * @param changes the change log. * @return if the change log has a dependency to the locks currently * present in this map. */ public boolean hasDependency(ChangeLog changes) { if (global) { // read lock present, which was downgraded from a write lock return true; } for (int i = 0; i < slots.length; i++) { Map<ItemId, Integer> locks = slots[i]; synchronized (locks) { for (ItemId id : locks.keySet()) { if (FineGrainedISMLocking.hasDependency(changes, id)) { return true; } } } } return false; } private static int slotIndex(ItemId id) { NodeId nodeId; if (id.denotesNode()) { nodeId = (NodeId) id; } else { nodeId = ((PropertyId) id).getParentId(); } return ((int) nodeId.getLeastSignificantBits()) & 0xf; } } }