Java并发------AbstractQueuedSynchronizer之共享模式(三)

一、CountDownLatch

CountDownLatch 是 AQS 共享模式使用的锁,共享的概念就是 N 条线程需要同一把锁,当 N 条线程全部执行完成后,才会继续向下执行。

这是一个简单应用:

public static void main(String[] args) {
    CountDownLatch doneSignal = new CountDownLatch(2);
    new Thread(() -> {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("加入线程:" + Thread.currentThread().getName());
        doneSignal.countDown();
    }).start();
    new Thread(() -> {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("加入线程:" + Thread.currentThread().getName());
        doneSignal.countDown();
    }).start();
    new Thread(() -> {
        try {
            System.out.println(Thread.currentThread().getName() + "加入等待队列,准备执行");
            doneSignal.await();
            System.out.println(Thread.currentThread().getName() + "开始执行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
    new Thread(() -> {
        try {
            System.out.println(Thread.currentThread().getName() + "加入等待队列,准备执行");
            doneSignal.await();
            System.out.println(Thread.currentThread().getName() + "开始执行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

实现原理:

1、构造函数:

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}
private static final class Sync extends AbstractQueuedSynchronizer {
    Sync(int count) {
        setState(count);
    }
}

这个 Sync 是 CountDownLatch 自己的静态内部类,与 ReentrantLock 的 Sync 是不一样的。Sync 构造函数中的 count,代表这个共享模式下线程的条数,每次添加一条线程 count 都会减 1,当 count 达到 0 时,才会放过所有线程一起执行。CountDownLatch 使用起来其实只有两个方法:countDown 和 await,下面分别看:

2、countDown

public void countDown() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
protected boolean tryReleaseShared(int releases) {
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

每次都把 state 减 1,直到 state 为 0 时,才会调用 doReleaseShared 方法,这个方法我们和后面的一起分析。

3、await:

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    //同样需要判断一下线程的中断状态
    if (Thread.interrupted())
        throw new InterruptedException();
    //只会返回1或者-1,1代表可以执行了,-1还没达到容量
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}
//这个方法其实不需要传参。。。
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

tryAcquireShared 这个方法被调用的很多,用来检查 state 是否达到 0。而只有线程调用 countDown 这个方法时,state 才会进行相应的操作。所以只要线程数还没达到容量的要求都会进入 doAcquireSharedInterruptibly,这个方法才有将线程挂起的操作;否则,直接向下执行代码。

注意 await 方法中,没有涉及到减少 state 值的操作,state 值的设置都在 countDown 方法中。

4、doAcquireSharedInterruptibly

private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
    //addWaiter还是将当前线程包装为Node对象,但是标记了共享模式
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        //与之前的方法很相似
        for (;;) {
            final Node p = node.predecessor();
            //检查前驱节点是不是头部,是的话检查一下state达到0了吗
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            //线程挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

这个方法与之前的 acquireQueued 加入等待队列的方法相似,都是判断一下,当前线程对象的前驱节点是否是头部节点,不是的话,直接挂起就可以了。如果是头部节点,并且 state 达到 0 了,调用 setHeadAndPropagate 方法。

private void setHeadAndPropagate(Node node, int propagate) {
    //直接将head设置为当前线程
    Node h = head; // Record old head for check below
    setHead(node);
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        //所有线程都已经在执行了,或者这是共享模式
        if (s == null || s.isShared())
            doReleaseShared();
    }
}
//分析是否是共享模式,这是抽取出来的,便于理解
Node node = new Node(Thread.currentThread(), mode);
Node(Thread thread, Node mode) {     // Used by addWaiter
    this.nextWaiter = mode;
    this.thread = thread;
}
final boolean isShared() {
    return nextWaiter == SHARED;
}

如果当前线程的前驱节点就是头部线程的话,将头部节点设置为当前线程,然后开始释放锁。后面的代码是表达 addWaiter 时,标记了这个节点是共享模式的。

5、doReleaseShared

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        //h == null || h == tail的情况:头部节点为空,头部节点刚刚初始化,头部节点就是等待队列的尾节点,就是所有线程都已经开始执行
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            //头部节点还没开始执行,如果这时已经有节点入队,那么头部节点状态会被设置为-1
            if (ws == Node.SIGNAL) {
                //将头部节点状态置为0,失败的话继续循环
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                //成功的话解除头部节点的后续节点的挂起状态,也就是唤醒等待队列的第一个节点
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                    !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        //如果唤醒后,头部线程还是之前的头部,那么退出循环
        if (h == head)                   // loop if head changed
            break;
    }
}

唤醒就比较简单了,唤醒的是头部节点的后续节点,后续节点还停留在 parkAndCheckInterrupt 中,把它唤醒,再由这条被唤醒去唤醒其他线程。

总结:

共享锁的使用方法就是:

  • 这里有多条任务线程,他们都调用 countDown 方法,这个方法只是用来记录线程数量是否达到 CountDownLatch 初始化给定的值。多条任务线程并不会要求它们一起执行,可以有先后,可以有时间长短不同,但是它们执行完都会回馈给 CountDownLatch。
  • 这里还有多条主线程,它们执行前都调用了 await 方法,await 方法会把所有调用者都阻塞起来,放入到等待队列中,等待所有任务线程执行完成后,再依次放开主线程去执行。
  • 当最后一条任务线程进来时,也就是使 state 到达 0 的线程,会由它来开始唤醒主线程。头节点的线程唤醒后,开始调用 setHeadAndPropagate,唤醒后续节点的线程。每条等待队列中的线程都负责将后续线程唤醒,然后退出循环,开始执行各自主线程内的方法。

二、CyclicBarrier

CyclicBarrier 和 CountDownLatch 很类似,但是 CountDownLatch 只能使用一次,大家可以测试一下,使用 CountDownLatch 时,当 state 已经达到 0,再调用 countDown 会发现,不管再添加多少条任务线,await 方法都会在 state 达到 0 时开始执行。

而 CyclicBarrier 字面意思,可重复使用,可以看下面一个例子:

public static void main(String[] args) {
    CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
    new Thread(new myRunnable(cyclicBarrier)).start();
    new Thread(new myRunnable(cyclicBarrier)).start();
    new Thread(new myRunnable(cyclicBarrier)).start();
    new Thread(new myRunnable(cyclicBarrier)).start();
}
static class myRunnable implements Runnable{
    CyclicBarrier cyclicBarrier;
    myRunnable(CyclicBarrier cyclicBarrier){
        this.cyclicBarrier = cyclicBarrier;
    }
    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + "准备执行");
            cyclicBarrier.await();
            System.out.println(Thread.currentThread().getName() + "开始执行");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

可以看出,每有 2 条线程进入 CyclicBarrier,await 就会开始执行;每次线程数量达到初始化设定的值后,await 开始执行,并会重置这个值,重新开始计算线程数量。

CyclicBarrier 的源码实现和 CountDownLatch 大相径庭,CountDownLatch 基于 AQS 的共享模式的使用,而 CyclicBarrier 基于 Condition 来实现。

三、Semaphore

Semaphore 可以比作一个停车场,有出有进,当停满的时候,只有出去一个才可以放进来一个。Semaphore 就是这种实现思路,也是 AQS 共享锁的体现,每条线程共享一个池子。可以看下面一个例子:

public static void main(String[] args) {
    Semaphore semaphore = new Semaphore(2);
    for (int i = 1; i < 5; i ++) {
        new Thread(new myRunnable(semaphore, i)).start();
    }
}
static class myRunnable implements Runnable{
    Semaphore semaphore;
    int i;
    myRunnable(Semaphore semaphore, int i){
        this.semaphore = semaphore;
        this.i = i;
    }
    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + "准备执行");
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + "开始执行");
            Thread.sleep(i * 1000);
            semaphore.release();
            System.out.println(Thread.currentThread().getName() + "执行完成");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

当新线程进来时,调用 acquire 方法(开到了停车场),这时不一定会有获取到锁(停车位),需要看 semaphore 池子中已经有多少条线程。如果没有达到设定的值,可以获得锁,并开始执行(停到车位中),执行完成后需要 release 释放锁,并告诉 semaphore 池子为止减少 1,可以唤醒后面等待的线程来拿锁(开走,后面的车可以继续进来停);如果已经达到设定的值(停车位满了),就需要等待其他线程释放锁,并且释放池子中的资源。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值