Writing custom synchronizers with AbstractQueuedSynchronizer
(AQS) in Java involves creating classes that encapsulate synchronization logic, such as locks, semaphores, or barriers. AQS simplifies the development of concurrency tools by handling queueing and thread state management. Below are the key steps and guidelines to write custom synchronizers using AbstractQueuedSynchronizer
:
1. Understand AQS Concepts
state
: AQS maintains a simpleint
value (state
) to represent the synchronization state. The meaning ofstate
varies depending on the synchronizer you create (e.g., lock held count for a reentrant lock, permits for a semaphore, etc.).- Exclusive Mode: Only one thread can hold the resource (e.g.,
ReentrantLock
). - Shared Mode: Multiple threads can share the resource (e.g.,
Semaphore
,CountDownLatch
).
2. Create a Custom Synchronizer Class
Your custom synchronizer class will generally:
- Extend
AbstractQueuedSynchronizer
. - Use the
state
variable to model your synchronization logic. - Implement key methods such as
tryAcquire
,tryRelease
,tryAcquireShared
, andtryReleaseShared
based on whether you’re implementing exclusive or shared behavior.
3. Implement Required Methods
For Exclusive Mode:
Override:
tryAcquire(int arg)
: Define the logic to acquire the resource exclusively. Returntrue
if the acquisition is successful, otherwise returnfalse
.tryRelease(int arg)
: Define the logic to release the resource. Returntrue
if the state transition occurs and allows waiting threads to proceed.
For Shared Mode:
Override:
tryAcquireShared(int arg)
: Define the logic to acquire the resource in shared mode. Return:- Negative if the acquisition fails.
- Zero if no more shared acquisitions are allowed.
- Positive if the acquisition is successful, and more threads can share the resource.
tryReleaseShared(int arg)
: Define the logic to release the resource in shared mode. Usually, decrement the state and decide if more threads can proceed.
4. Publish the Synchronizer to Clients
AQS is always used as part of a larger custom synchronization object. Expose public methods in your custom class to wrap the AQS functionality. For instance:
- For exclusive locks:
lock
andunlock
methods. - For shared locks: Methods such as
acquireShared
andreleaseShared
.
5. Example Implementations
Example 1: Simple Mutex (ReentrantLock Equivalent)
Code for an exclusive synchronizer:
package org.kodejava.util.concurrent;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class SimpleMutex {
// Custom synchronizer extending AQS
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int ignored) {
// Attempt to set state to 1 if it's currently 0 (lock is free)
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int ignored) {
// Only lock owner can release
if (getState() == 0 || getExclusiveOwnerThread() != Thread.currentThread()) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0); // Free the lock
return true; // Allow further attempts to acquire the lock
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1 && getExclusiveOwnerThread() == Thread.currentThread();
}
}
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
}
Example 2: Simple Semaphore (Shared Synchronizer)
Code demonstrating shared mode:
package org.kodejava.util.concurrent;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class SimpleSemaphore {
private static class Sync extends AbstractQueuedSynchronizer {
Sync(int permits) {
setState(permits);
}
@Override
protected int tryAcquireShared(int permits) {
for (; ; ) {
int current = getState();
int remaining = current - permits;
// Check if we can acquire the permits
if (remaining < 0 || compareAndSetState(current, remaining)) {
return remaining;
}
}
}
@Override
protected boolean tryReleaseShared(int permits) {
for (; ; ) {
int current = getState();
int next = current + permits;
// Release the permits
if (compareAndSetState(current, next)) {
return true;
}
}
}
}
private final Sync sync;
public SimpleSemaphore(int permits) {
sync = new Sync(permits);
}
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void release() {
sync.releaseShared(1);
}
}
6. Testing and Validation
- Test your custom synchronizer in multithreaded environments to ensure correctness.
- Use proper test tools like JUnit or TestNG.
- Validate edge cases, such as reentrancy (if applicable), releasing resources by non-owners, or negative state transitions.
Best Practices
- Always ensure a clean state transition in synchronization methods.
- Use atomic operations to modify
state
(e.g.,compareAndSetState
). - Avoid busy spinning (e.g.,
Thread.yield()
or blocking mechanisms are better). - Use AQS’s built-in blocking mechanisms like
acquire
,acquireShared
,release
, orreleaseShared
.
By following these steps and practices, you can create robust custom synchronizers tailored to your concurrency requirements.