Essence: locks are used to solve thread safety problems
Other implementations of Lock in Java, such as WiteLock write Lock and ReadLock read Lock, are mainly expanded with ReentrantLock reentry Lock in this paper
ReentrantLock reentrant lock
Reentry lock and mutex lock are used to solve the deadlock problem
1. Use of reentrantlock
static Lock lock = new ReentrantLock(); static int sum = 0; public static void incr(){ lock.lock(); //Preemptive lock. If there is no preemption, it will block try { Thread.sleep(1); sum ++ ; } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); //Release lock } } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000; i++) { new Thread(()->{ LockExample.incr(); }).start(); } Thread.sleep(3000); System.out.println(sum); //Output 1000 }
J. The difference between U.C Lock and Synchronized is that the locking and releasing of Lock require manual operation
2. Principle and implementation of reentrantlock
Satisfying the mutual exclusion of threads means that only one thread is allowed to enter the locked code at the same time
Basic conditions for a lock:
- Identification with lock and without lock
- Thread processing without preemptive lock
- Wait (directly block first and release CPU resources)
- wait/notify exists. The specified thread cannot be awakened
- LockSupport.park/unpark (block the specified thread and wake up the specified thread)
- Condition
- Queuing (N threads running are blocked and the thread is waiting)
- N queued threads are stored through a data structure
- Wait (directly block first and release CPU resources)
- Lock release process
- Locksupport.unpark (thread) - > wake up the specified thread in the queue
- Fairness of lock (whether queue jumping is allowed)
If the above conditions are not very clear, continue to look at the lock flow analysis chart below
3. Source code analysis
1. Lock.lock()
public void lock() { //Here you can see that there are two methods: FairSync and NonfairSync sync.lock(); } //Unfair lock code final void lock() { //Unfair lock and fair lock are different here //For unfair locks, first try to update the state state. If successful, get the lock directly //Fair lock is to get the preemptive lock directly if (compareAndSetState(0, 1)) //If the state is successfully modified here, set the lock owner to the current thread directly setExclusiveOwnerThread(Thread.currentThread()); else //Otherwise, grab the lock. Next, look here### acquire(1); } //AbstractQueuedSynchronizer public final void acquire(int arg) { //If the attempt to obtain the lock fails, it will be added to the AQS queue. First look at tryAcquire if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } //Code for fair lock preemption nonfairTryAcquire for non-public lock preemption protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //The difference from unfair locks is described under hasqueued predecessors if (!hasQueuedPredecessors() && //cas operation state compareAndSetState(0, acquires)) { //After getting the lock, set the thread variable to preempt the lock, and the process is over setExclusiveOwnerThread(current); return true; } } //Reentry lock else if (current == getExclusiveOwnerThread()) { //Record the number of re-entry locks int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); //Status is 0 or > 0 setState(nextc); return true; } return false; }
- Next, analyze the situation of fair lock hasQueuedPredecessors. Returning true indicates that the current thread is going to queue. There is no logic for non fair locks
public final boolean hasQueuedPredecessors() { //Tail node Node t = tail; //Head node Node h = head; //h later nodes Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); } //h!=t returns true, indicating that at least two different nodes exist in the queue. //(s = h.next) == null returns false, indicating that there is a successor node. //s.thread != Thread.currentThread() returns true, which means that the thread of the subsequent node is not the current thread, so the current thread naturally has to queue honestly.
- As can be seen above, if the above conditions are met and the CAS operation state is successful, the thread will get the lock. Next, continue to analyze how to deal with the thread that does not get the lock?
//Back to AbstractQueuedSynchronizer#acquire //Acquirequeueueueued (addwaiter (node. Exclusive, Arg)) / / node.exclusive exclusive exclusive state //Look at addWaiter first private Node addWaiter(Node mode) { //Encapsulate the current thread as a Node Node node = new Node(Thread.currentThread(), mode); Node pred = tail; //If the current AQS queue is not empty, the cas operation is used to add the node to the tail node if (pred != null) { //Tail interpolation node.prev = pred; //Set the new node as the tail node if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //cas failed to add to AQS queue through enq enq(node); return node; } //Add to AQS queue through continuous spin and CAS operation private Node enq(final Node node) { for (;;) { //When the thread is unsafe from the tail node to the head node, the next node of the head node keeps changing Node t = tail; //The current tail node is empty. Create a head and tail node if (t == null) { // Must initialize //Because the lock is not obtained at this time, it is not safe under multithreading, so CAS operation must be used if (compareAndSetHead(new Node())) tail = head; } else { //The current thread node is added after the tail node node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
- Next, let's see how to handle the threads added to the AQS queue
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; //spin for (;;) { //Take out the pre node of the current node (the previous node) final Node p = node.predecessor(); //If the front node is head, it means that the current node is in the first place in the waiting queue and directly obtains the lock if (p == head && tryAcquire(arg)) { //Lock acquired successfully //This indicates that the head node has completed execution and is the current node that releases the lock and wakes up //Set as the head node and return, and then execute the locked code setHead(node); //Remove the front node from the queue so that there is no point to it, which helps GC recycle quickly p.next = null; // help GC failed = false; return interrupted; //Returns the interrupt status. false indicates that there is no interrupt } //shouldParkAfterFailedAcquire has three situations //1. If the status is already signal (waiting to be woken up), return true //2 WS > 0 closed state (thread interrupted) remove closed thread and return false //3. When the update status is SIGNAL, false is returned //If false is returned, the next time you continue to spin to implement the operation, you will eventually return true and execute the blocking code if (shouldParkAfterFailedAcquire(p, node) && // LockSupport.park(this); Block the execution from here after the current thread wakes up parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
The lock code is analyzed here. Next, continue to analyze the lock release code.
2.Lock.unlock() release lock
public void unlock() { //Or take the secure lock as an example sync.release(1); } //arg represents the number of times the lock is re entered. The lock needs to be released to 0 before it can be obtained by other threads public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) //1. Modify state to initial state 0 //2.Node s = node.next; LockSupport.unpark(s.thread); Wake up the next thread unparkSuccessor(h); return true; } return false; } protected final boolean tryRelease(int releases) { //The reentry lock needs to be released to 0 before it can be obtained by other threads int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; } //Look again at releasing the lock and waking up the next waiting thread private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) //Modify state to initial state 0 compareAndSetWaitStatus(node, ws, 0); //Gets the next thread waiting to wake up Node s = node.next; //The next node is invalid or the node status is off if (s == null || s.waitStatus > 0) { s = null; //Start scanning from the tail node to find the one closest to the head for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) //Wake up the next valid thread. The next thread spins in acquirequeueueueued LockSupport.unpark(s.thread); }
Only I think spin works well? That is the whole content of this chapter.
Be sure to open the source code yourself and read it again. Don't talk on paper.
Previous: Ordering of thread safety and memory barrier
Next: Use of wait/notify and J.U.C Condition in synchronized thread communication and source code analysis
There is a road to the mountain of books. Diligence is the path. There is no end to learning. It is hard to make a boat