As a tool for concurrently sharing data and ensuring consistency, locks have multiple implementations on the JAVA platform (such as synchronized and ReentrantLock, etc.). These already written locks provide convenience for our development, but the specific nature and type of the locks are rarely mentioned. This series of articles will analyze common lock names and characteristics under JAVA to answer your questions.
1. Spin lock
Spin locks are implemented by allowing the current thread to continuously execute within the loop body. Only when the conditions of the loop are changed by other threads can the critical section be entered. Copy the code as follows:
public class SpinLock {
private AtomicReference<Thread> sign =new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while(!sign .compareAndSet(null, current)){
}
}
public void unlock (){
Thread current = Thread.currentThread();
sign .compareAndSet(current, null);
}
}
Using CAS atomic operations, the lock function sets the owner to the current thread and predicts that the original value is empty. The unlock function sets owner to null, and the predicted value is the current thread.
When a second thread calls the lock operation, because the owner value is not empty, the loop is executed until the first thread calls the unlock function to set the owner to null, and the second thread can enter the critical section.
Since the spin lock only keeps the current thread executing the loop body without changing the thread state, the response speed is faster. But when the number of threads continues to increase, performance drops significantly because each thread needs to be executed and takes up CPU time. If thread competition is not intense and the lock is maintained for a period of time. Suitable for use with spin locks.
Note: This example is an unfair lock. The order of obtaining the lock will not be based on the order of entering the lock.
2. Other types of spin locks
We talked about spin locks above. There are three common lock forms in spin locks: TicketLock, CLHlock and MCSlock.
Ticket lock mainly solves the problem of access sequence. The main problem is on multi-core CPU:
Copy the code code as follows:
package com.alipay.titan.dcc.dal.entity;
import java.util.concurrent.atomic.AtomicInteger;
public class TicketLock {
private AtomicInteger serviceNum = new AtomicInteger();
private AtomicInteger ticketNum = new AtomicInteger();
private static final ThreadLocal<Integer> LOCAL = new ThreadLocal<Integer>();
public void lock() {
int myticket = ticketNum.getAndIncrement();
LOCAL.set(myticket);
while (myticket != serviceNum.get()) {
}
}
public void unlock() {
int myticket = LOCAL.get();
serviceNum.compareAndSet(myticket, myticket + 1);
}
}
A serviceNum service number must be queried every time, which affects performance (it must be read from the main memory and other CPUs must be prevented from modifying it).
CLHLock and MCSLock are two similar types of fair locks, sorted in the form of a linked list.
Copy the code code as follows:
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class CLHLock {
public static class CLHNode {
private volatile boolean isLocked = true;
}
@SuppressWarnings("unused")
private volatile CLHNode tail;
private static final ThreadLocal<CLHNode> LOCAL = new ThreadLocal<CLHNode>();
private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class,
CLHNode.class, "tail");
public void lock() {
CLHNode node = new CLHNode();
LOCAL.set(node);
CLHNode preNode = UPDATER.getAndSet(this, node);
if (preNode != null) {
while (preNode.isLocked) {
}
preNode = null;
LOCAL.set(node);
}
}
public void unlock() {
CLHNode node = LOCAL.get();
if (!UPDATER.compareAndSet(this, node, null)) {
node.isLocked = false;
}
node = null;
}
}
CLHlock continuously queries precursor variables, making it unsuitable for use under NUMA architecture (in this architecture, each thread is distributed in a different physical memory area)
MCSLock loops through the nodes of local variables. There is no problem with CLHlock.
Copy the code code as follows:
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class MCSLock {
public static class MCSNode {
volatile MCSNode next;
volatile boolean isLocked = true;
}
private static final ThreadLocal<MCSNode> NODE = new ThreadLocal<MCSNode>();
@SuppressWarnings("unused")
private volatile MCSNode queue;
private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class,
MCSNode.class, "queue");
public void lock() {
MCSNode currentNode = new MCSNode();
NODE.set(currentNode);
MCSNode preNode = UPDATER.getAndSet(this, currentNode);
if (preNode != null) {
preNode.next = currentNode;
while (currentNode.isLocked) {
}
}
}
public void unlock() {
MCSNode currentNode = NODE.get();
if (currentNode.next == null) {
if (UPDATER.compareAndSet(this, currentNode, null)) {
} else {
while (currentNode.next == null) {
}
}
} else {
currentNode.next.isLocked = false;
currentNode.next = null;
}
}
}
From the code point of view, CLH is simpler than MCS.
The CLH queue is an implicit queue and has no real successor node attributes.
The MCS queue is an explicit queue with real successor node attributes.
The default lock used internally by JUC ReentrantLock is the CLH lock (there are many improvements, such as replacing spin locks with blocking locks, etc.).
(Full text ends)