Lock
Lock is the most core tool in J.U.C., its role and the previous explained synchronized, is also used to solve the problem of thread safety in multi-threaded environment. The Lock mechanism is used in many places in the J.U.C package.
J.U.C, java.util.concurrent, is a popular toolkit for concurrent programming. This package contains many components for use in concurrent scenarios, such as thread pools, blocking queues, timers, synchronizers, concurrent collections, and more. And the author of the package is the famous Doug Lea.
Prior to the Lock interface, Java applications had to rely on the synchronized keyword to secure concurrency for multiple threads. Synchronized, however, suffers from a disadvantage in some scenarios, namely that it is not suitable for all concurrent scenarios. But after Java5, the advent of Lock addresses the shortcomings of synchronized in certain scenarios, and it is more flexible than synchronized.
Lock is an interface that defines abstract methods for releasing and acquiring locks. There are many classes that implement the Lock interface. The following are some common Lock implementations:
-
ReentrantLock: represents a ReentrantLock and is the only class that implements the Lock interface. Reentrant lock refers to a thread that acquires a lock and does not block it again, but increases the number of reentrant times directly associated with a counter
-
ReentrantReadWriteLock: a reentrent read/WriteLock that implements the ReadWriteLock interface. This class maintains two locks, ReadLock and WriteLock, that implement the Lock interface. Read/write lock is a tool suitable for solving thread safety problems in the scenario of reading too much and writing too little. The basic principles are: Read and read are not mutually exclusive, read and write are mutually exclusive, and write and write are mutually exclusive. This means that operations that affect changes in data are mutually exclusive.
-
StampedLock: StampedLock is a new locking mechanism introduced in JDK8, which can be simply considered as an improved version of read/write lock. Read/write lock enables read/write concurrency through the separation of read and write functions. However, read/write conflicts may cause hunger of writer threads if a large number of read threads exist. StampedLock is an optimistic read strategy that keeps the optimistic lock from blocking the writer thread at all
ReentrantLock
usage
Count++ 10000 is less than 10,000 because it is non-atomic. One method is to add Synchronized and the other method is to add lock. The code is as follows:
public class ReentrantLockDemo {
static ReentrantLock lock = new ReentrantLock();
static int count = 0;
public static void incr(a) {
// Preempt the lock. If the lock is not preempt, it will block
lock.lock();
try {
count++;
} catch (Exception e) {
e.printStackTrace();
} finally{ lock.unlock(); }}public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000; i++) {
new Thread(ReentrantLockDemo::incr).start();
}
Thread.sleep(6000);
System.out.println("result:"+ count); }}Copy the code
The final result is 10,000.
Note that you must use unlock() in the finally block.
Reentrant lock
Reentrant lock means that the same thread can obtain the same lock repeatedly. In other words, if the current thread T1 obtains the lock by calling the lock method, it will not block to obtain the lock again. It simply increases the retry times. Synchronized and ReentrantLock are reentrant locks.
The class diagram
Here is the class diagram in ReentrantLock:
The ReentrantLock constructor defaults to an unfair lock and can also be specified.
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
Copy the code
Below into the key part, explain AbstractQueuedSynchronizer (AQS) source.
AQS
The importance of AQS
We first introduce AQS (AbstractQueuedSynchronizer), the importance of to see AQS is used in what kind of.
As shown in the figure, AQS are used in workers of ReentrantLock, ReentrantReadWriteLock, Semaphore, CountDownLatch, and ThreadPoolExcutor (JDK 1.8). AQS are the underlying principles of these classes.
Many of the above classes are frequently used by us, and most of them have been introduced in detail in previous classes. Therefore, behind many important tool classes in JUC package are inseparable from the AQS framework. Therefore, the importance of AQS is self-evident, and AQS is the cornerstone of J.U.C.
tryAcquire(arg)
First enter the Lock method of a fair lock and see a acquire method
Click inside to enter the parent AQS class
Click tryAcquire to go back to the specific subclass tryAcquire
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
2,3,4,5... That's the number of reentries
int c = getState();
// Indicates no lock
if (c == 0) {
// If the queue is empty, go to CAS to grab the lock
if(! hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
// Save the current thread to the 'exclusiveOwnerThread'
setExclusiveOwnerThread(current);
return true; }}// If you already have a lock, check if it is your own
else if (current == getExclusiveOwnerThread()) {
// If yes, add state +1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false; }}Copy the code
Let’s look at the lock methods for unfair locking
final void lock(a) {
// This is an unfair lock. If there is no thread waiting in the queue, CAS it first to see if it succeeds
if (compareAndSetState(0.1))
setExclusiveOwnerThread(Thread.currentThread());
else
// If not, proceed with acquire
acquire(1);
}
Copy the code
Or the method inside the AQS class
public final void acquire(int arg) {
if(! tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }Copy the code
Again, click tryAcquire to enter the implementation inside the unfair lock
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
Copy the code
Jump back to the Sync class. Note that it is very loopy, several classes jump back and forth, so first look at the inheritance of the above several classes
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
/ / unlocked
if (c == 0) {
/ / CAS lock
if (compareAndSetState(0, acquires)) {
// If successful, record the current thread
setExclusiveOwnerThread(current);
return true; }}// State +1 if you already have a lock
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
Copy the code
At this point, we are going to talk about the difference between a fair lock and an unfair lock.
Let’s say thread A holds A lock, thread B requests the lock, thread A already holds the lock, thread B is stuck waiting, and while it waits, thread B is suspended, that is, blocked, so when thread A releases the lock, it should be thread B’s turn to wake up and acquire it, However, if a thread C suddenly jumps the queue and requests the lock, the unfair strategy will be to give the lock to thread C, because it is expensive to wake up thread B, and it is likely that thread C has already acquired the lock and released the lock after it has completed its task. Compared to the long process of waiting for the awakened thread B, jump the queue behavior will make the thread C itself over into blocking process, if the lock code that executes the contents of the small, thread C can be quickly completed the task, and before the thread B is fully awakened, the lock out, this is a win-win situation, for the thread C, Not having to wait improves its efficiency, and for thread B, there is no delay in acquiring the lock, because by the time it wakes up, thread C has already released the lock, because thread C’s execution speed is faster than thread B’s awakening speed, so Java designers design unfair locks. Is to improve the overall operational efficiency.
As shown in the above source code, the tryAcquire and nonfairTryAcquire methods are almost the same, the only difference is that there is one more fair lock! Hasqueuedtoraise () determines the condition that, if it is a fair lock, the current thread will not attempt to acquire the lock once it is already queued; For an unfair lock, regardless of whether there is already a queue, the thread will try to acquire the lock, if not, queue again. An unfair lock is a critical section where the state is 0 when the previous thread has just released the lock, and then wakes up the next thread to grab the lock by CAS. At this time, another thread comes in and the CAS grabs the lock first.
ReentrantLock Default is unfair lock.
public ReentrantLock(a) {
sync = new NonfairSync();
}
Copy the code
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
The above acquisition failure proves that there is a thread in use, or the current thread cannot re-enter, so it needs to add the current thread to the bidirectional linked list and wait. AcquireQueued (addWaiter(Node.exclusive), ARg) splits it into two methods.
- AddWaiter (node.exclusive) -> Add a mutex Node
- AcquireQueued () -> Spinlock and blocking operations
See first addWaiter (Node. EXCLUSIVE)
private Node addWaiter(Node mode) {
// Encapsulate the current thread as a Node.
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// The tail node is not empty
if(pred ! =null) {
node.prev = pred;
// Try to insert CAS at the end of the chain
if (compareAndSetTail(pred, node)) {
pred.next = node;
returnnode; }}// If it is empty or fails to insert to the tail node
enq(node);
return node;
}
private Node enq(final Node node) {
/ / spin
for (;;) {
Node t = tail;
// The tail node is empty
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
// Both the head and tail nodes point to an empty node
tail = head;
} else {
node.prev = t;
// Repeatedly insert CAS to the end of the chain until successful
if (compareAndSetTail(t, node)) {
t.next = node;
returnt; }}}}Copy the code
Then look at acquireQueued ()
// After adding to the end of the chain, come in and spin again
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
/ / spin
for (;;) {
final Node p = node.predecessor();
// If the preceding node is the head node, prove yourself to be the first position, and proceed tryAcquire
//tryAcquire analyzed above that there are fair and unfair locks
if (p == head && tryAcquire(arg)) {
// Set yourself as the head node and return directly
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// Otherwise, let the thread unblock (park)
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true; }}finally {
if(failed) cancelAcquire(node); }}// This method is used to check whether you are ready to rest.
// If the thread at the front of the queue gives up, it will not work
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/* * This node has already set status asking a release * to signal it, so it can safely park. */
return true;
if (ws > 0) {
/* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt(a) {
/ / LockSupport. Park obstruction
LockSupport.park(this);
// Interrupt status
return Thread.interrupted();
}
Copy the code
- SIGNAL=-1 indicates that the successor node is waiting for the current node to wake up. When a successor node joins the queue, its status is updated to SIGNAL.
- Whereas ws > 0 only CANCELLED = 1, which means the thread has been CANCELLED. In this case, the node needs to be removed.
unlock()
Let’s look at the process of unlocking
public void unlock(a) {
sync.release(1);
}
Copy the code
So let’s go to the Release method
public final boolean release(int arg) {
if (tryRelease(arg)) {
// Get the head node in the current AQS queue.
Node h = head;
// The head node is not empty
if(h ! =null&& h.waitStatus ! =0)
unparkSuccessor(h);
return true;
}
return false;
}
Copy the code
unlock
protected final boolean tryRelease(int releases) {
// This is minus 1, because it might be reentrant
int c = getState() - releases;
if(Thread.currentThread() ! = getExclusiveOwnerThread())throw new IllegalMonitorStateException();
boolean free = false;
// If it is 0, it is completely released
if (c == 0) {
// mark it as free
free = true;
// marks the owner of the current thread as null
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
Copy the code
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// Indicates that the state can be awakened
if (ws < 0)
// return to 0
compareAndSetWaitStatus(node, ws, 0);
// The next node of the header
Node s = node.next;
// If the node is empty or the thread has been destroyed, an exception occurs, etc
if (s == null || s.waitStatus > 0) {
// Empty the node
s = null;
// Start at the end of the chain and find nodes less than or equal to 0
for(Node t = tail; t ! =null&& t ! = node; t = t.prev)if (t.waitStatus <= 0)
s = t;
}
// Wake up the thread if it is not empty
if(s ! =null)
LockSupport.unpark(s.thread);
}
Copy the code
The flow chart
Good, AQS source here on the end of the analysis, very clear, very simple, the above process with a diagram demonstration, deepen the impression.
Thank you for watching