Basic introduction
Many concurrent classes in JUC inherit AbstractQueuedSynchronizer (AQS), such as CountDownLatch, ReentrantLock, ThreadLocalExecutor, etc.
It mainly realizes the management of synchronization state and queuing and waiting for notifications of blocked threads. Take ReetrantLock as an example, it has the following functions
- acquire lock
- Threads that compete for the lock without success are stored in a collection
- Release the lock, the threads in the collection will be woken up and reproduced to compete for the lock
- Use locks to create Condition objects
- .....
The above writing functions are implemented by AQS, because ReetrantLock can only be acquired by one thread, so it is an exclusive lock, and ReadLock like ReentrantReadWriteLock can be shared by multiple threads, that is to say it is a shared lock. AQS provides some low-level implementations of exclusive locks and shared locks.
Therefore, the content in AQS can be mainly divided into four parts
- CLH queue: Store waiting threads, which are mainly implemented through a doubly linked list. CLH is the initials of the names of the three big brothers of its inventors.
- exclusive lock
- shared lock
- Condition implementation
AQS Code Overview
The AbstractQueuedSynchronizer class contains two inner classes, of which ConditionObject is the main implementation of the Condition function. Generally, the way to create a Condition is Lock.newCondition(), and we can find by looking at the ReentrantLock source code that the actually created Condition is an instance of ConditionObject.
Node is the carrier of the waiting thread, that is, the node on the doubly linked list where the waiting thread is located.
There are a large number of methods in AbstractQueuedSynchronizer, of which similar to tryAcquire and tryAcquireShared are different implementations of "similar methods" in exclusive locks and shared locks.
The following figure shows some member variables in AbstractQueuedSynchronizer, in which head and tail are Node variables used to represent the head and tail nodes of the queue respectively, state represents the synchronization state, stateOffset represents the offset of the state variable relative to the java object, and also It is the offset relative to AbstractQueuedSynchronizer.class (class is also an object, and everything in java is an object), which is mainly used to set values and modify values for corresponding variables by using CAS later. The same is true for headOffset and tailOffset, and waitStatusOffset and nextOffset is an offset relative to Node.class. In addition, there is an important variable exclusiveOwnerThread in the parent class AbstractOwnableSynchronizer of AbstractQueuedSynchronizer, which represents the thread that owns the current lock in exclusive mode.
Node class analysis
static final class Node {
//用于标记共享模式
static final Node SHARED = new Node();
//用于标记独占模式
static final Node EXCLUSIVE = null;
// waitStatus 为这个值的时候 表示线程已经被取消
static final int CANCELLED = 1;
// waitStatus 为这个值的时候 表示后继线程需要取消阻塞
static final int SIGNAL = -1;
// waitStatus 为这个值的时候 表示线程处于Condition下的等待状态
static final int CONDITION = -2;
//waitStatus 为这个值的时候 表示下个acquireShared操作将被允许
static final int PROPAGATE = -3;
/**
* 这个字段可能有以下状态
* SIGNAL: 该节点的后继节点被(或即将)阻塞(通过停放),因此当前节点在
* 释放或取消时必须解除其后继节点的停放。为了避免竞争,获取方法
* 必须首先表明它们需要一个信号,然后重试原子获取,然后在失败时
* 阻塞。
* CANCELLED: 节点因超时或中断而被取消
* CONDITION: 这个节点被用于condition队列,在装个状态下这个节点不会被用于
* 同步队列。
* PROPAGATE: 这个节点是被共享的
* 0: 以上都不是
*
* 这个字段的初始值为0,且是通过cas的方式对他进行安全写操作
*/
volatile int waitStatus;
//前置节点
volatile Node prev;
//继承节点
volatile Node next;
//该节点拥有的线程
volatile Thread thread;
//可能有两种作用
//1.独占模式下的condition条件下的等待节点
//2.用于判断是共享模式
Node nextWaiter;
//是否是共享模式
final boolean isShared() {
return nextWaiter == SHARED;
}
//返回前置节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
Snooping on AQS exclusive locks through ReentrantLock
Below we use several examples to explore the implementation of some methods in AQS and their role in ReentrantLock.
the simplest example
Let's start with a simple lock & unLock instance
Entering through a breakpoint, the specific implementation of lock is in the NonfairSync inner class of ReentrantLock, which is due to the unfair lock we set for the lock object.
Then we will enter the compareAndSetState method in AQS, which mainly judges whether the state is 0 by means of cas. If yes, change it to 1 and return true, if no, return false without modification. If it is 0, it means this The lock is now not held by any thread, and we change its state to 1 to indicate that it is held.
After returning yes, assign the field of the exclusive thread in AQS to the current thread, and then the lock is successful.
Then we enter the unlock method of ReentrantLock. The main implementation of this method is in the release method of AQS.
Then enter the tryRelease method, it will get the current lock state, and then use c to represent the target state to be changed. After verification, set the exclusive thread of the lock to empty and modify its state field state to the target state. Here the lock has been Released occupied.
After tryRelease returns to true, it will judge whether there is a waiting node in AQS, and if it exists, it will wake up (see the source code here later)
Reentrant lock instance
We use the same ReentrantLock for two lock operations. Since the first time is the same as the above simple example process, we only focus on the second lock and unLock
At this time, since it has been locked once, that is, state=1, compareAndSetState(0,1) will not be successfully assigned, so it will enter the acquire method, and then enter the tryAcquire method first.
We will judge the state of the lock again in TryAcquire (because the last lock may be released during this process), and then since the current thread is the exclusive thread of the lock, we can re-enter the lock, and finally the state The value of 2 is changed to represent that the lock is reentrant twice by the current thread.
Since tryAcquire(1) returns true, if !tryAcquire(1) is false, the program will not enter the subsequent execution flow in the acquire method. At this point, it means that the second lock has been completed.
Like the unlock in the simple example, the program will first enter the release method and then enter the tryRelease method, and then because the changed state is 1, the exclusive thread that will not talk about the current lock is set to null (it will be set in the last unlock)
lock contention example
Lock competition involves waiting queues and blocking and waking up of waiting nodes, so the complexity of its series of operations is higher than the above example. Use the following examples to experience the process of multi-threaded contention for locks.
t1 will acquire the lock first. This process is the same as the acquisition of a non-competitive lock. The main difference is that t2 acquires the lock and t1 releases the lock.
In the idea, you can switch the debugging thread in this position
After the t1 thread acquires the lock, we switch to the t2 thread and find that idea has marked us with lock at this time, and the lock has been occupied by t1.
Then it will enter the acquire method. Since t1 has already occupied the lock at this time, state ≠ 0 and the current thread holding the lock is t1 ≠ t2, so tryAcquire returns false, so the program will enter the addWaiter method.
In this method, the t2 thread is first encapsulated into a Node object, and then the tail node is used to determine whether the queue has been initialized. Since there are no elements in the CLH queue at this time, it will enter the enq method to initialize the queue for the first time.
This queue will be initialized in enq, the queue will be initialized, and then the incoming node will be inserted into the end of the queue, where we see the infinite loop of for(;;) (the elegant point can be called spin), then its role is what?
In this entry into enq, the for is actually only performed twice. The first time a head node without actual data is set for the head node, and the second time the incoming node is added to the end of the queue, then the above work we are It can be done in one loop, such as the implementation in the following code block
private Node enq(final Node node) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
}
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
In fact, the spin is to ensure thread safety. When the t2 thread acquires the lock, there may be other threads competing for the lock. For example, there is a thread that happens to be executing at t2 Node t = tail
and compareAndSetHead(new Node())
When the initialization is successful, the queue has set the head node, then compareAndSetHead
will return false and will not enter this branch. At this time, the tail node will be re-acquired and the incoming node node will be inserted into the tail next. , however, the tail may also be changed by other threads at this time, so it is necessary to keep trying to modify the spin until the successful position and the spin ends.
After addWaiter ends, it will enter acquireQueued. This method will mainly compete for land lock and block waiting, and finally judge whether to cancel the acquisition thread according to the failed field. In this case, the state is generally set to Canceled
shouldParkAfterFailedAcquire determines whether the node should block waiting. If the node is in the SIGNAL state, it means that the node's successor node should be blocked, and then the parkAndCheckInterrupt method will be executed to block it, and when it wakes up, it will determine whether the thread is interrupted. .
Under normal circumstances, if the t1 thread is not unlocked, then the t2 thread will always be blocked in the parkAndCheckInterrupt method, and when it wakes up, it will continue to spin and try to acquire the lock.
Then we switch back to the t1 thread, enter the unlock method, call the release method of AQS, and then the operation in tryRelease is the same as the above two instances, so I won't repeat it. The only difference is that the waiting queues of the previous instances are all empty, that is, the head nodes are all null , so it will not wake up the blocking node, because at this time the node where we have the t2 thread is stored in the queue, so the program will enter the unparkSuccessor method. After this method is executed, the t2 thread will be converted from the previous WAIT state to The RUNNING state is awakened!
After t2 is awakened, it will tryAcquire again. After success, it will execute the content of the critical section, and then release the lock normally.
end
The above uses ReentrantLock to introduce the content of AQS exclusive locks. In addition, the implementation of shared locks, the implementation of Condition and the use of AQS in other related JUC classes will be introduced later through ReentrantReadWriteLock.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。