AbstractQueuedSynchronizer提供了一个同步器的框架,可以基于它来实现各种锁和同步器。它是ReentrantLock的基础实现,ReentrantLock中的锁和同步器都是基于AbstractQueuedSynchronizer来实现的。在实现锁和同步器时,需要继承AbstractQueuedSynchronizer类,并实现它的一些抽象方法。
AbstractQueuedSynchronizer的主要作用如下:
2.1 提供了一个同步器的框架,可以基于它来实现各种锁和同步器。
2.2 通过state变量来记录锁的状态,包括锁的持有者、锁的状态等。
2.3 提供了基于FIFO队列的线程阻塞机制,实现了线程的等待和唤醒。
FairSync是Sync的子类,实现公平锁。公平锁指的是锁的获取按照请求的顺序进行,先请求的先获得锁,避免线程饥饿现象。FairSync实现了公平锁的逻辑,它在锁的获取过程中,会先检查队列中是否有等待的线程,如果有,则先释放锁,让等待的线程获取锁。
FairSync的主要作用是实现公平锁的逻辑。
ReentrantLock是Java中的一种锁机制,支持重入锁。重入锁指的是同一个线程可以多次获得同一把锁,而不会出现死锁等问题。
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 执行需要加锁的代码块
} finally {
lock.unlock(); // 记得释放锁
}
if (lock.tryLock()) {
try {
// 执行需要加锁的代码块
} finally {
lock.unlock(); // 记得释放锁
}
} else {
// 获取锁失败,处理逻辑
}
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
// 执行需要加锁的代码块
} finally {
lock.unlock(); // 记得释放锁
}
} else {
// 获取锁失败,处理逻辑
}
ReentrantLock是Java中的一种锁机制,相比于synchronized关键字,它有如下的优缺点:
同一个线程可以多次获得同一把锁,避免了死锁等问题
支持公平锁和非公平锁,可以根据情况选择
支持线程中断,即使线程已经获得了锁,也可以响应中断
支持多种锁操作,如tryLock()、tryLock(long time, TimeUnit unit)等,可以根据情况选择
相比于synchronized关键字,ReentrantLock的性能更好,特别是在高并发的情况下。
相比于synchronized关键字使用简单,ReentrantLock的使用更加复杂,需要手动获取锁和释放锁
如果使用不当,仍然可能出现死锁等问题
相比于synchronized关键字,ReentrantLock不支持隐式锁,需要手动获取锁和释放锁。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
当我们在用ReentrantLock独占锁的时候,如果不指定公平非公平,那默认是非公平的
public void lock() {
sync.lock();
}
这里以非公平为例:
当我们在外部调用lock()方法的时候会进入ReentrantLock内部的加锁lock方法,其中sync是ReentrantLock的内部类,sync直接继承了AbstractQueuedSynchronizer(AQS)。
代码调用lock会来到ReentrantLock 内部类NonfairSync的lock()方法,如下:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
lock 内部,一上来直接CAS操作AQS内部的一个state变量,尝试从 0 修改为1
如果修改成功,则获取锁。
否则 else 调用 AQS 内部的 acquire (模板)方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1.acquire方法内部会调用tryAcquire方法,点击去会来到AQS内部的tryAcquire,这个方法没有在内部实现,是为了让子类(ReentrantLock )去实现,模板方法模式的体现。
2. 这里又会回到ReentrantLock 的内部类NonfairSync的tryAcquire方法:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
接着调用nonfairTryAcquire方法:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
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;
}
该方法大致逻辑:
1. 获取AQS内部的state变量,如果等于0说明没有线程获取锁,继续CAS尝试获取锁,如果成功返回true。(如果是公平锁,这里需要判断队列里是否有阻塞的线程等着,入队前)
2. 如果不等于0,则判断当前线程current是否等于已经获取锁的线程,如果等于则+1,返回true,这里就是重入锁的逻辑。
3. 如果CAS即没有获取锁,同时也不是重入锁,则返回false。
到这里,返回false,会回到最初调用tryAcquire(AQS)方法的地方:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//如果acquireQueued返回true,说明阻塞线程在等待的过程中被中断,因为线程被唤醒没有获取到锁需要继续阻塞,导致parkAndCheckInterrupt内部调用了Thread.interrupted()方法给中断标志清除了,所以这里需要把中断标志重新恢复过来
selfInterrupt();
}
这里是竞争共享资源失败需要入队等待唤醒的逻辑,首先调用addWaiter(Node.EXCLUSIVE)方法将当前线程封装成一个Node结点。
addWaiter方法的代码如下:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
//将pred指针指向队列尾结点
if (pred != null) {
//如果不等于null,说明队列有等待线程
node.prev = pred;
//修正当前node结点的前驱指针,指向队列的尾结点
//CAS尝试将当前结点置为尾结点
if (compareAndSetTail(pred, node)) {
//修正队列尾结点的后继指针,指向当前结点
pred.next = node;
//当前结点入队成功则返回
return node;
}
}
//代码走到这里说明,队列里没有阻塞的线程
enq(node);
return node;
}
addWaiter方法内部涉及到enq(创建队列,队里的初始化操作)
private Node enq(final Node node) {
//这里是CAS+自旋机制,保证当前结点一定要入队成功,因为存在并发的情况
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
//这里是给队列进行初始化,构造头结点
if (compareAndSetHead(new Node()))
tail = head;
} else {
//这里是重点,线程入队步骤,如果这几步调换是会存在安全问题的,这3步顺序不能改变
node.prev = t;
//1.将当前node结点的前驱指针,指向队列的尾结点
if (compareAndSetTail(t, node)) {
//2. CAS尝试将当前结点置为尾结点
//3. 修正队列尾结点的后继指针,指向当前结点
t.next = node;
return t;
}
}
}
}
enq方法:for循环+CAS机制,保证结点一定要入队成功。
注意入队三步骤:(尾插法)
1.将当前node结点的前驱指针,指向队列的尾结点
2. CAS尝试将当前结点置为尾结点
3. 修正队列尾结点的后继指针,指向当前结点
addWaiter方法结束回到acquireQueued方法处:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取当前结点的前驱结点
final Node p = node.predecessor();
//如果前驱结点是头结点 出于性能考虑,会再次尝试获取锁
if (p == head && tryAcquire(arg)) {
//抢到锁了,将当前结点设置成头结点
setHead(node);
//释放原来的头结点,断点指针,让gc来进行回收
p.next = null; // help GC
failed = false;
return interrupted;
}
//到这里说明 当前结点的前驱结点不是头结点 或者 前驱结点是头结点但是没有拿到锁
//说明前驱结点是SIGNAL状态,那就调用park阻塞
//判断是不是需要park,并且梳理一下指针的指向关系
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//记录阻塞线程在等待获取锁的过程中有没有被中断
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//设置头结点
private void setHead(Node node) {
//这里因为只有一个结点会进来,所以不需要CAS
head = node;
node.thread = null;
node.prev = null;
}
acquireQueued方法 :大致逻辑是阻塞线程入队后获取前驱结点
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) {
//前驱结点状态是 CANCELLED 状态需要被取消
//意思就是没有必要在 CANCELLED 结点后面等着了,前驱结点不会进行通知当前结点,那就往前面找,直到不大于0,也就是不是取消 CANCELLED 状态的
/*
* 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;
}
最后来到parkAndCheckInterrupt方法:
private final boolean parkAndCheckInterrupt() {
//将当前线程进行阻塞,让出CPU执行权
LockSupport.park(this);
//返回中断标志位,并重置为false
//如果有其他线程调用interrupt方法将其唤醒,当前线程如果获取不到锁,仍然需要阻塞,所以这里需要通过interrupted方法清除中断标志,如果不清除线程会一直在这里自旋,耗费CPU资源
return Thread.interrupted();
}
调用LockSupport.park(this),将当前线程挂起,进行阻塞。
当我们在外部调用lock.unlock()方法的时候,代码如下:
public void unlock() {
sync.release(1);
}
可以看到方法里会通过sync内部类调用AQS内部的release方法:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
而release方法内部又会调用子类的tryRelease方法(ReentrantLock):
protected final boolean tryRelease(int releases) {
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;
}
protected final void setState(int newState) {
state = newState;
}
tryRelease方法大致的执行过程:
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
该方法大致过程如下: