Semaphore source code analysis of Java concurrent series, semaphore source code
Semaphore is a common class in the JUC package. It is an application in the AQS sharing mode and allows multiple threads to operate on shared resources at the same time, in addition, it can effectively control the number of concurrent jobs and use it to control traffic. Semaphore provides the concept of a license, which can be considered as a bus ticket. Only those who have successfully obtained the ticket can get on the bus, and there are a certain number of tickets, it is impossible to send it without limits, which will lead to overload of buses. So when the ticket is delivered (the bus is fully loaded), other people can only wait for the next bus. If someone gets off the bus halfway, his location will be free, so if someone else wants to get on the bus, he will be able to get the ticket again. We can use Semaphore to implement various pools. At the end of this article, we will write a simple database connection pool. First, let's take a look at the Semaphore constructor.
// Constructor 1 public Semaphore (int permits) {sync = new NonfairSync (permits);} // constructor 2 public Semaphore (int permits, boolean fair) {sync = fair? New FairSync (permits): new NonfairSync (permits );}
Semaphore provides two parameter-free constructors. Both constructors must pass in an initial License Quantity. semaphores constructed by constructor 1 are obtained in an unfair way when obtaining a license, you can use the constructor 2 to specify the way to obtain the license through parameters (fair or unfair ). Semaphore provides two types of APIS for obtaining and releasing licenses. By default, Semaphore obtains and releases a license, and can also input parameters to simultaneously obtain and release multiple licenses. In this article, we will only discuss how to obtain and release a license each time.
1. obtain a license
// Obtain a license (Response interrupted) public void acquire () throws InterruptedException {sync. acquireSharedInterruptibly (1);} // obtain a license (not responding to interruptions) public void acquireUninterruptibly () {sync. acquireShared (1);} // try to obtain a license (unfair access) public boolean tryAcquire () {return sync. nonfairTryAcquireShared (1)> = 0;} // try to obtain the license (timed acquisition) public boolean tryAcquire (long timeout, TimeUnit unit) throws InterruptedException {return sync. tryAcquireSharedNanos (1, unit. toNanos (timeout ));}
The above API is the default license acquisition operation provided by Semaphore. Only one license is obtained each time, which is a common situation in real life. In addition to direct retrieval, direct retrieval may block the thread after a failure, but it does not. Note that the tryAcquire method is obtained in an unfair way. In normal times, we usually use the acquire method to obtain a license. Let's take a look at how it is obtained. We can see that the acquire method directly calls sync. acquireSharedInterruptibly (1). This method is the method in AQS. We have discussed it in the AQS source code series. Now let's review it.
// Obtain the lock (share mode) public final void acquireSharedInterruptibly (int arg) throws InterruptedException {// first checks whether the Thread is interrupted. if yes, an exception is thrown if (Thread. interrupted () {throw new InterruptedException ();} // 1. try to get the lock if (tryAcquireShared (arg) <0) {// 2. if the retrieval fails, enter the method doAcquireSharedInterruptibly (arg );}}
The acquireSharedInterruptibly method first calls the tryAcquireShared method to try to obtain it. tryAcquireShared is an abstract method in AQS. The two Derived classes FairSync and NonfairSync implement the logic of this method. FairSync implements the fair access logic, while NonfairSync implements the unfair access logic.
Abstract static class Sync extends actqueuedsynchronizer {// unfair way to try to get final int nonfairTryAcquireShared (int acquires) {for (;) {// get available license int available = getState (); // obtain the remaining license int remaining = available-acquires; // 1. if remaining is less than 0, the system returns remaining // 2. if the remaining value is greater than 0, the synchronization status is updated first and then the remaining if (remaining <0 | compareAndSetState (available, remaining) {return remaining ;}}} is returned ;}}}} // non-fair synchronization static final class NonfairSync extends Sync {private static final long serialVersionUID =-2694181094443567898l; NonfairSync (int permits) {super (permits );} // try to obtain the license protected int tryAcquireShared (int acquires) {return nonfairTryAcquireShared (acquires) ;}// fair synchronization static final class FairSync extends Sync {private static final long serialVersionUID = nobody; fairSync (int permits) {super (permits) ;}// try to obtain the license protected int tryAcquireShared (int acquires) {(;;) {// determine whether there is a queue in front of the synchronization queue if (hasQueuedPredecessors () {// if yes, return-1 directly, indicating that the attempt to obtain the failed return-1 ;} // obtain the available license int available = getState (); // obtain the remaining license int remaining = available-acquires; // 1. if remaining is less than 0, the system returns remaining // 2. if the remaining value is greater than 0, the synchronization status is updated first and then the remaining if (remaining <0 | compareAndSetState (available, remaining) {return remaining ;}}} is returned ;}}}}
Note that the tryAcquireShared method of NonfairSync directly calls the nonfairTryAcquireShared method, which is in the parent Sync class. The logic for unfair lock acquisition is to first retrieve the current synchronization status (the synchronization status indicates the number of licenses), and subtract the current synchronization status from the parameters of the parameter, if the result is not less than 0, it indicates that there is still a license available, then you can directly use CAS to update the synchronization status, and finally return this result no matter whether the result is less than 0. Here, we need to understand the meaning of the return value of the tryAcquireShared method. If the return value is negative, the retrieval fails. If the return value is zero, the current thread gets the result successfully, but the subsequent thread cannot obtain the result, A positive number indicates that the current thread is successfully obtained and can be obtained by subsequent threads. Let's take a look at the code of the acquireSharedInterruptibly method.
// Obtain the lock (share mode) public final void acquireSharedInterruptibly (int arg) throws InterruptedException {// first checks whether the Thread is interrupted. if yes, an exception is thrown if (Thread. interrupted () {throw new InterruptedException ();} // 1. attempt to get the lock // negative number: indicates that the acquisition failed // zero value: indicates that the current thread has succeeded in obtaining the lock, but the subsequent thread cannot obtain the lock // positive number: indicates that the current thread has succeeded in obtaining the lock, and the successor thread can also obtain the successful if (tryAcquireShared (arg) <0) {// 2. if the retrieval fails, enter the method doAcquireSharedInterruptibly (arg );}}
If the returned remaining is less than 0, the retrieval fails. Therefore, tryAcquireShared (arg) <0 is true, so the doAcquireSharedInterruptibly method will be called next. We will talk about AQS in this method, it encapsulates the current thread as a node and puts it at the end of the synchronization queue, and may suspend the thread. This is also the reason why the thread will be blocked when the remaining is less than 0. If the returned remaining> = 0, it indicates that the current thread is successfully obtained. Therefore, if tryAcquireShared (arg) <0 is flase, the method doAcquireSharedInterruptibly is no longer called to block the current thread. The above is the entire unfair access logic, while the fair access is only prior to this, the hasQueuedPredecessors method is called to determine whether a synchronization queue is in the queue. If yes, return-1 indicates that the acquisition failed, otherwise, the following steps will be continued.
2. Release the license
// Release a license public void release () {sync. releaseShared (1 );}
The call to the release method is to release a license, and its operation is very simple. It calls the releaseShared method of AQS. Let's take a look at this method.
// Release Lock operation (share mode) public final boolean releaseShared (int arg) {// 1. try to release the lock if (tryReleaseShared (arg) {// 2. if the release succeeds, the other thread doReleaseShared (); return true;} return false;} will be awakened ;}
The releaseShared method of AQS first calls the tryReleaseShared method to try to release the lock. The implementation logic of this method is in the sub-class Sync.
Abstract static class Sync extends actqueuedsynchronizer {... // try to release protected final boolean tryReleaseShared (int releases) {for (;) {// get the current synchronization status int current = getState (); // Add the input parameter int next = current + releases to the current synchronization status; // if the addition result is smaller than the current synchronization status, an error is returned if (next <current) {throw new Error ("Maximum permit count exceeded");} // update the value of synchronization status in CAS mode. if the update is successful, true is returned. Otherwise, if (compareAndSetState (current, next) {return true ;}}}...}
You can see that the tryReleaseShared method uses the for loop for spin. First, get the synchronization status, add the synchronization status with the passed parameters, and then update the synchronization status in CAS mode, if the update is successful, true is returned and the method is displayed. Otherwise, the loop continues until the update is successful. This is the process of releasing the license.
3. Write a connection pool
The Semaphore code is not very complex. A common operation is to obtain and release a license. The implementation logic of these operations is also relatively simple, but this does not hinder the wide application of Semaphore. Next we will use Semaphore to implement a simple database connection pool. Through this example, we hope that readers can have a better understanding of the use of Semaphore.
Public class ConnectPool {// connection pool size private int size; // database connection set private Connect [] connects; // connection status flag private boolean [] connectFlag; // The remaining number of available connections private volatile int available; // Semaphore private semaphore Semaphore; // constructor public ConnectPool (int size) {this. size = size; this. available = size; semaphore = new Semaphore (size, true); connects = new Connect [size]; connectFlag = new boolean [size]; initConnects ();} // Initialize the private void initConnects () {// generate a specified number of database connections for (int I = 0; I <this. size; I ++) {connects [I] = new Connect () ;}// obtain the database connection private synchronized Connect getConnect () {for (int I = 0; I <connectFlag. length; I ++) {// traverse the set to find unused connections if (! ConnectFlag [I]) {// set the connection to connectFlag [I] = true; // The number of available connections minus 1 available --; System. out. println ("[" + Thread. currentThread (). getName () + "] to get the remaining connections:" + available); // return the connection reference return connects [I] ;}} return null ;} // obtain a connection to public Connect openConnect () throws InterruptedException {// obtain the license semaphore. acquire (); // get the database connection return getConnect () ;}// release a connection public synchronized void release (Connect connect Connect) {for (int I = 0; I <this. size; I ++) {if (connect = connects [I]) {// set the connection to connectFlag [I] = false; // The number of available connections plus 1 available ++; System. out. println ("[" + Thread. currentThread (). getName () + "] to release the remaining connections:" + available); // release the license semaphore. release () ;}}// number of available connections public int available () {return available ;}}
Test code:
Public class TestThread extends Thread {private static ConnectPool pool = new ConnectPool (3); @ Override public void run () {try {Connect connect = pool. openConnect (); Thread. sleep (100); // take a break from the pool. release (connect);} catch (InterruptedException e) {e. printStackTrace () ;}} public static void main (String [] args) {for (int I = 0; I <10; I ++) {new TestThread (). start ();}}}
Test results:
We use an array to store database connection references. When initializing the connection pool, we call the initConnects method to create a specified number of database connections and store their references in the array, in addition, there is an array of the same size to record whether the connection is available. When an External Thread requests a connection, it first calls semaphore. acquire () to obtain a license, sets the connection status to in use, and finally returns a reference to the connection. The number of licenses is determined by the parameters passed in during the construction. semaphore is called every time. the number of licenses in the acquire () method is reduced by 1. If the number is reduced to 0, no connection is available. If other threads attempt to obtain the license again, it will be blocked. When a thread releases a connection, semaphore is called. release () releases the license, and the total number of licenses increases again, indicating that the number of available connections has increased. The previously blocked thread will wake up to continue obtaining connections, in this case, the connection will be successfully obtained after obtaining the connection again. In the test example, a connection pool with three connections is initialized. We can see from the test results that each time the thread obtains a connection, the remaining connections will be reduced by 1, when the value is reduced to 0, other threads cannot obtain the data again. At this time, you must wait for a thread to release the connection before obtaining the data again. We can see that the number of remaining connections always changes between 0 and 3, which indicates that this test was successful.
The above is all the content of this article. I hope it will be helpful for your learning and support for helping customers.