JAVA AQS框架独占锁详解

AQS(AbstractQueuedSynchronizer),抽象的队列式的同步器框架,他提供了一个FIFO(first in first out)的队列,以基类的形式对外提供封装好的方法管理同步状态。

  • 在AQS内部维护了一个叫做status的volatile变量,使得此变量具有内存可见性。可以通过以下几个方法操作:
  1. getState
  2. setState
  3. compareAndSetState
  • AQS提供了两种形式的同步器
  1. 独占模式,基于此类的常见同步器如:ReentrantLock。
  2. 共享模式,基于此类的常见同步器如:CountDownLatch。
  • 使用同步器时需要继承AbstractQueuedSynchronizer类,因为AQS只是一个同步器的框架,至于具体如何获取资源/释放资源,需要我们自己定义重写方法。
  1. isHeldExclusively(),查看是否处于占用状态
  2. tryAcquire(int acquires),独占方式,独占式获取同步状态,试着获取,成功返回true,反之为false。
  3. tryRelease(int releases),独占方式,独占式释放同步状态,等待中的其他线程此时将有机会获取到同步状态。
  4. tryReleaseShared(int arg),共享方式,共享式获取同步状态,返回值大于等于0,代表获取成功;反之获取失败。
  5. tryAcquireShared(int arg),共享方式,共享式释放同步状态,成功为true,失败为false。
  • 这时候可能有人会问,既然必须自己实现为什么不设置为abstract方法? 原因是AQS框架不只提供了一种同步方式,还有上面提到的共享方式,如果设置为abstract方法,那么在使用中子类就必须将两种方式的资源获取/释放方法同时实现,这是不合理的,所以作者并未设置abstract。

基于AQS的独占锁实现代码

import java.util.concurrent.locks.ReentrantLock;

public class TestReentrantLock {

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        new Thread(() -> {
            try {
                lock.lock();
                System.out.println(1);
                Thread.sleep(1000 * 60 * 3);
                System.out.println(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }).start();
        new Thread(() -> {
            try {
                System.out.println(3);
                lock.lock();
                System.out.println(4);
            } finally {
                lock.unlock();
            }
        }).start();
    }
}
  • ReentrantLock的构造方法
// ReentrantLock的默认构造方法,初始化了非公平的同步器,即ReentrantLock默认是非公平实现
public ReentrantLock() {
    sync = new NonfairSync();
}
// 可以指定ReentrantLock实现方式,FairSync公平的同步器,NonfairSync非公平的同步器
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
  • 对比公平锁和非公平锁:公平性锁保证了锁的获取按照FIFO原则,而代价是线程的切换。非公平性锁虽然可能造成线程“饥饿”,但减少了线程切换,具有更大的吞吐量。
  • 上面提到的线程切换的开销,就是非公平锁效率高于公平锁的原因,因为非公平锁减少了线程挂起的几率,比如unlock后队列中的下一个线程还没有被唤醒,或者刚醒但还没有抢占到锁,这时一个新的线程插队先一步获取了锁,后来的线程正好在线程切换的空档中继续执行了,所以非公平会效率更高。
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;
    
    // 如果设置同步状态status成功,则设置拥有锁的线程为当前线程,获取锁成功
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            //执行AQS入队操作,具体入队操作见下文
            acquire(1);
    }
    // 实现AQS tryAcquire 方法,提供非公平的获取锁方式,AQS只提供了抽象的tryAcquire
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // status==0 && 设置同步状态成功,获取锁成功
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 锁重入
    // 如果status != 0 && 当前获取锁的线程与正在执行的线程是同一个,也可以获取锁
    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;
}
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }
    // 实现AQS tryAcquire 方法,提供公平的获取锁方式, AQS只提供了抽象的tryAcquire
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 公平与非公平实现的不同处,判断当前node是否在队列中,hasQueuedPredecessors()
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

// 查询是否有任何线程等待获取的时间长于当前线程
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

AQS框架实现代码

// tryAcquire 是FairSync和NonfairSync重写的
public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
	selfInterrupt();
}
  1. 首先,调用使用者重写的tryAcquire方法,如果成功,后面的入队逻辑不再执行;若失败,进入步骤2
  2. 此时,获取同步状态失败,构造独占式同步结点,通过addWatiter将此结点添加到同步队列的尾部
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;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
  1. 因为设置同步状态失败了,所以创建节点并加入到AQS队列尾部
  2. 代码中使用compareAndSetTail()方法,采用CAS方式保证了线程的安全性,因为同一时间可能有多个线程正在尝试加入到队列尾部
  3. 如果队列没有初始化则执行enq
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
  1. enq()同样使用CAS方式compareAndSetTail(),注意enq方法采用了自旋方式是一个死循环,保证了node一定会被设置成功,这是一种乐观并发策略
  2. 队列为空初始化队,并将node放到tail之后,注意这里返回的tail,并不是最后一个node
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);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
  1. 入队成功后,自旋检查node是不是老二节点,如果是则可以尝试去获取锁了,如果tryAcquire成功,原来的头节点会被踢出队列(p.next = null,被GC回收出队了),设置node为了head节点
  2. 如果当前node还不是老二节点,那么就要去检查自己前节点的状态是否正常,确保前节点可以正确执行,执行后可以唤醒自己,如果有存在问题的node,那么通过shouldParkAfterFailedAcquire调整队列中存在问题的node,调整后一切准备就绪后,执行parkAndCheckInterrupt()阻塞当前执行的线程等待唤醒
  3. cancelAcquire()只有出现异常时才会执行
  4. 注意此处同样采用自旋方式,即使线程被唤醒也会从上述第一步开始重新执行,直到自己tryAcquire成功便成AQS的头节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * -1 SIGNAL 表示后继线程需要被唤醒
             * 意味着当前结点可以被安全地park
             */
            return true;
        if (ws > 0) {
            /*
             * 1 CANCELLED 表示线程已被取消(等待超时或者被中断),
             * 只有CANCEL状态ws才大于0。若前驱结点处于CANCEL状态,也就是此结点线程已经无效,从后往前遍历,找到一个非CANCEL状态的结点,将自己设置为它的后继结点
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * -2 CONDITION 表示结点线程等待在condition上,当被signal后,会从等待队列转移到同步到队列中
             * -3 PROPAGATE 表示下一次共享式同步状态会被无条件地传播下去
             * 若前驱结点为其他状态,将其设置为SIGNAL状态
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
  1. node默认waitStatus=0
  2. shouldParkAfterFailedAcquire是在死循环中调用,第一次进入waitStatus=0会执行else将waitStatus CAS赋值为 Node.SIGNAL状态,会一直遍历到ws == Node.SIGNAL为止
private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
  1. 队列中所有节点waitStatus都调整完毕后,阻塞当前线程,当锁被释放后被唤醒的线程从 LockSupport.park(this); 继续执行下一次循环,尝试获取锁 执行 tryAcquire,步骤参考前几步
// 出现异常当前node应被提出队列
private void cancelAcquire(Node node) {
        // 忽略不存在的节点
        if (node == null)
            return;

        node.thread = null;

        Node pred = node.prev;
        // 注意我们之前提到过waitStatus > 0只有一种状态,即CANCELLED状态,表示线程已被取消(等待超时或者被中断)
        // 注意这里是while循环
        while (pred.waitStatus > 0){
            //拆分成几步方便理解
            //pred = node.prev.prev;
            //node.prev = pred;
            //意思是跳过node之前所有的CANCELLED状态节点,找到一个状态正确的节点来做node的前节点
            node.prev = pred = pred.prev;
        }

        // 此时的pred对象肯定是一个状态不为CANCELLED的节点
        // predNext对象就是队列中有状态不正常的第一个节点
        // pred对象就是队列中node所在位置之前第一个正常的节点
        Node predNext = pred.next;

        // 设置CANCELLED状态
        node.waitStatus = Node.CANCELLED;

        // 因为当前node是异常节点,如果node是tail的话,就把pred设置为队列尾部
        if (node == tail && compareAndSetTail(node, pred)) {
            // 将pred的next设置为null,即所有无效节点都出队了
            compareAndSetNext(pred, predNext, null);
        } else {
            // 此处有3个要注意的判断
            // 1、(pred != head)
            // 如果当前pred不是head并且node也不是tail,即node不是老二节点,也不在队列尾部
            // 2、(ws = pred.waitStatus) == Node.SIGNAL ||(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))
            // 如果pred的waitStatus状态是SIGNAL那没问题,就算不是SIGNAL反正只要不是CANCELLED,就把他的状态改为SIGNAL
            // 3、pred.thread != null
            // pred要有对应的线程
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                // 获取node的下一个节点,并将pred的next设置为node的next,意味着在队列中pred至node这个范围内的所有节点都被剔出队列了
                // 这里只是将pred的next设置了,至于前节点当其他线程执行到shouldParkAfterFailedAcquire()时就会处理这个问题了
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                // 如果node的前节点是head,自己是老二了唤醒队列中下一个可用节点,重点是unparkSuccessor的节点遍历会找到一个正常的节点唤醒,代码请看下面的介绍
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }
  1. 执行了cancelAcquire说明当前加入队列的node应当踢出队列,只有程序异常时才会发生这种情况,因为try中代码是自旋的.

  • 释放锁
lock.unlock();

public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

// ReentrantLock 重新的tryRelease
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;
}

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    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);
}
  1. 如果节点状态小于0,则初始化为0
  2. 如果下一个节点是空的,或者状态为CANCELLED(status>0的状态只有一种即CANCELLED),那么就从tail向前找到一个处于阻塞状态状态正常的节点,注意此处是全部遍历而不是从后向前找到一个就退出
  3. 唤醒找到的节点,唤醒后的节点会从LockSupport.park(this);处继续运行

转载于:https://my.oschina.net/kdy1994/blog/3022593

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值