Semaphore is a count semaphore. Conceptually, semaphores maintain a set of licenses. If necessary, each one is blocked before the license is available acquire()
, and then the license is acquired. Each release()
add a license, which may release a blocked fetch. However, instead of using the actual license object, Semaphore
only the number of available licenses is counted and action is taken accordingly.
Plainly, semaphore is a counter that releases the thread when the counter is not 0, and once it reaches 0, all new threads of the requested resource are blocked, including increasing the request to the licensed thread, which means that semaphore is not reentrant. Each request for a license causes the counter to be reduced by 1, and the same time each release of a license causes the counter to increase by 1, and once the 0 is reached, the new license request thread will be suspended.
The cache pool is good to use this idea to implement, such as link pool, object pool, and so on.
Listing 1 Object Pools
Package xylz.study.concurrency.lock;
Import Java.util.concurrent.Semaphore;
Import Java.util.concurrent.locks.Lock;
Import Java.util.concurrent.locks.ReentrantLock;
public class Objectcache<t> {
Public interface Objectfactory<t> {
T Makeobject ();
}
Class Node {
T obj;
Node Next;
}
final int capacity;
Final objectfactory<t> factory;
Final lock lock = new Reentrantlock ();
Final Semaphore Semaphore;
Private Node head;
Private Node tail;
public ObjectCache (int capacity, objectfactory<t> Factory) {
This.capacity = capacity;
This.factory = Factory;
This.semaphore = new semaphore (this.capacity);
This.head = null;
This.tail = null;
}
Public T GetObject () throws Interruptedexception {
Semaphore.acquire ();
return GetNextObject ();
}
Private T GetNextObject () {
Lock.lock ();
try {
if (head = = null) {
return Factory.makeobject ();
} else {
Node ret = head;
head = Head.next;
if (head = = null) tail = NULL;
Ret.next = Null;//help GC
return ret.obj;
}
} finally {
Lock.unlock ();
}
}
private void Returnobjecttopool (T t) {
Lock.lock ();
try {
node node = new node ();
node.obj = t;
if (tail = = null) {
head = tail = node;
} else {
tail.next = node;
tail = node;
}
} finally {
Lock.unlock ();
}
}
public void Returnobject (T t) {
Returnobjecttopool (t);
Semaphore.release ();
}
}
Listing 1 Describes an object pool implementation based on semaphore semaphore. This object pool supports a maximum of capacity objects, which are passed in the constructor. The Object pool has a FIFO-based queue that takes objects from the head node of the object pool each time, and constructs a new object directly if the head node is empty. Otherwise, the head node object is removed and the head node moves backwards. In particular, if the number of objects is exhausted, the new thread will be blocked until an object is returned. When the object is returned, the object is added to the tail node of the FIFO and an idle semaphore is released to indicate that an object pool is available.
In fact, the principle of object pool, thread pool is basically like this, but the real object pool, thread pools to deal with more complex logic, so the implementation of the need to do a lot of work, such as time-out mechanism, automatic recovery mechanism, the validity of objects and so on.
In particular, the signal volume is only suspended when the signal is not enough, but it is not guaranteed enough to get the object and return the object is thread-safe, so in Listing 1 still need to lock lock to ensure concurrency correctness.
Initializes the semaphore to 1 so that it can be used as a mutually exclusive lock with a maximum of one license available at the time of use. This is often referred to as a binary semaphore because it can only have two states: one available license, or 0 licenses available. When used in this manner, the binary semaphore has some attribute ( Lock
Unlike many implementations) that can be freed by the thread "lock" rather than by the owner (because the semaphore has no ownership concept). This can be useful in some specialized contexts, such as deadlock recovery.
The above remark means that when a thread A has a semaphore with a semaphore of 1, the other thread can only wait for the thread to release the resource to continue, when thread A that holds the semaphore is the equivalent of holding the lock, and the other thread's continuation requires the lock, so the release of thread A determines the operation of the other thread. The equivalent of playing a "lock" role.
In addition, the same as fair lock unfair lock, the semaphore also has the fairness. If a semaphore is fair, it indicates that the thread is licensed in the FIFO order when the semaphore is acquired, which is released in the order in which it was requested. In particular, the so-called Order of requests refers to the order in which the request semaphore enters the FIFO queue, it is possible that a line enters upgradeable the request signal and then go in the request queue, then the order of the thread gets the semaphore is later than the request but advanced into the request queue of the thread. This has been a lot of talk in the fair lock and the non-fair lock.
In addition to acquire, Semaphore has several similar acquire methods that can better handle interrupts and timeouts or asynchronous features, and can refer to the JDK API.
Follow the same learning principles and analyze the main implementations below. The acquire method of semaphore actually accesses the AQS acquiresharedinterruptibly (Arg) method. This can be referred to countdownlatch section or AQS section.
So semaphore's await implementation is also relatively simple. Unlike Countdownlatch, Semaphore distinguishes between fair and non-fair signals.
Listing 2 Fair Signal acquisition method
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;
}
}
Listing 3 non-fair signal acquisition methods
protected int tryacquireshared (int acquires) {
Return nonfairtryacquireshared (acquires);
}
final int nonfairtryacquireshared (int acquires) {
for (;;) {
int available = GetState ();
int remaining = Available-acquires;
if (Remaining < 0 | |
Compareandsetstate (available, remaining))
return remaining;
}
}
As you can see in Listing 2 and listing 3, the fair signal and the non-fair signal are the first attempts to get a signal, and the fair Semaphore always queues the current thread into the Aqs CLH queue (because the head node thread of the queue is most likely not the current thread when the first attempt is made. Of course, the same thread is not excluded to enter the semaphore for the second time, thereby obtaining the semaphore sequentially according to the order FIFO of the Aqs CLH queue, and for the non-fair semaphore, the first attempt immediately to get the semaphore, once the semaphore remaining number available greater than the number of requests (acquires is usually 1) , the thread is immediately freed without the need to queue Aqs queues. Only remaining<0 (that is, when the semaphore is not enough) will enter the Aqs queue.
Therefore, the throughput of the non-fair semaphore is always greater than the fair semaphore throughput, but there is a "hunger and thirst" phenomenon in which the non-fair semaphore and the unfair lock need to be emphasized, i.e. the active thread may always get the semaphore, while the inactive thread may have difficulty getting the semaphore. For fair semaphores, this problem does not exist because the semaphore is always obtained by the order of the requested thread.
References:
- The use of semaphores (Semaphore) in producer and consumer models
- What is a mutex and semaphore in Java? What is the main difference?
- 5 things you don't know about Java.util.concurrent, part 2nd
- Semahores
In layman's Java Concurrency (12): Lock mechanism Part 7 semaphore (Semaphore) [Turn]