Skip to content

BiasedLock Implementation #24

@jtnelson

Description

@jtnelson
/*
 * Copyright (c) 2013-2025 Cinchapi Inc.
 *
 * 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.
 */
package com.cinchapi.concourse.server.concurrent;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReadWriteLock;

import com.cinchapi.common.reflect.Reflection;
import com.cinchapi.concourse.util.Logger;

/**
 * A {@link ReadWriteLock} that enforces a configurable priority for either
 * readers or writers under contention.
 * <p>
 * A {@link BiasedLock} is designed for workloads with predictable access
 * patterns where giving preference to either readers or writers can improve
 * overall throughput. For example, in read-heavy workloads, prioritizing
 * readers can increase concurrency, while in write-heavy workloads,
 * prioritizing writers can reduce lock contention and improve write throughput.
 * </p>
 * <p>
 * Unlike standard read-write locks, this implementation actively enforces the
 * configured bias by allowing the preferred lock type to "jump ahead" of
 * waiting threads of the non-preferred type. However, it also includes
 * starvation prevention mechanisms to ensure that non-preferred lock requests
 * will eventually be granted, even under continuous contention from preferred
 * lock types.
 * </p>
 * <p>
 * <strong>IMPORTANT:</strong> This lock is <em>not</em> reentrant. A thread
 * that holds a read lock cannot acquire another read lock, and a thread that
 * holds a write lock cannot acquire another write lock. Attempting to do so
 * will result in deadlock. Additionally, there is no automatic upgrading from
 * read to write locks - a thread must release its read lock before acquiring a
 * write lock.
 * </p>
 * <p>
 * This lock is particularly useful in scenarios where the application has
 * specific performance requirements that can benefit from biased lock
 * acquisition policies.
 * </p>
 * 
 * @author Jeff Nelson
 */
@SuppressWarnings("restriction")
public class BiasedLock implements ReadWriteLock {

    /**
     * Creates a new {@link BiasedLock} instance with the default bias favoring
     * read operations.
     * <p>
     * This is equivalent to calling {@link #favoringReads()} and is suitable
     * for read-heavy workloads where maximizing concurrent read access is more
     * important than write latency.
     * </p>
     * 
     * @return a new {@link BiasedLock} instance that favors read operations
     */
    public static BiasedLock create() {
        return favoringReads();
    }

    /**
     * Creates a new {@link BiasedLock} instance that favors read operations
     * over write operations.
     * <p>
     * When there is contention for the lock, read operations will be given
     * priority. This configuration is optimal for read-heavy workloads where
     * maximizing concurrent read access is more important than write latency.
     * </p>
     * 
     * @return a new {@link BiasedLock} instance that favors read operations
     */
    public static BiasedLock favoringReads() {
        return new BiasedLock(Bias.READS);
    }

    /**
     * Creates a new {@link BiasedLock} instance that favors write operations
     * over read operations.
     * <p>
     * When there is contention for the lock, write operations will be given
     * priority. This configuration is optimal for write-heavy workloads or
     * scenarios where minimizing write latency is more critical than maximizing
     * read concurrency.
     * </p>
     * 
     * @return a new {@link BiasedLock} instance that favors write operations
     */
    public static BiasedLock favoringWrites() {
        return new BiasedLock(Bias.WRITES);
    }

    /**
     * Returns the number of exclusive holds represented in count
     * 
     * @param c the current state value
     * @return the number of exclusive (writer) locks currently held (0 or 1)
     */
    static int exclusiveCount(int c) {
        return c & EXCLUSIVE_MASK;
    }

    /**
     * Returns the number of shared holds represented in count
     * 
     * @param c the current state value
     * @return the number of shared (reader) locks currently held
     */
    static int sharedCount(int c) {
        return c >>> SHARED_SHIFT;
    }

    /**
     * Bit shift for the shared (reader) count in the state value. This
     * determines how many bits are allocated for reader vs writer counts in the
     * state integer.
     */
    private static final int SHARED_SHIFT = 16;

    /**
     * Unit increment for each reader in the state value. Each reader
     * acquisition adds this value to the state.
     */
    private static final int SHARED_UNIT = (1 << SHARED_SHIFT);

    /**
     * Maximum number of concurrent readers supported by this lock
     * implementation. This is determined by the number of bits allocated for
     * reader count.
     */
    private static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;

    /**
     * Mask to extract the exclusive (writer) count from the state value.
     * Used to determine if a writer holds the lock.
     */
    private static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

    /**
     * Maximum backoff time in nanoseconds before a deferred actor
     * is queued normally.
     */
    private static final long MAX_BACKOFF_NANOS = 1_000_000;

    /**
     * Initial backoff time in nanoseconds for deferred actors.
     */
    private static final long INITIAL_BACKOFF_NANOS = 100_000;

    /**
     * Instance of {@link sun.misc.Unsafe} to use for low-level operations.
     * This provides direct memory access for improved performance in critical
     * sections.
     */
    private static final sun.misc.Unsafe unsafe = Reflection
            .getStatic("theUnsafe", sun.misc.Unsafe.class);

    /**
     * Memory offset for the head field in {@link AbstractQueuedSynchronizer}.
     * Used for direct field access via Unsafe.
     */
    private static final long HEAD_OFFSET;

    /**
     * Memory offset for the next field in
     * {@link AbstractQueuedSynchronizer.Node}.
     * Used for direct field access via Unsafe.
     */
    private static final long NEXT_OFFSET;

    /**
     * Memory offset for the nextWaiter field in
     * {@link AbstractQueuedSynchronizer.Node}.
     * Used for direct field access via Unsafe.
     */
    private static final long NEXT_WAIT_OFFSET;

    /**
     * Reference to the SHARED static field in
     * {@link AbstractQueuedSynchronizer.Node}.
     * Used to identify nodes that represent shared (read) lock requests.
     */
    private static final Object SHARED;

    /**
     * Memory offset for the thread field in
     * {@link AbstractQueuedSynchronizer.Node}.
     * Used for direct field access via Unsafe.
     */
    private static final long THREAD_OFFSET;

    static {
        HEAD_OFFSET = unsafe.objectFieldOffset(Reflection
                .getDeclaredField("head", AbstractQueuedSynchronizer.class));
        Class<?> NODE_CLASS = Reflection.getClassCasted(
                "java.util.concurrent.locks.AbstractQueuedSynchronizer$Node");
        NEXT_OFFSET = unsafe.objectFieldOffset(
                Reflection.getDeclaredField("next", NODE_CLASS));
        NEXT_WAIT_OFFSET = unsafe.objectFieldOffset(
                Reflection.getDeclaredField("nextWaiter", NODE_CLASS));
        SHARED = Reflection.getStatic("SHARED", NODE_CLASS);
        THREAD_OFFSET = unsafe.objectFieldOffset(
                Reflection.getDeclaredField("thread", NODE_CLASS));
    }

    /**
     * The bias preference for this lock instance, determining whether readers
     * or writers are given priority under contention.
     */
    private final Bias bias;

    /**
     * The synchronizer that manages lock state and implements the
     * core locking mechanisms.
     */
    private final Sync sync;

    /**
     * The read lock implementation returned by {@link #readLock()}.
     */
    private final Lock readLock = new ReadLock();

    /**
     * The write lock implementation returned by {@link #writeLock()}.
     */
    private final Lock writeLock = new WriteLock();

    /**
     * Construct a new instance.
     * 
     * @param bias the preference for this lock, determining whether readers or
     *            writers
     *            are given priority under contention
     */
    public BiasedLock(Bias bias) {
        this.bias = bias;
        this.sync = new Sync();
    }

    @Override
    public Lock readLock() {
        return readLock;
    }

    @Override
    public Lock writeLock() {
        return writeLock;
    }

    /**
     * Defines the bias preference for the lock, determining whether read or
     * write operations should be given priority when there is contention.
     */
    public enum Bias {
        /**
         * Indicates that read operations should be given priority over write
         * operations when there is contention for the lock. This is useful for
         * read-heavy workloads where maximizing concurrent read access is more
         * important than write latency.
         */
        READS,

        /**
         * Indicates that write operations should be given priority over read
         * operations when there is contention for the lock. This is useful for
         * write-heavy workloads or when write latency is more critical than
         * read concurrency.
         */
        WRITES
    }

    /**
     * Read lock implementation with adaptive back-off for non-preferred actors
     */
    private class ReadLock implements Lock {
        @Override
        public void lock() {
            if(bias == Bias.READS) {
                sync.acquireShared(1);
            }
            else {
                long backoff = INITIAL_BACKOFF_NANOS;
                while (backoff < MAX_BACKOFF_NANOS) {
                    LockSupport.parkNanos(backoff);
                    if(sync.tryAcquireShared(1) >= 0) {
                        // If we can acquire immediately, it means there is no
                        // contention and we can proceed, even if we are not the
                        // preferred actor
                        return;
                    }
                    backoff = Math.min(backoff * 2, MAX_BACKOFF_NANOS);
                }

                // If we've reached max backoff, queue normally
                sync.acquireShared(1);
            }
        }

        @Override
        public void lockInterruptibly() throws InterruptedException {
            if(bias == Bias.READS) {
                sync.acquireSharedInterruptibly(1);
            }
            else {
                Thread.yield();
                long backoff = INITIAL_BACKOFF_NANOS;
                while (backoff < MAX_BACKOFF_NANOS) {
                    if(Thread.currentThread().isInterrupted()) {
                        throw new InterruptedException();
                    }
                    else if(sync.tryAcquireShared(1) >= 0) {
                        // If we can acquire immediately, it means there is no
                        // contention and we can proceed, even if we are not the
                        // preferred actor
                        return;
                    }
                    else {
                        // If there is condition, backoff exponentially until we
                        // reach a defined threshold so that we aren't starved
                        // forever
                        LockSupport.parkNanos(backoff);
                        backoff = Math.min(backoff * 2, MAX_BACKOFF_NANOS);
                    }
                }

                // If we've reached max backoff, queue normally
                sync.acquireSharedInterruptibly(1);
            }
        }

        @Override
        public Condition newCondition() {
            throw new UnsupportedOperationException(
                    "Conditions not supported on read lock");
        }

        @Override
        public boolean tryLock() {
            if(bias != Bias.READS) {
                Thread.yield();
            }
            return sync.tryAcquireShared(1) >= 0;
        }

        @Override
        public boolean tryLock(long time, TimeUnit unit)
                throws InterruptedException {
            long timeout = unit.toNanos(time);
            if(bias == Bias.READS) {
                return sync.tryAcquireSharedNanos(1, timeout);
            }
            else {
                long deadline = System.nanoTime() + timeout;
                Thread.yield();
                long remaining = deadline - System.nanoTime();
                long backoff = INITIAL_BACKOFF_NANOS;
                while (remaining > 0 && backoff < MAX_BACKOFF_NANOS) {
                    if(Thread.currentThread().isInterrupted()) {
                        throw new InterruptedException();
                    }
                    else if(sync.tryAcquireShared(1) >= 0) {
                        // If we can acquire immediately, it means there is no
                        // contention and we can proceed, even if we are not the
                        // preferred actor
                        return true;
                    }
                    else {
                        // If there is condition, backoff exponentially (up to
                        // starvation threshhold) while staying within the
                        // deadline.
                        long parkTime = Math.min(backoff, remaining);
                        if(parkTime > 0) {
                            LockSupport.parkNanos(parkTime);
                            backoff = Math.min(backoff * 2, MAX_BACKOFF_NANOS);
                        }
                        remaining = deadline - System.nanoTime();
                    }
                }
                if(remaining > 0 && sync.tryAcquireSharedNanos(1, remaining)) {
                    // The deadline is longer than our starvation threshold, but
                    // we were able to acquire within the remaining time
                    return true;
                }
                else {
                    return false;
                }
            }
        }

        @Override
        public void unlock() {
            sync.releaseShared(1);
        }
    }

    /**
     * Core synchronizer using AQS state: high bits for readers, low bits for
     * writer.
     */
    private class Sync extends AbstractQueuedSynchronizer {

        private static final long serialVersionUID = 1L;

        /**
         * Threshold for the number of consecutive reader acquisitions before
         * forcing readers to queue when writers are waiting. This prevents
         * writer starvation in read-biased locks under high read contention.
         */
        private static final int WRITER_STARVATION_THRESHOLD = 5;

        /**
         * Counter for tracking consecutive reader acquisitions to prevent
         * writer starvation.
         */
        private volatile int consecutiveReaderCount = 0;

        @Override
        protected boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

        @Override
        protected boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();

            if(c != 0) {
                // These locks are non-reentrant, so if any hold exists
                // acquisition cannot occur
                return false;
            }
            else if(bias == Bias.READS && hasQueuedPredecessors()) {
                // When there is read bias, an exclusive acquisition attempt
                // cannot happen if others are in the queue ahead of this
                // thread. If the predecessors are writers, this just enforces
                // FIFO so writes can't barge ahead of others.
                return false;
            }
            else if(firstQueuedIsExclusive()) {
                // If the next queued thread is a writer, let them go forward
                // and allow this thread to be placed at the end of the queue.
                return false;
            }
            else if(compareAndSetState(c, acquires)) {
                // Reset consecutive reader count when a writer acquires the
                // lock
                consecutiveReaderCount = 0;
                setExclusiveOwnerThread(current);
                return true;
            }
            else {
                return false;
            }
        }

        @Override
        protected int tryAcquireShared(int $) {
            for (;;) {
                int c = getState();
                if(exclusiveCount(c) != 0) { // A writer is holding
                    return -1;
                }
                else if(bias == Bias.WRITES
                        && hasQueuedExclusivePredecessors()) {
                    // When there is a write bias, do not allow this read to
                    // acquire while writes are ahead of this thread in the
                    // queue
                    return -1;
                }
                else if(readerShouldQueue()) {
                    // Force readers to queue when writers have been waiting too
                    // long
                    return -1;
                }
                else {
                    int r = sharedCount(c);
                    if(r == MAX_COUNT) {
                        throw new Error("Maximum lock count exceeded");
                    }
                    int next = c + SHARED_UNIT;
                    if(compareAndSetState(c, next)) {
                        consecutiveReaderCount++;
                        return 1;
                    }
                }
            }
        }

        @Override
        protected boolean tryRelease(int $) {
            if(getExclusiveOwnerThread() != Thread.currentThread()) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0); // Non-reentrant, so always release fully
            return true;
        }

        @Override
        protected boolean tryReleaseShared(int $) {
            for (;;) {
                int c = getState();
                int r = sharedCount(c);
                if(r == 0) {
                    throw new IllegalMonitorStateException(
                            "Read lock not held");
                }
                int next = c - SHARED_UNIT;
                if(compareAndSetState(c, next)) {
                    return (next >>> SHARED_SHIFT) == 0;
                }
            }
        }

        Condition newCondition() {
            return new ConditionObject();
        }

        /**
         * Determines if the apparent first queued thread is waiting for
         * exclusive (write) access.
         * <p>
         * This method examines the wait queue to determine if the next thread
         * in line is attempting to acquire the lock in exclusive mode. This
         * information is used to implement the biasing policy, allowing write
         * operations to proceed when appropriate even if they are not at the
         * head of the queue.
         * </p>
         * <p>
         * The method uses {@link sun.misc.Unsafe} to directly access the
         * internal queue structure of {@link AbstractQueuedSynchronizer} for
         * improved performance.
         * </p>
         * 
         * @return {@code true} if the next queued thread is waiting for
         *         exclusive access, {@code false} otherwise
         */
        private boolean firstQueuedIsExclusive() {
            try {
                // Get the head node
                Object headNode = unsafe.getObject(this, HEAD_OFFSET);
                if(headNode == null) {
                    // queue is empty
                    return false;
                }
                // Get head.next
                Object first = unsafe.getObject(headNode, NEXT_OFFSET);
                if(first == null) {
                    // no real nodes enqueued yet
                    return false;
                }

                // Determine who "owns" that node?
                Thread owner = (Thread) unsafe.getObject(first, THREAD_OFFSET);
                if(owner == Thread.currentThread()) {
                    // if it's *you* at the head, let yourself through
                    return false;
                }

                // Inspect its nextWaiter field
                Object waitMode = unsafe.getObject(first, NEXT_WAIT_OFFSET);
                // SHARED marker means this is a reader; anything else is an
                // exclusive waiter
                return waitMode != SHARED;
            }
            catch (Exception e1) {
                Logger.error("", e1);
                return false;
            }
        }

        /**
         * Checks if there are any exclusive (write) mode waiters ahead of the
         * current thread in the queue.
         * 
         * <p>
         * IMPORTANT: This method uses {@link sun.misc.Unsafe} to directly
         * access the internal queue structure of
         * {@link AbstractQueuedSynchronizer}. This approach is
         * brittle and may break with Java upgrades or JVM changes, as it
         * depends on the internal implementation details of AQS.
         * </p>
         * 
         * @return true if there are exclusive mode waiters ahead in the queue
         */
        private boolean hasQueuedExclusivePredecessors() {
            try {
                // Get the head node of the wait queue
                // This is a direct field access to avoid method call overhead
                Object h = unsafe.getObject(this, HEAD_OFFSET);
                if(h == null)
                    return false;

                // Traverse the queue starting from head.next
                // We're looking for any exclusive mode nodes (writers)
                Object p = unsafe.getObject(h, NEXT_OFFSET);
                while (p != null) {
                    // Check if this node is in exclusive mode by examining its
                    // nextWaiter field
                    // In AQS implementation, shared mode nodes have nextWaiter
                    // == SHARED
                    Object mode = unsafe.getObject(p, NEXT_WAIT_OFFSET);
                    if(mode != SHARED) {
                        return true; // Found a writer-mode node
                    }
                    // Move to the next node in the queue
                    p = unsafe.getObject(p, NEXT_OFFSET);
                }
                return false; // No exclusive mode waiters found
            }
            catch (Exception e) {
                // If our unsafe approach fails, fall back to the standard
                // method This is less efficient but safer
                Logger.error("", e);
                return getExclusiveQueuedThreads().size() > 0;
            }
        }

        /**
         * Determines if a reader should queue instead of immediately acquiring
         * the lock.
         * This method helps prevent writer starvation by forcing readers to
         * queue
         * when there have been too many consecutive reader acquisitions and
         * there are
         * writers waiting.
         * 
         * @return {@code true} if the reader should queue, {@code false} if it
         *         can
         *         proceed with acquisition
         */
        private boolean readerShouldQueue() {
            // Only force readers to queue if:
            // 1. There have been too many consecutive reader acquisitions
            // 2. There is at least one writer waiting in the queue
            return consecutiveReaderCount >= WRITER_STARVATION_THRESHOLD
                    && hasQueuedExclusivePredecessors();
        }
    }

    /**
     * Write lock with adaptive back-off for non-preferred actors
     */
    private class WriteLock implements Lock {
        @Override
        public void lock() {
            if(bias == Bias.WRITES) {
                sync.acquire(1);
            }
            else {
                long backoff = INITIAL_BACKOFF_NANOS;
                while (backoff < MAX_BACKOFF_NANOS) {
                    LockSupport.parkNanos(backoff); // TODO: interrupt?
                    if(sync.tryAcquire(1)) {
                        // If we can acquire immediately, it means there is no
                        // contention and we can proceed, even if we are not the
                        // preferred actor
                        return;
                    }
                    backoff = Math.min(backoff * 2, MAX_BACKOFF_NANOS);
                }

                // If we've reached max backoff, queue normally
                sync.acquire(1);
            }
        }

        @Override
        public void lockInterruptibly() throws InterruptedException {
            if(bias == Bias.WRITES) {
                sync.acquireInterruptibly(1);
            }
            else {
                Thread.yield();
                long backoff = INITIAL_BACKOFF_NANOS;
                while (backoff < MAX_BACKOFF_NANOS) {
                    if(Thread.currentThread().isInterrupted()) {
                        throw new InterruptedException();
                    }
                    else if(sync.tryAcquire(1)) {
                        // If we can acquire immediately, it means there is no
                        // contention and we can proceed, even if we are not the
                        // preferred actor
                        return;
                    }
                    else {
                        // If there is condition, backoff exponentially until we
                        // reach a defined threshold so that we aren't starved
                        // forever
                        LockSupport.parkNanos(backoff);
                        backoff = Math.min(backoff * 2, MAX_BACKOFF_NANOS);
                    }
                }

                // If we've reached max backoff, queue normally
                sync.acquireInterruptibly(1);
            }
        }

        @Override
        public Condition newCondition() {
            return sync.newCondition();
        }

        @Override
        public boolean tryLock() {
            if(bias != Bias.WRITES) {
                Thread.yield();
            }
            return sync.tryAcquire(1);
        }

        @Override
        public boolean tryLock(long time, TimeUnit unit)
                throws InterruptedException {
            long timeout = unit.toNanos(time);
            if(bias == Bias.WRITES) {
                return sync.tryAcquireNanos(1, timeout);
            }
            else {
                long deadline = System.nanoTime() + timeout;
                Thread.yield();
                long remaining = deadline - System.nanoTime();
                long backoff = INITIAL_BACKOFF_NANOS;
                while (remaining > 0 && backoff < MAX_BACKOFF_NANOS) {
                    if(Thread.currentThread().isInterrupted()) {
                        throw new InterruptedException();
                    }
                    else if(sync.tryAcquire(1)) {
                        // If we can acquire immediately, it means there is no
                        // contention and we can proceed, even if we are not the
                        // preferred actor
                        return true;
                    }
                    else {
                        // If there is condition, backoff exponentially (up to
                        // starvation threshhold) while staying within the
                        // deadline.
                        long parkTime = Math.min(backoff, remaining);
                        if(parkTime > 0) {
                            LockSupport.parkNanos(parkTime);
                            backoff = Math.min(backoff * 2, MAX_BACKOFF_NANOS);
                        }
                        remaining = deadline - System.nanoTime();
                    }
                }
                if(remaining > 0 && sync.tryAcquireNanos(1, remaining)) {
                    // The deadline is longer than our starvation threshold, but
                    // we were able to acquire within the remaining time
                    return true;
                }
                else {
                    return false;
                }
            }
        }

        @Override
        public void unlock() {
            sync.release(1);
        }
    }
}
/*
 * Copyright (c) 2013-2025 Cinchapi Inc.
 *
 * 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.
 */
package com.cinchapi.concourse.server.concurrent;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

/**
 *
 *
 * @author jeff
 */
public abstract class AbstractPreferentialLockTest {

    /**
     * Factory method to create a lock that gives preference to readers.
     */
    protected abstract ReadWriteLock createReadPriorityLock();

    /**
     * Factory method to create a lock that gives preference to writers.
     */
    protected abstract ReadWriteLock createWritePriorityLock();

    /**
     * Executor for running concurrent tasks.
     */
    private ExecutorService executor;

    @Before
    public void setup() {
        executor = Executors.newCachedThreadPool();
    }

    /**
     * Verify that readers jump ahead of a queued writer up to the threshold.
     */
    @Test
    public void testReadersJumpAheadUntilThreshold() throws Exception {
        ReadWriteLock lock = createReadPriorityLock();
        int threshold = 5;
        // Pre-acquire write lock to force queuing of subsequent readers/writer
        lock.writeLock().lock();
        CountDownLatch start = new CountDownLatch(1);
        ConcurrentLinkedQueue<String> order = new ConcurrentLinkedQueue<>();
        // Schedule threshold+1 readers
        for (int i = 0; i < threshold + 1; i++) {
            final int idx = i;
            executor.submit(() -> {
                try {
                    start.await();
                    lock.readLock().lock();
                    order.add("R" + idx);
                    lock.readLock().unlock();
                }
                catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }
        // And one writer
        executor.submit(() -> {
            try {
                start.await();
                lock.writeLock().lock();
                order.add("W");
                lock.writeLock().unlock();
            }
            catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        // Release initial write lock and start
        lock.writeLock().unlock();
        start.countDown();
        executor.shutdown();
        executor.awaitTermination(5, TimeUnit.SECONDS);
        // First 'threshold' entries must be readers
        for (int i = 0; i < threshold; i++) {
            Assert.assertEquals("R" + i, order.poll());
        }
        // Then writer, then last reader
        Assert.assertEquals("W", order.poll());
        Assert.assertEquals("R" + threshold, order.poll());
    }

    @Test
    public void testDeterministicBias() throws Exception {
        // Create a lock with write bias
        ReadWriteLock lock = createWritePriorityLock();

        // 1) Pre-acquire write lock so nobody can get in immediately
        lock.writeLock().lock();

        // Create a latch to control when threads start competing
        CountDownLatch startLatch = new CountDownLatch(1);

        // Track acquisition order
        ConcurrentLinkedQueue<String> acquisitionOrder = new ConcurrentLinkedQueue<>();

        // Create a reader thread that will compete for the lock
        Thread reader = new Thread(() -> {
            try {
                startLatch.await();
                lock.readLock().lock();
                acquisitionOrder.add("R");
                lock.readLock().unlock();
            }
            catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // Create a writer thread that will compete for the lock
        Thread writer = new Thread(() -> {
            try {
                startLatch.await();
                lock.writeLock().lock();
                acquisitionOrder.add("W");
                lock.writeLock().unlock();
            }
            catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // Start the threads
        reader.start();
        writer.start();

        // Release the pre-acquired lock to allow competition
        Thread.sleep(10); // Small delay to ensure threads are waiting

        // 3) Now let them contend by releasing the pre‑acquired write lock
        lock.writeLock().unlock();

        // Let the threads compete for the lock
        startLatch.countDown();

        // Wait for threads to complete
        reader.join(1000);
        writer.join(1000);

        // With write bias, the writer should acquire first
        Assert.assertEquals("W", acquisitionOrder.poll());
        Assert.assertEquals("R", acquisitionOrder.poll());
    }

    /**
     * Ensure no deferral when lock is free.
     */
    @Test
    public void testNoDeferralWhenFree() {
        ReadWriteLock rp = createReadPriorityLock();
        long t0 = System.nanoTime();
        rp.writeLock().lock();
        long dt = System.nanoTime() - t0;
        rp.writeLock().unlock();
        Assert.assertTrue(dt < TimeUnit.MILLISECONDS.toNanos(1));

        ReadWriteLock wp = createWritePriorityLock();
        t0 = System.nanoTime();
        wp.readLock().lock();
        dt = System.nanoTime() - t0;
        wp.readLock().unlock();
        Assert.assertTrue(dt < TimeUnit.MILLISECONDS.toNanos(1));
    }

    /**
     * Shared unlock without a prior lock should throw.
     */
    @Test(expected = IllegalMonitorStateException.class)
    public void testSharedUnlockWithoutLockThrows() {
        createReadPriorityLock().readLock().unlock();
    }

    /**
     * Exclusive unlock without a prior lock should throw.
     */
    @Test(expected = IllegalMonitorStateException.class)
    public void testExclusiveUnlockWithoutLockThrows() {
        createWritePriorityLock().writeLock().unlock();
    }

    /**
     * Multiple readers should be allowed concurrently.
     */
    @Test
    public void testConcurrentReaders() throws Exception {
        ReadWriteLock lock = createReadPriorityLock();
        int n = 10;
        CountDownLatch start = new CountDownLatch(1);
        AtomicInteger concurrent = new AtomicInteger();
        AtomicInteger max = new AtomicInteger();
        List<Future<?>> futures = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            futures.add(executor.submit(() -> {
                try {
                    start.await();
                    lock.readLock().lock();
                    int c = concurrent.incrementAndGet();
                    max.updateAndGet(m -> Math.max(m, c));
                    Thread.sleep(50);
                    concurrent.decrementAndGet();
                    lock.readLock().unlock();
                }
                catch (Exception e) {
                    Thread.currentThread().interrupt();
                }
            }));
        }
        start.countDown();
        for (Future<?> f : futures)
            f.get();
        Assert.assertTrue(max.get() > 1);
    }

    /**
     * Writers must be mutually exclusive.
     */
    @Test
    public void testExclusiveWriters() throws Exception {
        ReadWriteLock lock = createWritePriorityLock();
        int n = 5;
        CountDownLatch start = new CountDownLatch(1);
        AtomicInteger concurrent = new AtomicInteger();
        AtomicInteger max = new AtomicInteger();
        List<Future<?>> futures = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            futures.add(executor.submit(() -> {
                try {
                    start.await();
                    lock.writeLock().lock();
                    int c = concurrent.incrementAndGet();
                    max.updateAndGet(m -> Math.max(m, c));
                    Thread.sleep(50);
                    concurrent.decrementAndGet();
                    lock.writeLock().unlock();
                }
                catch (Exception e) {
                    Thread.currentThread().interrupt();
                }
            }));
        }
        start.countDown();
        for (Future<?> f : futures)
            f.get();
        Assert.assertEquals(1, max.get());
    }

    /**
     * Readers cannot be starved indefinitely in write-priority locks.
     */
    @Test
    public void testNoStarvation() throws Exception {
        ReadWriteLock lock = createWritePriorityLock();
        int writers = 3;
        int readers = 1;
        CountDownLatch done = new CountDownLatch(writers + readers);
        AtomicReference<Boolean> readerAcquired = new AtomicReference<>(false);
        // flood writers
        for (int i = 0; i < writers; i++) {
            executor.submit(() -> {
                for (int j = 0; j < 5; j++) {
                    lock.writeLock().lock();
                    lock.writeLock().unlock();
                }
                done.countDown();
            });
        }
        // one reader
        executor.submit(() -> {
            lock.readLock().lock();
            readerAcquired.set(true);
            lock.readLock().unlock();
            done.countDown();
        });
        done.await();
        Assert.assertTrue(readerAcquired.get());
    }

    /**
     * Subclasses must return a fresh instance of their ReadWriteLock
     * implementation.
     */
    protected abstract ReadWriteLock getLock();

    /**
     * Multiple readers should be allowed concurrently.
     */
    @Test
    public void testMultipleReaders() throws InterruptedException {
        final ReadWriteLock lock = getLock();
        final int threadCount = 20;
        final CountDownLatch latch = new CountDownLatch(threadCount);
        final AtomicInteger concurrentReaders = new AtomicInteger(0);
        final AtomicInteger maxConcurrentReaders = new AtomicInteger(0);

        Runnable reader = new Runnable() {
            @Override
            public void run() {
                lock.readLock().lock();
                try {
                    // Track the number of threads holding the read lock
                    // simultaneously
                    int current = concurrentReaders.incrementAndGet();
                    maxConcurrentReaders
                            .updateAndGet(max -> Math.max(max, current));

                    latch.countDown();
                    Thread.sleep(100);
                }
                catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                finally {
                    concurrentReaders.decrementAndGet();
                    lock.readLock().unlock();
                }
            }
        };

        // Create and start 20 reader threads
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < threadCount; i++) {
            Thread t = new Thread(reader);
            threads.add(t);
            t.start();
        }

        // Wait for all threads to acquire the read lock
        boolean allAcquired = latch.await(1000, TimeUnit.MILLISECONDS);
        Assert.assertTrue("All read locks should be acquired concurrently",
                allAcquired);
        Assert.assertEquals("All threads should hold read lock simultaneously",
                threadCount, maxConcurrentReaders.get());
        System.out.println(maxConcurrentReaders.get());

        // Wait for all threads to complete
        for (Thread t : threads) {
            t.join(500);
        }
    }

    /**
     * Only one writer at a time; writers must exclude one another.
     */
    @Test
    public void testWriteLockIsExclusive() throws InterruptedException {
        final ReadWriteLock lock = getLock();
        final CountDownLatch firstWriter = new CountDownLatch(1);
        final CountDownLatch releaseFirst = new CountDownLatch(1);

        Thread writer1 = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.writeLock().lock();
                try {
                    firstWriter.countDown();
                    releaseFirst.await();
                }
                catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                finally {
                    lock.writeLock().unlock();
                }
            }
        });
        writer1.start();
        Assert.assertTrue("First writer should acquire the write lock",
                firstWriter.await(500, TimeUnit.MILLISECONDS));

        final boolean[] secondGotIt = { false };
        Thread writer2 = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.writeLock().lock();
                try {
                    secondGotIt[0] = true;
                }
                finally {
                    lock.writeLock().unlock();
                }
            }
        });
        writer2.start();

        Thread.sleep(100);
        Assert.assertFalse("Second writer must wait", secondGotIt[0]);

        releaseFirst.countDown();
        writer1.join();
        writer2.join(500);
        Assert.assertTrue("Second writer should acquire after release",
                secondGotIt[0]);
    }

    /**
     * A held write lock must block new readers.
     */
    @Test
    public void testWriteLockBlocksReaders() throws InterruptedException {
        final ReadWriteLock lock = getLock();
        final CountDownLatch writerStarted = new CountDownLatch(1);
        final CountDownLatch releaseWriter = new CountDownLatch(1);
        final boolean[] readerGotIt = { false };

        Thread writer = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.writeLock().lock();
                try {
                    writerStarted.countDown();
                    releaseWriter.await();
                }
                catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                finally {
                    lock.writeLock().unlock();
                }
            }
        });
        writer.start();
        Assert.assertTrue("Writer must hold lock",
                writerStarted.await(500, TimeUnit.MILLISECONDS));

        Thread reader = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.readLock().lock();
                try {
                    readerGotIt[0] = true;
                }
                finally {
                    lock.readLock().unlock();
                }
            }
        });
        reader.start();

        Thread.sleep(100);
        Assert.assertFalse("Reader must be blocked by writer", readerGotIt[0]);

        releaseWriter.countDown();
        writer.join();
        reader.join(500);
        Assert.assertTrue("Reader should proceed after writer releases",
                readerGotIt[0]);
    }

    /**
     * A held read lock must block new writers.
     */
    @Test
    public void testWriteLockBlockedByReadLock() throws InterruptedException {
        final ReadWriteLock lock = getLock();
        final CountDownLatch readerStarted = new CountDownLatch(1);
        final CountDownLatch releaseReader = new CountDownLatch(1);
        final boolean[] writerGotIt = { false };

        Thread reader = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.readLock().lock();
                try {
                    readerStarted.countDown();
                    releaseReader.await();
                }
                catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                finally {
                    lock.readLock().unlock();
                }
            }
        });
        reader.start();
        Assert.assertTrue("Reader must hold lock",
                readerStarted.await(500, TimeUnit.MILLISECONDS));

        Thread writer = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.writeLock().lock();
                try {
                    writerGotIt[0] = true;
                }
                finally {
                    lock.writeLock().unlock();
                }
            }
        });
        writer.start();

        Thread.sleep(100);
        Assert.assertFalse("Writer must be blocked by reader", writerGotIt[0]);

        releaseReader.countDown();
        reader.join();
        writer.join(500);
        Assert.assertTrue("Writer should proceed after reader releases",
                writerGotIt[0]);
    }

    /**
     * tryLock() on write lock should succeed when free.
     */
    @Test
    public void testTryWriteLockWithoutReadLock() {
        final ReadWriteLock lock = getLock();
        Lock w = lock.writeLock();
        boolean got = w.tryLock();
        try {
            Assert.assertTrue("tryLock should succeed when free", got);
        }
        finally {
            if(got) {
                w.unlock();
            }
        }
    }

    /**
     * tryLock() on write lock should fail if this thread already holds a read
     * lock.
     */
    @Test
    public void testTryWriteLockWithReadLock() {
        final ReadWriteLock lock = getLock();
        lock.readLock().lock();
        try {
            boolean got = lock.writeLock().tryLock();
            if(got) {
                lock.writeLock().unlock();
            }
            Assert.assertFalse("tryLock write should fail under held read lock",
                    got);
        }
        finally {
            lock.readLock().unlock();
        }
    }

    /**
     * tryLock() on read lock should succeed when free.
     */
    @Test
    public void testReadLockTryLock() {
        final ReadWriteLock lock = getLock();
        Lock r = lock.readLock();
        boolean got = r.tryLock();
        try {
            Assert.assertTrue("tryLock read should succeed when free", got);
        }
        finally {
            if(got) {
                r.unlock();
            }
        }
    }

    /**
     * tryLock() on read lock should fail if a writer holds the lock.
     */
    @Test
    public void testTryReadLockWithWriteLock() {
        final ReadWriteLock lock = getLock();
        lock.writeLock().lock();
        try {
            boolean got = lock.readLock().tryLock();
            if(got) {
                lock.readLock().unlock();
            }
            Assert.assertFalse(
                    "tryLock read should fail while writer holds lock", got);
        }
        finally {
            lock.writeLock().unlock();
        }
    }

}
/*
 * Copyright (c) 2013-2025 Cinchapi Inc.
 *
 * 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.
 */
package com.cinchapi.concourse.server.concurrent;

import java.util.concurrent.locks.ReadWriteLock;

/**
 *
 *
 * @author jeff
 */
public class BiasedLockTest extends AbstractPreferentialLockTest {

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.cinchapi.concourse.server.concurrent.AbstractPreferentialLockTest#
     * createReadPriorityLock()
     */
    @Override
    protected ReadWriteLock createReadPriorityLock() {
        return BiasedLock.favoringReads();
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.cinchapi.concourse.server.concurrent.AbstractPreferentialLockTest#
     * createWritePriorityLock()
     */
    @Override
    protected ReadWriteLock createWritePriorityLock() {
        return BiasedLock.favoringWrites();
    }

    @Override
    protected ReadWriteLock getLock() {
        return BiasedLock.create();
    }

}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions