J.U.C 学习【二】AbstractQueuedSynchronizer独占模式和示例

文章中的源码均来自JDK1.8

前言

     上一篇 Java并发学习【一】AbstractQueuedSynchronizer队列实现和API简介 的文章中介绍了 AQS 的 API 和队列节点Node的源码,接下来两篇分别介绍 AQS 的独占模式和共享模式,先来看看独占模式。

     独占模式:是指当一个线程获取到了锁,其他线程则无法获取到锁而进入阻塞状态,直到拥有锁的线程释放了锁,其他线程才能去争夺锁。

1. 独占模式示例

     上一篇API介绍中自定义同步组件需要继承AQS并重写指定的方法来实现功能,要想实现独占锁,需要重写其 tryAcquire 和 tryRelease ,如下简单的独占锁实现

public class ExclusiveLock {

    private final Sync sync;

    public ExclusiveLock () {
        sync = new Sync();
    }

    static final class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int acquires) {
            // 获取同步状态
            int state = this.getState();
            // 判断当前同步状态是否为0,为0代表可以尝试获取锁
            if (state == 0) {
                // 使用CAS修改同步状态state,成功则将当前线程设置为独占锁的拥有,并返回true
                if (this.compareAndSetState(0, acquires)) {
                    this.setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int acquires) {
            // 如果当前线程不是占用锁的线程则抛出异常
            if (Thread.currentThread() != this.getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            // 设置占用锁的线程为null
            this.setExclusiveOwnerThread(null);
            // 将同步状态state设置为0
            this.setState(0);
            return true;
        }

        @Override
        protected boolean isHeldExclusively() {
            return this.getState() == 1;
        }
    }

    /**
     * 获取锁
     */
    public void lock () {
        sync.acquire(1);
    }

    /**
     * 释放锁
     */
    public void unlock () {
        sync.release(1);

    }
    /**
     * 锁是否被获取了
     * @return
     */
    public boolean isLocked () {
        return sync.isHeldExclusively();
    }
}

    上面代码中使用一个内部类 Sync 继承 AbstractQueuedSynchronizer,继承对应的方法,而外部类中方法的功能是通过Sync来实现的。

2. 源码解析

2.1 独占式获取同步状态

    示例中的lock方法中使用Sync的示例调用acquire方法,那我们先来看看acquire方法

    public final void acquire(int arg) {
	// tryAcquire由子类实现,返回true则获取到同步状态
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // EXCLUSIVE是独占模式
            selfInterrupt(); // 中断当前线程
    }

tryAcquire方法是由子类实现的,所以这里会回调示例中重写的tryAcquire,当该方法返回true的时候说明获取同步状态成功,如果返回false则进入 && 后面的方法,接下来看看 addWaiter 方法的实现

    /**
     * 加入同步队列
     */
    private Node addWaiter(Node mode) {
	Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail; 
        if (pred != null) { // 如果尾节点不为空
            node.prev = pred;
            if (compareAndSetTail(pred, node)) { // CAS 设置为尾节点
                pred.next = node;
                return node;
            }
        }
        enq(node); // 设置队列为空或者设置尾节点失败则使用自旋加入队列
        return node;
    }

addWaiter方法是将创建包含当前线程的Node对象,然后使用CAS方式放入同步队列中去,如果失败则进入enq(node)方法

    /**
     * CAS循环加入同步队列
     */
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // 如果同步队列为空则初始化头节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

enq方法是使用循环加入同步队列,方法中先判断尾节点tail是否为空,如果为空说明同步队列为空,则初始化队列,因为该线程是获取锁失败才进来这方法,说明有线程占有锁,所以创建个空的头节点,之后再将当前线程的节点Node加入队列,加入成功后返回当前线程的节点。

    上面acquire方法中addWaiter执行完后返回的Node对象被当作参数传入acquireQueued方法,下面来看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);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
		// shouldParkAfterFailedAcquire 当尝试获取锁失败后是否进入等待
		// parkAndCheckInterrupt 等待并检查是否中断了
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

在这个方法中,节点会进行自我检查,当检查到自己的前驱节点为头节点时,会去调用子类重写的tryAcquire方法尝试获取锁,获取成功则将当前节点设置为头节点,并删除原来的头节点。如果失败的话会进入下面的判断,先来看看shouldParkAfterFailedAcquire方法

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
	// 表示后继节点处于等待状态返回true
        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) { // 当前节点超时或者中断状态,删除该节点
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 将waitStatus设置为 -1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

获取前驱节点的waitStatus, 当ws = 1(SIGNAL)时,则返回true说明当前节点的线程需要进行阻塞,当ws > 0(CANCELLD)则前驱节点为超时或者中断,删除该节点。当前ws为0或者-3(PROPAGATE)时尝试将前驱节点的waitStatus状态改成1。如果当前的节点需要进行阻塞则进入parkAndCheckInterrupt方法,该方法主要是阻塞当前线程并检查当前线程是否被中断。

    讲到这里独占锁的获取已经结束了,下面画个图来回顾下整个获取独占锁的过程

    

2.2 独占式释放同步状态

示例中的锁的释放是通过Sync调用release方法来实现的,下面来看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方法释放同步状态,如果释放成功则调用uparkSuccessor方法唤醒后继节点

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0); // 设置头节点的waitStatus为0

        /*
         * 获取下个节点
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) { // 下一个节点为null或者超时中断,则从尾节点往前查找可唤醒的最前面的节点
            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); // 唤醒查找到的节点对应的线程
    }

uparkSuccessor的过程是获取头节点的后继节点,如果不为空并且不是超时或者中断状态的情况下则直接唤醒,否则从尾节点往前查找非超时或中断的最前面的节点,查找到则唤醒该节点对应的线程。


参考资料:
《Java并发编程的艺术》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值