JUC并发工具的使用和原理

Condition

Condition原理

Condition 是一个多线程协调通信的工具类,可以让某些线程一起等待某个条件(condition),只有满足条件时,线程才会被唤醒。

在AQS中存在两个FIFO队列:同步队列 和 等待队列。本篇文章主要是讲condition实现原理(即等待队里),同步队列实现原理看这篇文章:深入剖析AQS。等待队列是由Condition内部实现的,是一个虚拟的FIFO单向队列,在AQS中同步队列、等待队列组成关系如下图:

img

  • (1)AQS中tail 和 head主要构成了一个FIFO双向的同步队列。

  • (2)AQS中condition构成了一个FIFO单向等待队列。condition是AQS内部类,每个Condition对象中保存了firstWaiter和lastWaiter作为队列首节点和尾节点,每个节点使用Node.nextWaiter保存下一个节点的引用,因此等待队列是一个单向队列。

在Object的监视器(monitor)模型上,一个对象拥有一个同步队列和一个等待队列;而并发包中的AQS上拥有一个同步队列和多个等待队列。两者的具体实现原理的有所不同,但在多线程下等待/唤醒 操作的思路有相同之处,Object的监视器模型 和 AQS对同步队列、等待队列对应关系如下图

(1)Object的监视器模型同步、等待队列对应关系图

img

多个线程并发访问某个对象监视器(Monitor对象)的时候,即多线程执行Synchonized处的代码时,monitor处理过程包括:

  • (1)thread进入Synchonized代码时,会执行Monitor.Enter命令来获取monitor对象。如果命令执行成功获取Monitor对象成功,执行失败线程会进入synchronized同步队列中,线程处于BLOCKED,直到monitor对象被释放。

  • (2)thread执行完Synchonized同步代码块后,会执行Monitor.exit命令来释放monitor对象,并通知同步队列会获取monitor对象。

  • (3)如果线程执行object.wait(),线程会进入synchronized等待队列进行WAITING,直到其他线程线程执行notify()或notifyAll()方法,将等待队列中的一个或多个等待线程从等待队列中移到同步队列中,被移动的线程状态由WAITING变为BLOCKED。

(2)AQS中同步、等待队列对应关系图

当多线程并发访问AQS的lock()、await()、single()方法时,同步队列和等待队列变化处理过程包括:

  • (1)多个形成执行lock()方法时,线程会竞争获取同步锁state,获取成功的线程占有锁state、获取失败的线程会封装成node加入到AQS的同步队列中,等待锁state的释放。

  • (2)等获取了state锁的线程(同步队列中head节点)执行await()方法时,condition会将当前线程封装成一个新的node添加到condition等待队列的尾部,同时阻塞(waiting),直到被唤醒。

  • (3)等获取了state锁的线程(同步队列中head节点)single()方法时,condition会将等待队列首节点移动到同步队列的尾部,直到获取同步锁state才被唤醒。

Condition 的基本使用

public class ConditionDemoWait implements Runnable {
    private Lock lock;
    private Condition condition;
​
    public ConditionDemoWait(Lock lock,
                             Condition condition) {
        this.lock = lock;
        this.condition = condition;
    }
​
    @Override
    public void run() {
        System.out.println("begin - ConditionDemoWait");
        try {
            lock.lock();
            condition.await();
            System.out.println("end - ConditionDemoWait");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
​
}
​
​
public class ConditionDemoSignal implements
        Runnable {
    private Lock lock;
    private Condition condition;
​
    public ConditionDemoSignal(Lock lock,
                               Condition condition) {
        this.lock = lock;
        this.condition = condition;
    }
​
    @Override
    public void run() {
        System.out.println("begin - ConditionDemoSignal");
        try {
            lock.lock();
            condition.signal();
            System.out.println("end - ConditionDemoSignal");
        } finally {
            lock.unlock();
        }
    }
​
}

通过这个案例简单实现了 wait 和 notify 的功能,当调用await 方法后,当前线程会释放锁并等待,而其他线程调用condition 对象的 signal 或者 signalall 方法通知并被阻塞的线程,然后自己执行 unlock 释放锁,被唤醒的线程获得之前的锁继续执行,最后释放锁。所以,condition 中两个最重要的方法,一个是 await,一个是 signal 方法

await:把当前线程阻塞挂起

signal:唤醒阻塞的线程

Condition 实现源码分析

1. 等待的实现

当线程调用Condition.await()方法时,将会把前线程封装成node节点,并将节点加入等待队列的尾部,然后释放同步state状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。当前线程加入Condition的等待队列逻辑如下图:

  • 能够调用Condition.await()方法的节点是获取了同步state锁的node,即同步队列中的head节点;调用Condition的await()方法(或者以await开头的方法)会使当前线程进入等待队列并释放锁、唤醒同步队列中的后继节点,最后线程状态变为等待状态。

  • Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。

  • 调用Condition.await()节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了state锁的线程,也就是说该过程是由锁来保证线程安全的。

await()方法源码

整个await()的执行的过程可以总结如下几步:

  • 将当前线程封装成node加入Condition等待队列尾部。

  • 释放state锁:不管重入几次,都把state释放为0,同时唤醒同步队列的后继节点。

  • 自旋:直到node节点在等待队列上的节点移动到了同步队列(通过其他线程调用signal())或被中断。

  • 阻塞当前节点,直到node获取到了锁,也就是node在同步队列上的节点排队排到了队首。

public final void await() throws InterruptedException {
            if (Thread.interrupted())////如果当前线程中断,抛出异常
                throw new InterruptedException();
    //1.将当前线程封装成一个node并加入到等待队列队尾(这里如果lastWaiter是CANCELLED取消状态,那么会把它踢出Condition队列)。
            Node node = addConditionWaiter();
         //2.释放当前线程的独占锁,不管重入几次,都把state释放为0
            int savedState = fullyRelease(node);
            int interruptMode = 0;
    //3.判断当前节点没有在同步队列上,没有在同步队列上(即还没有被signal),则将当前线程阻塞
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                //4.判断标记两种中断:是在被signal前中断还是在被signal后中断,分别标记上THROW_IE和REINTERRUPT。
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
         //5.这个时候线程已经被signal()或者signalAll()操作给唤醒了,退出了3中的while循环自旋等待,尝试再次获取锁
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
         // clean up if cancelled 清除等待队列中等待状态不为CONDITION的节点
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
         //6.在第4步中标记的中断状态,如果是在被signal前中断还是在被signal后中断,如果是被signal前就被中断则抛出 InterruptedException,否则执行 Thread.currentThread().interrupt();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

addConditionWaiter()方法源码

//addConditionWaiter()方法主要是将线程封装成节点,添加到等待队列尾部
private Node addConditionWaiter() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node t = lastWaiter;
    //Condition里面的节点状态不是等待状态CONDITION时,会清除节点
            // If lastWaiter is cancelled, clean out.
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
//将当前线程分装成一个node,加到等待多了尾部
            Node node = new Node(Node.CONDITION);
​
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

fullyRelease()源码

fullyRelease()方法中会调用release()释放掉state锁,不管重入几次,都把state释放为0,同时唤醒同步队列的后继节点。

final int fullyRelease(Node node) {
        try {
            //获取持有state锁的次数
            int savedState = getState();
            //把state释放为0
            if (release(savedState))
                return savedState;
            throw new IllegalMonitorStateException();
        } catch (Throwable t) {
            //释放锁失败,再将node设置为CANCELLED状态
            node.waitStatus = Node.CANCELLED;
            throw t;
        }
    }

isOnSyncQueue()源码

  final boolean isOnSyncQueue(Node node) {
         //如果当前节点状态是CONDITION或node.prev是null,则证明当前节点在等待队列上而不是同步队列上。用node.prev来判断,是因为一个节点如果要加入同步队列,在加入前就会设置好prev字段。
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        //如果node.next不为null,则一定在同步队列上,因为node.next是在节点加入同步队列后设置的
        if (node.next != null) // If has successor, it must be on queue
            return true;
        //从等待队列的尾部遍历判断node是否在等待队列
        return findNodeFromTail(node);
    }

reportInterruptAfterWait()方法源码

reportInterruptAfterWait()方法会根据中断状态来判断是抛出异常,还是执行中断。即判断线程是在被signal前中断,还是在被signal后中断;如果是被signal前就被中断则抛出 InterruptedException,否则执行 Thread.currentThread().interrupt()。

private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
            if (interruptMode == THROW_IE)
                throw new InterruptedException();
            else if (interruptMode == REINTERRUPT)
                selfInterrupt();
        }

到此condition的wait()方法分析就完了,可以看出,await()的操作过程和Object.wait()方法是一样,只不过await()采用了Condition等待队列的方式实现了Object.wait()的功能。

2.通知的实现

调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将等待队列中节点移到同步队列中。Condition的signal()方法将节点从等待队列移动到同步队列逻辑如下图:

整个signal()的过程可以总结如下:

  • (1)执行signal()唤醒线程时,先判断当前线程是否是同步锁state持有线程,所以能够调用signal()方法的线程一定持有了同步锁state。

  • (2)自旋唤醒等待队列的firstWaiter(首节点),在唤醒firstWaiter节点之前,会将等待队列首节点移到同步队列中。

public final void signal() {
       //判断当前线程是否是state锁持有线程
       if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
       Node first = firstWaiter;
       //等待队列首节点不为null时,唤醒首节点
       if (first != null)
            doSignal(first);
}
​
//自旋唤醒首节点
private void doSignal(Node first) {
        do {
            //移动头节点指针firstWaiter
            if ( (firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            //从等待队列中移除首节点
            first.nextWaiter = null;
                 //transferForSignal方法尝试唤醒当前节点,如果唤醒失败,则继续尝试唤醒
         } while (!transferForSignal(first) &&
                (first = firstWaiter) != null);
}
​
//尝试唤醒当前节点,并将当前节点移动到同步队列中
final boolean transferForSignal(Node node) {
        //如果当前节点状态为CONDITION,则将状态改为0准备加入同步队列;如果当前状态不为CONDITION,说明该节点等待已被中断
        if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
            return false;
     //将node添加到同步队列中,返回的p是node节点在同步队列中的先驱节点
        Node p = enq(node);
        int ws = p.waitStatus;
         //如果先驱节点的状态为CANCELLED(大于0) 或设置先驱节点的状态为SIGNAL失败,那么就立即唤醒当前节点对应的线程,线程被唤醒后会执行await()方法中的acquireQueued()方法,该方法会重新尝试将节点的先驱状态设为SIGNAL并再次park线程;如果当前设置前驱节点状态为SIGNAL成功,那么就不需要马上唤醒线程了,当它的前驱节点成为同步队列的首节点且释放同步状态后,会自动唤醒它。
        if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

总结:

  • (1)Condition等待通知的本质就是等待队列 和 同步队列的交互的过程,跟object的wait()/notify()机制一样;Condition是基于同步锁state实现的,而objec是基于monitor模式实现的。

  • (2)一个lock(AQS)可以有多个Condition,即多个等待队列,只有一个同步队列。

  • (3)Condition.await()方法执行时,会将同步队列里的head锁释放掉,把线程封装成新node添加到等待队列中;Condition.signal()方法执行时,会把等待队列中的首节点移到同步队列中去,直到锁state被获取才被唤醒。

CountDownLatch

countdownlatch 是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完毕再执行。从命名可以解读到 countdown 是倒数的意思,类似于我们倒计时的概念。countdownlatch 提供了两个方法,一个是 countDown,一个是 await,countdownlatch 初始化的时候需要传入一个整数,在这个整数倒数到 0 之前,调用了 await 方法的程序都必须要等待,然后通过 countDown 来倒数。

使用案例

在应急项目中的首页查询接口中用到了countDownLatch,目的是阻塞主线程防止子线程还未查询完成就直接返回不完整的结果给前端

CountDownLatch endGate = new CountDownLatch(9);
LoginUserDetails loginUserDetails = UserUtil.getUser();
​
try {
    //1.化工危化
    new Thread(() -> {
        listVos.add(safetyRiskSourceService.getNaturalDataAuto(loginUserDetails,appendSufix(new String [] {"905265f501ab74ed2a968436d08ecde0"},loginUserDetails.getDistrict()),"化工危化企业(个)",1,null));
        endGate.countDown();
    }).start();
​
    //2.规上企业
    new Thread(() -> {
        listVos.add(safetyRiskSourceService.getNaturalDataAuto(loginUserDetails,appendSufix(new String [] {"6850908220874f05ca5cfdf87ceda4dd"},loginUserDetails.getDistrict()),"规上企业(个)",2,null));
        endGate.countDown();
    }).start();
​
    //3.非煤矿山
    new Thread(() -> {
        listVos.add(safetyRiskSourceService.getNaturalDataAuto(loginUserDetails,appendSufix(new String [] {"506a689d0f42df11cfaa63af3eec90e4"},loginUserDetails.getDistrict()),"非煤矿山(个)",3,null));
        endGate.countDown();
    }).start();
​
    //4.烟花爆竹
    new Thread(() -> {
        listVos.add(safetyRiskSourceService.getNaturalDataAuto(loginUserDetails,appendSufix(new String [] {"e8a2668a0d7b79a66a197a7a14925201"},loginUserDetails.getDistrict()),"烟花爆竹(个)",4,null));
        endGate.countDown();
    }).start();
​
    //5.自然灾害
    new Thread(() -> {
        listVos.add(safetyRiskSourceService.getNaturalDataAuto(loginUserDetails,appendSufix(new String [] {"1ff895900f7c7a708ef103168597b1c1"},loginUserDetails.getDistrict()),"自然灾害",5,null));
        endGate.countDown();
    }).start();
​
    //6.道路运输
    new Thread(() -> {
        listVos.add(safetyRiskSourceService.getNaturalDataAuto(loginUserDetails,appendSufix(new String []{"5357f32abc4dda610c622a89ee88d006"},loginUserDetails.getDistrict()),"道路运输",6,null));
        endGate.countDown();
    }).start();
​
    //7.建设工程
    new Thread(() -> {        listVos.add(safetyRiskSourceService.getNaturalDataAuto(loginUserDetails,getJSGCWJFW(loginUserDetails.getDistrict()),"建设工程及危旧房屋",7,null));
        endGate.countDown();
    }).start();
​
    //8.密集场所
    new Thread(() -> {
        listVos.add(safetyRiskSourceService.getNaturalDataAuto(loginUserDetails,appendSufix(new String[]{"f0422a6fd0a15f5d99602bca4f8e0446"},loginUserDetails.getDistrict()),"人员密集场所",8,null));
        endGate.countDown();
    }).start();
​
    //9.山塘易涝积水
    new Thread(() -> {
        listVos.add(safetyRiskSourceService.getNaturalDataAuto(loginUserDetails,appendSufix(new String [] {"47c95f2c234549f54c5213d0ae0d6ba9"},loginUserDetails.getDistrict()),"水利工程",9,null));
        endGate.countDown();
    }).start();
​
    endGate.await();
​
} catch (InterruptedException e) {
    result.error500("查询失败");
    e.printStackTrace();
}

模拟高并发场景

static CountDownLatch countDownLatch=new CountDownLatch(1);
@Override
public void run() {
    try {
         countDownLatch.await();
         //TODO
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
        System.out.println("ThreadName:"+Thread.currentThread().getName());
    }
public static void main(String[] args) throws InterruptedException {
     for(int i=0;i<1000;i++){
         new Demo().start();
     }
     countDownLatch.countDown();
}

总的来说,凡事涉及到需要指定某个人物在执行之前,要等到前置人物执行完毕之后才执行的场景,都可以使用CountDownLatch

源码分析

CountDownLatch的UML类图如下:

img

CountDownLatch的数据结构很简单,它是通过共享锁实现的。它包含了sync对象,sync是Sync类型。Sync是实例类,它继承于AQS。对于 CountDownLatch,我们仅仅需要关心两个方法,一个是 countDown() 方法,另一个是 await() 方法。countDown() 方法每次调用都会将 state 减 1,直到state 的值为 0;而 await 是一个阻塞方法,当 state 减 为 0 的时候,await 方法才会返回。await 可以被多个线程调用,大家在这个时候脑子里要有个图:所有调用了await 方法的线程阻塞在 AQS 的阻塞队列中,等待条件满足(state == 0),将线程从队列中一个个唤醒过来。

首先我们来看一下await() 的源码:

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

//判断当前线程是否获取到了共享锁( 在CountDownLatch 中,使用的是共享锁机制,因为CountDownLatch并不需要实现互斥的特性)
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
         //state 如果不等于0,说明当前线程需要加入到共享锁队列中
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        ////创建一个共享模式的节点添加到队列中
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    //就判断尝试获取锁
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        ////r>=0 表示获取到了执行权限,这个时候因为 state!=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);
        }
    }

加入这个时候有 3 个线程调用了 await 方法,由于这个时候 state 的值还不为 0,所以这三个线程都会加入到 AQS队列中。并且三个线程都处于阻塞状态

image-20201124131314402

CountDownLatch.countDown

由于线程被 await 方法阻塞了,所以只有等到countdown 方法使得 state=0 的时候才会被唤醒,我们来看看 countdown 做了什么

  1. 只有当 state 减为 0 的时候,tryReleaseShared 才返回 true, 否则只是简单的 state = state - 1

  2. 如果 state=0, 则调用 doReleaseShared唤醒处于 await 状态下的线程

public void countDown() {
        sync.releaseShared(1);
    }

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

//用自旋的方法实现 state 减 1
protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

AQS. doReleaseShared

共享锁的释放和独占锁的释放有一定的差别前面唤醒锁的逻辑和独占锁是一样,先判断头结点是不是SIGNAL 状态,如果是,则修改为 0,并且唤醒头结点的下一个节点PROPAGATE: 标识为PROPAGATE状态的节点,是共享锁模式下的节点状态,处于这个状态下的节点,会对线程的唤醒进行传播

private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                //这个 CAS 失败的场景是:执行到这里的时候,刚好有一个节点入队,入队会将这个 ws 设置为 -1
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            // 如果到这里的时候,前面唤醒的线程已经占领了 head,那么再循环
             // 通过检查头节点是否改变了,如果改变了就继续循环
            if (h == head)                   // loop if head changed
                break;
        }
    }

h == head:说明头节点还没有被刚刚用unparkSuccessor 唤醒的线程(这里可以理解为ThreadB)占有,此时 break 退出循环。h != head:头节点被刚刚唤醒的线程(这里可以理解为ThreadB)占有,那么这里重新进入下一轮循环,唤醒下一个节点(这里是 ThreadB )。我们知道,等到ThreadB 被唤醒后,其实是会主动唤醒 ThreadC...

doAcquireSharedInterruptibly

一旦 ThreadA 被唤醒,代码又会继续回到doAcquireSharedInterruptibly 中来执行。如果当前 state满足=0 的条件,则会执行 setHeadAndPropagate 方法

private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                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);
        }
    }

setHeadAndPropagate

这个方法的主要作用是把被唤醒的节点,设置成 head 节 点。 然后继续唤醒队列中的其他线程。由于现在队列中有 3 个线程处于阻塞状态,一旦 ThreadA被唤醒,并且设置为 head 之后,会继续唤醒后续的ThreadB

private void setHeadAndPropagate(Node node, int propagate) {
        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();
        }
    }

image-20201124131920440

Semaphore

semaphore 也就是我们常说的信号灯,semaphore 可以控制同时访问的线程个数,通过 acquire 获取一个许可,如果没有就等待,通过 release 释放一个许可。有点类似限流的作用。叫信号灯的原因也和他的用处有关,比如某商场就 5 个停车位,每个停车位只能停一辆车,如果这个时候来了 10 辆车,必须要等前面有空的车位才能进入。

使用案例

public class SemaphoreDemo {

    //限流(AQS)

    //permits; 令牌(5)

    //公平和非公平


    static class Car extends  Thread{
        private int num;
        private Semaphore semaphore;

        public Car(int num, Semaphore semaphore) {
            this.num = num;
            this.semaphore = semaphore;
        }
        public void run(){
            try {
                semaphore.acquire(); //获得一个令牌, 如果拿不到令牌,就会阻塞
                System.out.println("第"+num+" 抢占一个车位");
                Thread.sleep(2000);
                System.out.println("第"+num+" 开走喽");
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Semaphore semaphore=new Semaphore(5);
        for(int i=0;i<10;i++){
            new Car(i,semaphore).start();
        }
    }
}

使用场景

Semaphore 比较常见的就是用来做限流操作了

源码分析

从 Semaphore 的功能来看,我们基本能猜测到它的底层实现一定是基于 AQS 的共享锁,因为需要实现多个线程共享一个领排池创建 Semaphore 实例的时候,需要一个参数 permits,这个基本上可以确定是设置给 AQS 的 state 的,然后每个线程调用 acquire 的时候,执行 state = state - 1,release 的时候执行 state = state + 1,当然,acquire 的时候,如果 state = 0,说明没有资源了,需要等待其他线程 release。Semaphore 分公平策略和非公平策略

FairSync

image-20201124134122164

NonfairSync

image-20201124134159195

两者区别在于是不是会先判断是否有线程在排队,然后才进行 CAS 减操作

CyclicBarrier

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续工作。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 当前线程已经到达了屏障,然后当前线程被阻塞

使用场景

当存在需要所有的子任务都完成时,才执行主任务,这个时候就可以选择使用 CyclicBarrier(跟countDownLatch很像,不过countDownLatch不可重用)

案例略......

注意点

1)对于指定计数值 parties,若由于某种原因,没有足够的线程调用 CyclicBarrier 的 await,则所有调用 await 的线程都会被阻塞;

2)同样的 CyclicBarrier 也可以调用 await(timeout, unit),设置超时时间,在设定时间内,如果没有足够线程到达,则解除阻塞状态,继续工作;

3)通过 reset 重置计数,会使得进入 await 的线程出现BrokenBarrierException;

4 )如果采用是 CyclicBarrier(int parties, Runnable barrierAction) 构造方法,执行 barrierAction 操作的是最后一个到达的线程

 

Condition

Condition原理

 

Condition 是一个多线程协调通信的工具类,可以让某些线程一起等待某个条件(condition),只有满足条件时,线程才会被唤醒。

 

在AQS中存在两个FIFO队列:同步队列 和 等待队列。本篇文章主要是讲condition实现原理(即等待队里),同步队列实现原理看这篇文章:深入剖析AQS。等待队列是由Condition内部实现的,是一个虚拟的FIFO单向队列,在AQS中同步队列、等待队列组成关系如下图:

 

image

 

  • (1)AQS中tail 和 head主要构成了一个FIFO双向的同步队列。
  • (2)AQS中condition构成了一个FIFO单向等待队列。condition是AQS内部类,每个Condition对象中保存了firstWaiter和lastWaiter作为队列首节点和尾节点,每个节点使用Node.nextWaiter保存下一个节点的引用,因此等待队列是一个单向队列。

 

在Object的监视器(monitor)模型上,一个对象拥有一个同步队列和一个等待队列;而并发包中的AQS上拥有一个同步队列和多个等待队列。两者的具体实现原理的有所不同,但在多线程下等待/唤醒 操作的思路有相同之处,Object的监视器模型 和 AQS对同步队列、等待队列对应关系如下图

 

(1)Object的监视器模型同步、等待队列对应关系图

 

image

 

多个线程并发访问某个对象监视器(Monitor对象)的时候,即多线程执行Synchonized处的代码时,monitor处理过程包括:

 

  • (1)thread进入Synchonized代码时,会执行Monitor.Enter命令来获取monitor对象。如果命令执行成功获取Monitor对象成功,执行失败线程会进入synchronized同步队列中,线程处于BLOCKED,直到monitor对象被释放。
  • (2)thread执行完Synchonized同步代码块后,会执行Monitor.exit命令来释放monitor对象,并通知同步队列会获取monitor对象。
  • (3)如果线程执行object.wait(),线程会进入synchronized等待队列进行WAITING,直到其他线程线程执行notify()或notifyAll()方法,将等待队列中的一个或多个等待线程从等待队列中移到同步队列中,被移动的线程状态由WAITING变为BLOCKED。

 

(2)AQS中同步、等待队列对应关系图

15.PNG

当多线程并发访问AQS的lock()、await()、single()方法时,同步队列和等待队列变化处理过程包括:

 

  • (1)多个形成执行lock()方法时,线程会竞争获取同步锁state,获取成功的线程占有锁state、获取失败的线程会封装成node加入到AQS的同步队列中,等待锁state的释放。
  • (2)等获取了state锁的线程(同步队列中head节点)执行await()方法时,condition会将当前线程封装成一个新的node添加到condition等待队列的尾部,同时阻塞(waiting),直到被唤醒。
  • (3)等获取了state锁的线程(同步队列中head节点)single()方法时,condition会将等待队列首节点移动到同步队列的尾部,直到获取同步锁state才被唤醒。

 

Condition 的基本使用

 

public class ConditionDemoWait implements Runnable {
    private Lock lock;
    private Condition condition;
    public ConditionDemoWait(Lock lock,
                             Condition condition) {
        this.lock = lock;
        this.condition = condition;
    }
    @Override
    public void run() {
        System.out.println("begin - ConditionDemoWait");
        try {
            lock.lock();
            condition.await();
            System.out.println("end - ConditionDemoWait");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public class ConditionDemoSignal implements
        Runnable {
    private Lock lock;
    private Condition condition;
    public ConditionDemoSignal(Lock lock,
                               Condition condition) {
        this.lock = lock;
        this.condition = condition;
    }
    @Override
    public void run() {
        System.out.println("begin - ConditionDemoSignal");
        try {
            lock.lock();
            condition.signal();
            System.out.println("end - ConditionDemoSignal");
        } finally {
            lock.unlock();
        }
    }
}

 

通过这个案例简单实现了 wait 和 notify 的功能,当调用await 方法后,当前线程会释放锁并等待,而其他线程调用condition 对象的 signal 或者 signalall 方法通知并被阻塞的线程,然后自己执行 unlock 释放锁,被唤醒的线程获得之前的锁继续执行,最后释放锁。所以,condition 中两个最重要的方法,一个是 await,一个是 signal 方法

 

await:把当前线程阻塞挂起

 

signal:唤醒阻塞的线程

 

Condition 实现源码分析

 

1. 等待的实现

 

当线程调用Condition.await()方法时,将会把前线程封装成node节点,并将节点加入等待队列的尾部,然后释放同步state状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。当前线程加入Condition的等待队列逻辑如下图:

 

16.png

  • 能够调用Condition.await()方法的节点是获取了同步state锁的node,即同步队列中的head节点;调用Condition的await()方法(或者以await开头的方法)会使当前线程进入等待队列并释放锁、唤醒同步队列中的后继节点,最后线程状态变为等待状态。
  • Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。
  • 调用Condition.await()节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了state锁的线程,也就是说该过程是由锁来保证线程安全的。

 

await()方法源码

 

整个await()的执行的过程可以总结如下几步:

 

  • 将当前线程封装成node加入Condition等待队列尾部。
  • 释放state锁:不管重入几次,都把state释放为0,同时唤醒同步队列的后继节点。
  • 自旋:直到node节点在等待队列上的节点移动到了同步队列(通过其他线程调用signal())或被中断。
  • 阻塞当前节点,直到node获取到了锁,也就是node在同步队列上的节点排队排到了队首。

 

public final void await() throws InterruptedException {
            if (Thread.interrupted())////如果当前线程中断,抛出异常
                throw new InterruptedException();
    //1.将当前线程封装成一个node并加入到等待队列队尾(这里如果lastWaiter是CANCELLED取消状态,那么会把它踢出Condition队列)。
            Node node = addConditionWaiter();
            //2.释放当前线程的独占锁,不管重入几次,都把state释放为0
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            //3.判断当前节点没有在同步队列上,没有在同步队列上(即还没有被signal),则将当前线程阻塞
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                //4.判断标记两种中断:是在被signal前中断还是在被signal后中断,分别标记上THROW_IE和REINTERRUPT。
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //5.这个时候线程已经被signal()或者signalAll()操作给唤醒了,退出了3中的while循环自旋等待,尝试再次获取锁
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            // clean up if cancelled 清除等待队列中等待状态不为CONDITION的节点
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
         //6.在第4步中标记的中断状态,如果是在被signal前中断还是在被signal后中断,如果是被signal前就被中断则抛出 InterruptedException,否则执行 Thread.currentThread().interrupt();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

 

addConditionWaiter()方法源码

 

//addConditionWaiter()方法主要是将线程封装成节点,添加到等待队列尾部
private Node addConditionWaiter() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node t = lastWaiter;
            //Condition里面的节点状态不是等待状态CONDITION时,会清除节点
            // If lastWaiter is cancelled, clean out.
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            //将当前线程分装成一个node,加到等待多了尾部
            Node node = new Node(Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

 

fullyRelease()源码

 

fullyRelease()方法中会调用release()释放掉state锁,不管重入几次,都把state释放为0,同时唤醒同步队列的后继节点。

 

final int fullyRelease(Node node) {
        try {
            //获取持有state锁的次数
            int savedState = getState();
            //把state释放为0
            if (release(savedState))
                return savedState;
            throw new IllegalMonitorStateException();
        } catch (Throwable t) {
            //释放锁失败,再将node设置为CANCELLED状态
            node.waitStatus = Node.CANCELLED;
            throw t;
        }
    }

 

isOnSyncQueue()源码

 

    final boolean isOnSyncQueue(Node node) {
         //如果当前节点状态是CONDITION或node.prev是null,则证明当前节点在等待队列上而不是同步队列上。用node.prev来判断,是因为一个节点如果要加入同步队列,在加入前就会设置好prev字段。
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        //如果node.next不为null,则一定在同步队列上,因为node.next是在节点加入同步队列后设置的
        if (node.next != null) // If has successor, it must be on queue
            return true;
        //从等待队列的尾部遍历判断node是否在等待队列
        return findNodeFromTail(node);
    }

 

reportInterruptAfterWait()方法源码

 

reportInterruptAfterWait()方法会根据中断状态来判断是抛出异常,还是执行中断。即判断线程是在被signal前中断,还是在被signal后中断;如果是被signal前就被中断则抛出 InterruptedException,否则执行 Thread.currentThread().interrupt()。

 

private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
            if (interruptMode == THROW_IE)
                throw new InterruptedException();
            else if (interruptMode == REINTERRUPT)
                selfInterrupt();
        }

 

到此condition的wait()方法分析就完了,可以看出,await()的操作过程和Object.wait()方法是一样,只不过await()采用了Condition等待队列的方式实现了Object.wait()的功能。

 

2.通知的实现

 

调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将等待队列中节点移到同步队列中。Condition的signal()方法将节点从等待队列移动到同步队列逻辑如下图:

 

17.png

 

整个signal()的过程可以总结如下:

 

  • (1)执行signal()唤醒线程时,先判断当前线程是否是同步锁state持有线程,所以能够调用signal()方法的线程一定持有了同步锁state。
  • (2)自旋唤醒等待队列的firstWaiter(首节点),在唤醒firstWaiter节点之前,会将等待队列首节点移到同步队列中。

 

public final void signal() {
       //判断当前线程是否是state锁持有线程
       if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
       Node first = firstWaiter;
       //等待队列首节点不为null时,唤醒首节点
       if (first != null)
            doSignal(first);
}
//自旋唤醒首节点
private void doSignal(Node first) {
        do {
            //移动头节点指针firstWaiter
            if ( (firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            //从等待队列中移除首节点
            first.nextWaiter = null;
                 //transferForSignal方法尝试唤醒当前节点,如果唤醒失败,则继续尝试唤醒
         } while (!transferForSignal(first) &&
                (first = firstWaiter) != null);
}
//尝试唤醒当前节点,并将当前节点移动到同步队列中
final boolean transferForSignal(Node node) {
        //如果当前节点状态为CONDITION,则将状态改为0准备加入同步队列;如果当前状态不为CONDITION,说明该节点等待已被中断
        if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
            return false;
        //将node添加到同步队列中,返回的p是node节点在同步队列中的先驱节点
        Node p = enq(node);
        int ws = p.waitStatus;
         //如果先驱节点的状态为CANCELLED(大于0) 或设置先驱节点的状态为SIGNAL失败,那么就立即唤醒当前节点对应的线程,线程被唤醒后会执行await()方法中的acquireQueued()方法,该方法会重新尝试将节点的先驱状态设为SIGNAL并再次park线程;如果当前设置前驱节点状态为SIGNAL成功,那么就不需要马上唤醒线程了,当它的前驱节点成为同步队列的首节点且释放同步状态后,会自动唤醒它。
        if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

 

总结:

 

  • (1)Condition等待通知的本质就是等待队列 和 同步队列的交互的过程,跟object的wait()/notify()机制一样;Condition是基于同步锁state实现的,而objec是基于monitor模式实现的。
  • (2)一个lock(AQS)可以有多个Condition,即多个等待队列,只有一个同步队列。
  • (3)Condition.await()方法执行时,会将同步队列里的head锁释放掉,把线程封装成新node添加到等待队列中;Condition.signal()方法执行时,会把等待队列中的首节点移到同步队列中去,直到锁state被获取才被唤醒。

 

CountDownLatch

 

countdownlatch 是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完毕再执行。从命名可以解读到 countdown 是倒数的意思,类似于我们倒计时的概念。countdownlatch 提供了两个方法,一个是 countDown,一个是 await,countdownlatch 初始化的时候需要传入一个整数,在这个整数倒数到 0 之前,调用了 await 方法的程序都必须要等待,然后通过 countDown 来倒数。

 

使用案例

 

在应急项目中的首页查询接口中用到了countDownLatch,目的是阻塞主线程防止子线程还未查询完成就直接返回不完整的结果给前端

 

CountDownLatch endGate = new CountDownLatch(9);
LoginUserDetails loginUserDetails = UserUtil.getUser();
try {
    //1.化工危化
    new Thread(() -> {
        listVos.add(safetyRiskSourceService.getNaturalDataAuto(loginUserDetails,appendSufix(new String [] {"905265f501ab74ed2a968436d08ecde0"},loginUserDetails.getDistrict()),"化工危化企业(个)",1,null));
        endGate.countDown();
    }).start();
    //2.规上企业
    new Thread(() -> {
        listVos.add(safetyRiskSourceService.getNaturalDataAuto(loginUserDetails,appendSufix(new String [] {"6850908220874f05ca5cfdf87ceda4dd"},loginUserDetails.getDistrict()),"规上企业(个)",2,null));
        endGate.countDown();
    }).start();
    //3.非煤矿山
    new Thread(() -> {
        listVos.add(safetyRiskSourceService.getNaturalDataAuto(loginUserDetails,appendSufix(new String [] {"506a689d0f42df11cfaa63af3eec90e4"},loginUserDetails.getDistrict()),"非煤矿山(个)",3,null));
        endGate.countDown();
    }).start();
    //4.烟花爆竹
    new Thread(() -> {
        listVos.add(safetyRiskSourceService.getNaturalDataAuto(loginUserDetails,appendSufix(new String [] {"e8a2668a0d7b79a66a197a7a14925201"},loginUserDetails.getDistrict()),"烟花爆竹(个)",4,null));
        endGate.countDown();
    }).start();
    //5.自然灾害
    new Thread(() -> {
        listVos.add(safetyRiskSourceService.getNaturalDataAuto(loginUserDetails,appendSufix(new String [] {"1ff895900f7c7a708ef103168597b1c1"},loginUserDetails.getDistrict()),"自然灾害",5,null));
        endGate.countDown();
    }).start();
    //6.道路运输
    new Thread(() -> {
        listVos.add(safetyRiskSourceService.getNaturalDataAuto(loginUserDetails,appendSufix(new String []{"5357f32abc4dda610c622a89ee88d006"},loginUserDetails.getDistrict()),"道路运输",6,null));
        endGate.countDown();
    }).start();
    //7.建设工程
    new Thread(() -> {        listVos.add(safetyRiskSourceService.getNaturalDataAuto(loginUserDetails,getJSGCWJFW(loginUserDetails.getDistrict()),"建设工程及危旧房屋",7,null));
        endGate.countDown();
    }).start();
    //8.密集场所
    new Thread(() -> {
        listVos.add(safetyRiskSourceService.getNaturalDataAuto(loginUserDetails,appendSufix(new String[]{"f0422a6fd0a15f5d99602bca4f8e0446"},loginUserDetails.getDistrict()),"人员密集场所",8,null));
        endGate.countDown();
    }).start();
    //9.山塘易涝积水
    new Thread(() -> {
        listVos.add(safetyRiskSourceService.getNaturalDataAuto(loginUserDetails,appendSufix(new String [] {"47c95f2c234549f54c5213d0ae0d6ba9"},loginUserDetails.getDistrict()),"水利工程",9,null));
        endGate.countDown();
    }).start();
    endGate.await();
} catch (InterruptedException e) {
    result.error500("查询失败");
    e.printStackTrace();
}

 

模拟高并发场景

 

static CountDownLatch countDownLatch=new CountDownLatch(1);
@Override
public void run() {
    try {
        countDownLatch.await();
        //TODO
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("ThreadName:"+Thread.currentThread().getName());
    }
public static void main(String[] args) throws InterruptedException {
    for(int i=0;i<1000;i++){
        new Demo().start();
    }
    countDownLatch.countDown();
}

 

总的来说,凡事涉及到需要指定某个人物在执行之前,要等到前置人物执行完毕之后才执行的场景,都可以使用CountDownLatch

 

源码分析

 

CountDownLatch的UML类图如下:

 

image

 

CountDownLatch的数据结构很简单,它是通过共享锁实现的。它包含了sync对象,sync是Sync类型。Sync是实例类,它继承于AQS。对于 CountDownLatch,我们仅仅需要关心两个方法,一个是 countDown() 方法,另一个是 await() 方法。countDown() 方法每次调用都会将 state 减 1,直到state 的值为 0;而 await 是一个阻塞方法,当 state 减 为 0 的时候,await 方法才会返回。await 可以被多个线程调用,大家在这个时候脑子里要有个图:所有调用了await 方法的线程阻塞在 AQS 的阻塞队列中,等待条件满足(state == 0),将线程从队列中一个个唤醒过来。

 

首先我们来看一下await() 的源码:

 

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
//判断当前线程是否获取到了共享锁( 在CountDownLatch 中,使用的是共享锁机制,因为CountDownLatch并不需要实现互斥的特性)
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //state 如果不等于0,说明当前线程需要加入到共享锁队列中
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        ////创建一个共享模式的节点添加到队列中
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    //就判断尝试获取锁
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        ////r>=0 表示获取到了执行权限,这个时候因为 state!=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);
        }
    }

 

加入这个时候有 3 个线程调用了 await 方法,由于这个时候 state 的值还不为 0,所以这三个线程都会加入到 AQS队列中。并且三个线程都处于阻塞状态

 

image

 

CountDownLatch.countDown

 

由于线程被 await 方法阻塞了,所以只有等到countdown 方法使得 state=0 的时候才会被唤醒,我们来看看 countdown 做了什么

 

  1. 只有当 state 减为 0 的时候,tryReleaseShared 才返回 true, 否则只是简单的 state = state - 1
  2. 如果 state=0, 则调用 doReleaseShared唤醒处于 await 状态下的线程

 

public void countDown() {
        sync.releaseShared(1);
    }
public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
//用自旋的方法实现 state 减 1
protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

 

AQS. doReleaseShared

 

共享锁的释放和独占锁的释放有一定的差别前面唤醒锁的逻辑和独占锁是一样,先判断头结点是不是SIGNAL 状态,如果是,则修改为 0,并且唤醒头结点的下一个节点PROPAGATE: 标识为PROPAGATE状态的节点,是共享锁模式下的节点状态,处于这个状态下的节点,会对线程的唤醒进行传播

 

private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                //这个 CAS 失败的场景是:执行到这里的时候,刚好有一个节点入队,入队会将这个 ws 设置为 -1
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            // 如果到这里的时候,前面唤醒的线程已经占领了 head,那么再循环
            // 通过检查头节点是否改变了,如果改变了就继续循环
            if (h == head)                   // loop if head changed
                break;
        }
    }

 

h == head:说明头节点还没有被刚刚用unparkSuccessor 唤醒的线程(这里可以理解为ThreadB)占有,此时 break 退出循环。h != head:头节点被刚刚唤醒的线程(这里可以理解为ThreadB)占有,那么这里重新进入下一轮循环,唤醒下一个节点(这里是 ThreadB )。我们知道,等到ThreadB 被唤醒后,其实是会主动唤醒 ThreadC...

 

doAcquireSharedInterruptibly

 

一旦 ThreadA 被唤醒,代码又会继续回到doAcquireSharedInterruptibly 中来执行。如果当前 state满足=0 的条件,则会执行 setHeadAndPropagate 方法

 

private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                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);
        }
    }

 

setHeadAndPropagate

 

这个方法的主要作用是把被唤醒的节点,设置成 head 节 点。 然后继续唤醒队列中的其他线程。由于现在队列中有 3 个线程处于阻塞状态,一旦 ThreadA被唤醒,并且设置为 head 之后,会继续唤醒后续的ThreadB

 

private void setHeadAndPropagate(Node node, int propagate) {
        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();
        }
    }

 

image

 

Semaphore

 

semaphore 也就是我们常说的信号灯,semaphore 可以控制同时访问的线程个数,通过 acquire 获取一个许可,如果没有就等待,通过 release 释放一个许可。有点类似限流的作用。叫信号灯的原因也和他的用处有关,比如某商场就 5 个停车位,每个停车位只能停一辆车,如果这个时候来了 10 辆车,必须要等前面有空的车位才能进入。

 

使用案例

 

public class SemaphoreDemo {
    //限流(AQS)
    //permits; 令牌(5)
    //公平和非公平
    static class Car extends  Thread{
        private int num;
        private Semaphore semaphore;
        public Car(int num, Semaphore semaphore) {
            this.num = num;
            this.semaphore = semaphore;
        }
        public void run(){
            try {
                semaphore.acquire(); //获得一个令牌, 如果拿不到令牌,就会阻塞
                System.out.println("第"+num+" 抢占一个车位");
                Thread.sleep(2000);
                System.out.println("第"+num+" 开走喽");
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        Semaphore semaphore=new Semaphore(5);
        for(int i=0;i<10;i++){
            new Car(i,semaphore).start();
        }
    }
}

 

使用场景

 

Semaphore 比较常见的就是用来做限流操作了

 

源码分析

 

从 Semaphore 的功能来看,我们基本能猜测到它的底层实现一定是基于 AQS 的共享锁,因为需要实现多个线程共享一个领排池创建 Semaphore 实例的时候,需要一个参数 permits,这个基本上可以确定是设置给 AQS 的 state 的,然后每个线程调用 acquire 的时候,执行 state = state - 1,release 的时候执行 state = state + 1,当然,acquire 的时候,如果 state = 0,说明没有资源了,需要等待其他线程 release。Semaphore 分公平策略和非公平策略

 

FairSync

 

image

 

NonfairSync

 

image

 

两者区别在于是不是会先判断是否有线程在排队,然后才进行 CAS 减操作

 

CyclicBarrier

 

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续工作。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 当前线程已经到达了屏障,然后当前线程被阻塞

 

使用场景

 

当存在需要所有的子任务都完成时,才执行主任务,这个时候就可以选择使用 CyclicBarrier(跟countDownLatch很像,不过countDownLatch不可重用)

 

案例略......

 

注意点

 

1)对于指定计数值 parties,若由于某种原因,没有足够的线程调用 CyclicBarrier 的 await,则所有调用 await 的线程都会被阻塞;

 

2)同样的 CyclicBarrier 也可以调用 await(timeout, unit),设置超时时间,在设定时间内,如果没有足够线程到达,则解除阻塞状态,继续工作;

 

3)通过 reset 重置计数,会使得进入 await 的线程出现BrokenBarrierException;

 

4 )如果采用是 CyclicBarrier(int parties, Runnable barrierAction) 构造方法,执行 barrierAction 操作的是最后一个到达的线程

posted @ 2020-11-24 13:56  胡庆安  阅读(572)  评论(0编辑  收藏  举报