Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.
ReentrantLock: ReentrantLock: ReentrantLock: ReentrantLock: ReentrantLock: ReentrantLock: ReentrantLock: ReentrantLock: ReentrantLock: ReentrantLock: ReentrantLock: ReentrantLock: ReentrantLock Have you ever wondered why simply calling lock() and unlock() can solve thread safety problems?
CAS
As we all know, Java also has a way of thread synchronization, synchronized keyword, which can solve the problem of thread safety. However, because synchronized is realized through the operating system Mutex Lock, the efficiency of synchronized is relatively low. It’s called a heavyweight lock. Fortunately, JDK1.6 officially changed synchronized in a more in-depth way, introducing biased locking, lightweight locking, lock elimination, lock coarsening and other mechanisms, which greatly improved the performance of synchronized.
Before JDK1.6, in order to solve the problem of low synchronized performance, Doug Lea developed Java and distributed a large number of components, which made a great contribution to the development of Java. It implemented a variety of clever mechanisms to ensure thread safety without locking, such as:
public class LockDemo {
private AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock(a) {
// Get the current thread object
Thread thread = Thread.currentThread();
// Spin wait
while(! atomicReference.compareAndSet(null, thread)) {
}
}
public void unlock(a) {
// Get the current thread object
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
}
static int count = 0;
public static void main(String[] args) throws InterruptedException {
LockDemo lockDemo = new LockDemo();
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 50; i++) {
Thread thread = new Thread(() -> {
lockDemo.lock();
for (int j = 0; j < 1000; j++) {
count++;
}
lockDemo.unlock();
});
thread.start();
threadList.add(thread);
}
// Wait for the thread to complete
for(Thread thread : threadList) { thread.join(); } System.out.println(count); }}Copy the code
The program uses CAS mechanism to implement a spin lock, to ensure thread safety, Java and a large number of packages using CAS.
AQS
Moving on to the topic of this article, AQS, let’s take a ReentrantLock program as an example:
public class LockDemo {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
List<Thread> threadList = new ArrayList<>();
Lock lock = new ReentrantLock();
for (int i = 0; i < 50; ++i) {
Thread thread = new Thread(() -> {
lock.lock();
try {
for (int j = 0; j < 1000; j++) { count++; }}finally{ lock.unlock(); }}); thread.start(); threadList.add(thread); }for(Thread thread : threadList) { thread.join(); } System.out.println(count); }}Copy the code
When we create a ReentrantLock object:
public ReentrantLock(a) {
sync = new NonfairSync();
}
Copy the code
The NonfairSync object is created and assigned to sync. What is sync?
private final Sync sync;
Copy the code
It is a variable of type Sync, which is an inner class of ReentrantLock:
abstract static class Sync extends AbstractQueuedSynchronizer {... }Copy the code
Sync inherited from AbstractQueuedSynchronizer, it is our key to introduce AQS, meaning abstract queue synchronizer. So what we’re actually creating is an abstract queue synchronizer.
A thread executes the lock() method.
public void lock(a) {
sync.lock();
}
Copy the code
Sync’s lock() method is called:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */
final void lock(a) {
if (compareAndSetState(0.1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
returnnonfairTryAcquire(acquires); }}Copy the code
This is an inner class of ReentrantLock that inherits from Sync, so execute its Lock method, in which CAS is used, first execute the compareAndSetState() method, since neither NonfairSync nor Sync classes override this method, So it performs the AbstractQueuedSynchronizer class:
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
Copy the code
The meaning of this method is to predict AbstractQueuedSynchronizer class attributes in the state value is 0, if it is 0, it is updated to 1, for the first thread, it must be established, so the modification is successful, return true value, and continue to perform the if block in the method:
setExclusiveOwnerThread(Thread.currentThread());
Copy the code
It is still performed AbstractQueuedSynchronizer methods in:
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
Copy the code
It sets the current owner of the exclusive mode synchronization, i.e., whichever thread has state set to 1, which means it owns it, makes that thread the owner of the resource, where the lock() method ends.
At this point, if a second thread wants to grab the resource, it executes the lock() method, also going to this method:
final void lock(a) {
if (compareAndSetState(0.1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
Copy the code
At this point, the thread expects state to be 0, but state has already been changed to 1 by the first thread. The second thread must fail to update and return false, so acquire() :
public final void acquire(int arg) {
if(! tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }Copy the code
The core content of AQS is involved here, because the current state of the value is 1, so the current thread that exclusive resources by other threads, the thread needs to wait for, at this time in AQS maintains a queue, it is made of two-way linked list implementation, when a thread needs to wait for resources, just as a node in the queue.
Now that the first thread has finished executing, call unlock() to release the lock:
public void unlock(a) {
sync.release(1);
}
Copy the code
It calls AQS’s release() method:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if(h ! =null&& h.waitStatus ! =0)
unparkSuccessor(h);
return true;
}
return false;
}
Copy the code
AQS determines if there is a node in the queue, and if there is, it obtains a node from the queue and wakes it up.
The above is the whole process of ReentrantLock lock unlock, from the source code is not difficult to find, the bottom of ReentrantLock is all implemented by AQS.
Finally, I conclude with a graph: