Semaphore
public class Semaphore implements java.io.Serializable
- A counting semaphore.
- Usually used to limit the number of threads that can access certain resources (physical or logical).
Conceptually, semaphores maintain a license set state. If necessary, each acquire() is blocked before the license is available, and then the license is obtained. Each release() adds a license, which may release a blocking acquirer. However, without using the actual license object, Semaphore only counts the number of licenses available and takes corresponding actions.
When the semaphore==1, it is a mutually exclusive semaphore and can be used as a mutually exclusive lock.
_is divided into two modes: fair and unfair. Fair mode is FIFO order to select site and obtain permission. Unfair mode does not guarantee the order in which threads obtain permission and allows queue jumping.
Member variables
/** All operations pass through the AbstractQueuedSynchronizer subclass Sync */
private final Sync sync;
Construction method
/**
* Create Semaphore with a given number of licenses and unfair fair fair settings.
*/
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
/**
* Create a Semaphore with a given number of licenses and a given fair setting.
*/
public Semaphore(int permits, boolean fair) {
sync = (fair)? new FairSync(permits) : new NonfairSync(permits);
}
acquire() or acquire (int permits): acquire() is equivalent to acquire(1), from which the semaphore obtains a given number of permissions, blocking the thread until these permissions are provided, or the thread has been interrupted.
/*Semaphore.Sync Called AQS.acquireSharedInterruptibly*/
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
/*tryAcquireShared Overwrite in Semphore. FairSync or NonFairSync*/
/**
* Fair version
*/
final static class FairSync extends Sync {
FairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
Thread current = Thread.currentThread();
for (;;) {
Thread first = getFirstQueuedThread();
if (first != null && first != current)
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
/**
* NonFair version
*/
final static class NonfairSync extends Sync {
NonfairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
/**
* NonFair version
*/
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
/**
* Acquires in shared interruptible mode.
* @param arg the acquire argument
*/
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
From the source code, we can see that FairSync overwrote tryAcquireShared itself, and NonfairSync overwrote it, but called the nonfairTryAcquireShared method of the parent Sync.
Acquire Uninterruptibly (): acquire Uninterruptibly () == acquire Uninterruptibly (1), from which semaphores obtain a given number of licenses and block threads until they are provided.
Acquisition Uninterruptibly is similar to acquire, except that if the current thread is interrupted while waiting for permission, it will continue to wait and its position in the queue will not be affected. The latter throws InterruptedException.
acquire can be divided into four steps as a whole:
1. If acquire(arg) succeeds, there will be no problem. The lock() process will be over. If it fails, do operation 2.
2. Create an exclusive or shared node (Node) and join the end of the CHL queue (CHL queue is a non-blocking FIFO queue). Operation 3.
3. Spin attempts to acquire locks, failing to decide whether to hang (park()) based on the previous node until the lock is successfully acquired. Operation 4.
4. If the current thread has been interrupted, then the current thread (clearing the interrupt bit) is interrupted.
If the operation is uninterruptible, then there is no fourth step. If acquire Uninterruptibly, it will spin all the time.
boolean tryAcquire(): Equivalent to tryAcquire(1), which is obtained from this semaphore only when there is a given number of permissions in the semaphore at the time of invocation.
Even if this semaphore has been set to use a fair sort strategy, calling tryAcquire will immediately obtain permission (if one is available), regardless of whether there are currently waiting threads. In some cases, this "intrusion" may be useful, even if it breaks down fairness. If you want to comply with fair settings, use tryAcquire(permits, 0, TimeUnit.SECONDS), which is almost equivalent (it also detects interrupts).
public boolean tryAcquire(int permits) {
if (permits < 0) throw new IllegalArgumentException();
return sync.nonfairTryAcquireShared(permits) >= 0;
}
In fact, tryAcquire is the same as the former part of acquisition implementation. The latter part is much more than the latter. If the lock is not acquire d at that time, it will spin and wait. There are two strategies: fair and unfair, while tryAcquire only takes unfair strategy.
tryAcquire also supports spinning attempts to acquire locks within a given waiting time.
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquireShared(arg) >= 0 ||
doAcquireSharedNanos(arg, nanosTimeout);//Spinning attempts to acquire locks within a given waiting time
}
release(): Equivalent to release(1), releases a given number of licenses and returns them to the semaphore.
/*Sync The parent AQS.releaseShared method*/
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();//Wake up waiting threads
return true;
}
return false;
}
/*Sync Override the parent AQS.tryReleaseShared method*/
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int p = getState();
if (compareAndSetState(p, p + releases))
return true;
}
}
From the source code, we can see that the semaphore acquisition and release actions are implemented by the sub-class Sync, and the subsequent actions of action success or failure are implemented by AQS, mainly thread blocking and wake-up, namely LockSupport.park() and LockSupport.unpark().
CountDownLatch
public class CountDownLatch
- A synchronization helper class that allows one or more threads to wait until a set of operations are performed in other threads.
- Initialize CountDownLatch with the given count. Because the countDown() method is called, the await method is blocked until the current count reaches zero. After that, all waiting threads are released, and all subsequent calls to await are immediately returned. This phenomenon occurs only once - counts cannot be reset. If you need to reset the count, consider using Cyclic Barrier.
- CountDownLatch is a general synchronization tool that has many uses. Count 1 initialized Count DownLatch is used as a simple on/off latch, or entry: all threads that call await wait at the entry until the entry is opened by calling countDown().
- A useful feature of CountDownLatch is that it does not require the thread calling the countDown method to continue until the count reaches zero, but only prevents any thread from continuing to pass through an await until all threads can pass.
Construction method
/**
* Construct a CountDownLatch initialized with a given count.
*/
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
await(): causes the current thread to wait until the latch counts down to zero, unless the thread is interrupted.
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);//AQS parent method
}
/*AQS.acquireSharedInterruptibly Method*/
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
/*Sync Override the parent AQStryAcquireShared method*/
public int tryAcquireShared(int acquires) {
return getState() == 0? 1 : -1;
}
From the source code, counting judges subclass maintenance, and AQS defines volatile int state to ensure its visibility.
AQS then judges whether to wake up a thread based on the results of subclasses. Compared with await(long timeout, TimeUnit unit), it adds a timeout judgment operation.
void countDown(): Decreases the count of latches and releases all waiting threads if the count reaches zero.
public void countDown() {
sync.releaseShared(1);
}
/*AQS.releaseShared*/
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
/*Sync Overwritten AQS. release Shared*/
public boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
long getCount(): Returns the current count.
public long getCount() {
return sync.getCount();
}
int getCount() {
return getState();
}
Semaphore, CountDownLatch (which also interprets semaphores or counts as locks):
1. Mutual exclusive semaphores can be used.
2. Semaphore supports interruptible, uninterruptible and timeout acquisition locks, while CountDownLatch has only two types: interruptible and timeout.
3. Lock maintenance has its own internal class Sync implementation (acquisition, release), the behavior after maintenance is implemented by AQS, thread blocking and wake-up.
4. Semaphore can be understood as creating N licenses on a fixed basis while instantiating, and at most N threads are licensed. Other threads can only obtain permission until they have a thread release license.
5. CountDownLatch understands that when instantiating, N preconditions are created. Only when all preconditions are countDown completed, the waiting thread will be released, otherwise it will wait all the time.
StampedLock
public class StampedLock implements java.io.Serializable
- Support three modes of reading, writing and optimistic reading lock.
- An API added to java.util.concurrent.locks in jdk1.8.
- Based on CLH (spin lock), CLH lock is a kind of spin lock, i.e. spin waiting, which guarantees no starvation and the sequence of FIFO operation.
writeLock | Exclusive lock, while only one thread can acquire the lock, other write threads must wait |
readLock | Shared lock. When a write lock is not held, multiple threads can acquire the lock at the same time. If held, other threads block. |
OptimisticRead | Optimistic read locks, optimistic that write locks are not held, operating data is not set through CAS state, |
write is similar to read
writeLock() | Blocked and uninterruptible |
tryWriteLock() | Return immediately |
tryWriteLock(long time, TimeUnit unit) | Blocking with time constraints, responding to interrupts |
writeLockInterruptibly() | Blocking, responding interrupt |
unlockWrite(long stamp) | Release lock |
Optimistic Read Lock: When acquiring a lock, it will determine whether the write lock is held by other threads. If it is held, it will return 0 immediately. Otherwise, it will return a stamp version information of 0. At this time, it can be considered that the read lock is owned. However, in the process of acquiring data and processing data, the write lock may be held by other threads, that is, the data may change, so optimism is used. When reading locks, consider this issue, such as using violate to ensure memory visibility, or judging whether write changes exist during this period, and then retrieving the changed data or throwing exceptions.
However, it is undeniable that optimistic read locks are suitable for scenarios with more reads and fewer writes, which can significantly improve program throughput.
When using acquisition locks, do not use threads with interrupt state to perform acquisition blocking operations without responding to interrupt locks. It is easy to cause infinite dead cycles, resulting in full load of the system.