Java高并发系列——检视阅读(四)

Java高并发系列——等待唤醒

疑问:

Q:Condition能够支持超时时间的设置,而Object不支持。Object不是有wait(long timeout)超时时间设置么?

这个指的是Object没有方法可以使当前线程释放锁并进入等待状态到将来某个时间。

Q:在Condition.await()方法被调用时,当前线程会释放这个锁,并且当前线程会进行等待(处于阻塞状态)?是阻塞状态不是等待状态?是因为这事api语言级别的等待/通知机制么?

不是处于阻塞状态,而是进入等待队列。

JUC中的LockSupport工具类——无需加锁及考虑等待通知顺序

使用Object类中的方法实现线程等待和唤醒

关于Object类中的用户线程等待和唤醒的方法,总结一下:

  1. wait()/notify()/notifyAll()方法都必须放在同步代码(必须在synchronized内部执行)中执行,需要先获取锁(否则抛出了 IllegalMonitorStateException异常 )
  2. 线程唤醒的方法(notify、notifyAll)需要在等待的方法(wait)之后执行,等待中的线程才可能会被唤醒,否则无法唤醒

使用Condition实现线程的等待和唤醒

关于Condition中方法使用总结:

  1. 使用Condtion中的线程等待和唤醒方法之前,需要先获取锁。否者会报 IllegalMonitorStateException异常
  2. signal()方法先于await()方法之前调用,线程无法被唤醒

Object和Condition的局限性

关于Object和Condtion中线程等待和唤醒的局限性,有以下几点:

  1. 两种方式中的让线程等待和唤醒的方法能够执行的先决条件是:线程需要先获取锁
  2. 唤醒方法需要在等待方法之后调用,线程才能够被唤醒

关于这2点,LockSupport都不需要,就能实现线程的等待和唤醒。

LockSupport类介绍

LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程。主要是通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作的。(注意park方法等待不释放锁)

每个线程都有一个许可(permit),permit只有两个值1和0,默认是0。

  1. 当调用unpark(thread)方法,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)。
  2. 当调用park()方法,如果当前线程的permit是1,那么将permit设置为0,并立即返回。如果当前线程的permit是0,那么当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0,并返回。

注意:因为permit默认是0,所以一开始调用park()方法,线程必定会被阻塞。调用unpark(thread)方法后,会自动唤醒thread线程,即park方法立即返回。

LockSupport中常用的方法

阻塞线程

  • void park():阻塞当前线程,如果调用unpark方法或者当前线程被中断,从能从park()方法中返回
  • void park(Object blocker):功能同方法1,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查
  • void parkNanos(long nanos):阻塞当前线程,最长不超过nanos纳秒,增加了超时返回的特性
  • void parkNanos(Object blocker, long nanos):功能同方法3,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查
  • void parkUntil(long deadline):阻塞当前线程,直到deadline,deadline是一个绝对时间,表示某个时间的毫秒格式
  • void parkUntil(Object blocker, long deadline):功能同方法5,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;

唤醒线程

  • void unpark(Thread thread):唤醒处于阻塞状态的指定线程

1、LockSupport调用park、unpark方法执行唤醒等待无需加锁。

2、LockSupport中,唤醒的方法不管是在等待之前还是在等待之后调用,线程都能够被唤醒。 唤醒方法在等待方法之前执行,线程也能够被唤醒,这点是另外两种方法无法做到的。而Object和Condition中的唤醒必须在等待之后调用,线程才能被唤醒。

3、park方法可以相应线程中断。

LockSupport.park方法让线程等待之后,唤醒方式有2种:

  1. 调用LockSupport.unpark方法
  2. 调用等待线程的 interrupt()方法,给等待的线程发送中断信号,可以唤醒线程

线程t1和t2的不同点是,t2中调用park方法传入了一个BlockerDemo对象,从上面的线程堆栈信息中,发现t2线程的堆栈信息中多了一行 -parking to waitfor<0x00000007180bfeb0>(a com.itsoku.chat10.Demo10$BlockerDemo),刚好是传入的BlockerDemo对象,park传入的这个参数可以让我们在线程堆栈信息中方便排查问题,其他暂无他用。

线程等待和唤醒的3种方式做个对比

  1. 方式1:Object中的wait、notify、notifyAll方法
  2. 方式2:juc中Condition接口提供的await、signal、signalAll方法
  3. 方式3:juc中的LockSupport提供的park、unpark方法

3种方式对比:

Object Condtion LockSupport
前置条件 需要在synchronized中运行 需要先获取Lock的锁
无限等待 支持 支持 支持
超时等待 支持 支持 支持
等待到将来某个时间返回 不支持 支持 支持
等待状态中释放锁 会释放 会释放 不会释放
唤醒方法先于等待方法执行,能否唤醒线程 可以
是否能响应线程中断
线程中断是否会清除中断标志
是否支持等待状态中不响应中断 不支持 支持 不支持

实例:

public class LockSupportTest {

    /**
     * 输出:
     * create a thread start
     * 主线程执行完毕!
     * thread 被唤醒
     */
    public static void main1(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("create a thread start");
            LockSupport.park();
            System.out.println("thread 被唤醒");
        });
        t.start();

        TimeUnit.SECONDS.sleep(3);
        //唤醒处于阻塞状态的指定线程
        LockSupport.unpark(t);
        //响应线程中断
        //t.interrupt();
        System.out.println("主线程执行完毕!");
    }

    /**
     * 唤醒方法在等待方法之前执行,线程也能够被唤醒
     *输出:
     * create a thread start
     * 主线程执行完毕!
     * thread 被唤醒
     */
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("create a thread start");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LockSupport.park();
            System.out.println("thread 被唤醒");
        });
        t.start();

        TimeUnit.SECONDS.sleep(1);
        //唤醒处于阻塞状态的指定线程
        LockSupport.unpark(t);
        //响应线程中断
        //t.interrupt();
        System.out.println("主线程执行完毕!");
    }
}
疑问:

Q:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程。主要是通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作的。(注意park方法等待不释放锁)不释放锁的等待唤醒是在什么场景下使用?为什么要这样?

在我看来是因为LockSupport是不需要锁就能进行等待唤醒的,因此如果有锁是话也是其他资源方面的锁,跟LockSupport无关。

JUC中的Semaphore(信号量)——多把锁控制,用于限流

synchronized和重入锁ReentrantLock,这2种锁一次都只能允许一个线程访问一个资源,而信号量可以控制有多少个线程可以访问特定的资源。

Semaphore常用场景:限流

举个例子:

比如有个停车场(临界值,共享资源),有5个空位,门口有个门卫,手中5把钥匙分别对应5个车位上面的锁,来一辆车,门卫会给司机一把钥匙,然后进去找到对应的车位停下来,出去的时候司机将钥匙归还给门卫。停车场生意比较好,同时来了100两车,门卫手中只有5把钥匙,同时只能放5辆车进入,其他车只能等待,等有人将钥匙归还给门卫之后,才能让其他车辆进入。

上面的例子中门卫就相当于Semaphore,车钥匙就相当于许可证,车就相当于线程。

Semaphore主要方法

Semaphore(int permits):构造方法,参数表示许可证数量,用来创建信号量

Semaphore(int permits,boolean fair):构造方法,当fair等于true时,创建具有给定许可数的计数信号量并设置为公平信号量

void acquire() throws InterruptedException:从此信号量获取1个许可前线程将一直阻塞,相当于一辆车占了一个车位,此方法会响应线程中断,表示调用线程的interrupt方法,会使该方法抛出InterruptedException异常

void acquire(int permits) throws InterruptedException :和acquire()方法类似,参数表示需要获取许可的数量;比如一个大卡车要入停车场,由于车比较大,需要申请3个车位才可以停放

void acquireUninterruptibly(int permits) :和acquire(int permits) 方法类似,只是不会响应线程中断

boolean tryAcquire():尝试获取1个许可,不管是否能够获取成功,都立即返回,true表示获取成功,false表示获取失败

boolean tryAcquire(int permits):和tryAcquire(),表示尝试获取permits个许可

boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException:尝试在指定的时间内获取1个许可,获取成功返回true,指定的时间过后还是无法获取许可,返回false

boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException:和tryAcquire(long timeout, TimeUnit unit)类似,多了一个permits参数,表示尝试获取permits个许可

void release():释放一个许可,将其返回给信号量,相当于车从停车场出去时将钥匙归还给门卫

void release(int n):释放n个许可

int availablePermits():当前可用的许可数

获取许可之后不释放

取许可后,没有释放许可的代码,最终导致,可用许可数量为0,其他线程无法获取许可,会在 semaphore.acquire();处等待,导致程序无法结束。

没有获取到许可却执行释放(没有获取到许可却在finally中直接执行release方法)

如果获取锁的过程中发生异常,导致获取锁失败,最后finally里面也释放了许可,最终会导致许可数量凭空增长了。

释放许可正确的姿势

程序中增加了一个变量 acquireSuccess用来标记获取许可是否成功,在finally中根据这个变量是否为true,来确定是否释放许可。

在规定的时间内希望获取许可

Semaphore内部2个方法可以提供超时获取许可的功能:

public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedExceptionpublic boolean tryAcquire(int permits, long timeout, TimeUnit unit)        throws InterruptedException 

在指定的时间内去尝试获取许可,如果能够获取到,返回true,获取不到返回false。

其他一些使用说明

  1. Semaphore默认创建的是非公平的信号量,什么意思呢?这个涉及到公平与非公平。让新来的去排队就表示公平,直接去插队争抢第一个,就表示不公平。对于停车场,排队肯定更好一些。不过对于信号量来说不公平的效率更高一些,所以默认是不公平的。
  2. 方法中带有 throwsInterruptedException声明的,表示这个方法会响应线程中断信号,什么意思?表示调用线程的 interrupt()方法后,会让这些方法触发 InterruptedException异常,即使这些方法处于阻塞状态,也会立即返回,并抛出 InterruptedException异常,线程中断信号也会被清除。

示例:

public class SemaphoreTest {

    private static Semaphore semaphore = new Semaphore(2);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            Thread t = new T1("T" + i);
            t.start();
            if (i > 2) {
                TimeUnit.SECONDS.sleep(1);
                t.interrupt();
            }
        }
    }

    public static void main2(String[] args) {
        for (int i = 0; i < 5; i++) {
            Thread t = new T2("T" + i);
            t.start();
        }
    }

    /**
     * 在指定的时间内去尝试获取许可,如果能够获取到,返回true,获取不到返回false。
     */
    public static class T1 extends Thread {
        public T1(String name) {
            super(name);
        }

        @Override
        public void run() {
            Boolean hasTicket = false;
            Thread thread = Thread.currentThread();
            try {
                //semaphore.acquire();
                hasTicket = semaphore.tryAcquire(1, TimeUnit.SECONDS);
                if (hasTicket) {
                    System.out.println(thread + "获取到停车位!");
                } else {
                    System.out.println(thread + "获取不到停车位!走了");
                }
                //hasTicket = true;
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (hasTicket) {
                    semaphore.release();
                    //没有获取到许可却在finally中直接执行release方法
                    //Thread[T6,5,main]离开停车位!当前空余停车位数量10
                    System.out.println(thread + "离开停车位!当前空余停车位数量" + semaphore.availablePermits());
                }
            }
        }
    }

    /**
     * 正确的释放锁的方式
     */
    public static class T2 extends Thread {
        public T2(String name) {
            super(name);
        }

        @Override
        public void run() {
            Boolean hasTicket = false;
            Thread thread = Thread.currentThread();
            try {
                semaphore.acquire();
                hasTicket = true;
                System.out.println(thread + "获取到停车位!");
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (hasTicket) {
                    semaphore.release();
                    System.out.println(thread + "离开停车位!当前空余停车位数量" + semaphore.availablePermits());
                }
            }
        }
    }
}

输出:

test1
Thread[T0,5,main]获取到停车位!
Thread[T1,5,main]获取到停车位!
Thread[T2,5,main]获取不到停车位!走了
java.lang.InterruptedException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedNanos(AbstractQueuedSynchronizer.java:1039)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.tryAcquireSharedNanos(AbstractQueuedSynchronizer.java:1328)
    at java.util.concurrent.Semaphore.tryAcquire(Semaphore.java:409)
    at com.self.current.SemaphoreTest$T1.run(SemaphoreTest.java:52)
java.lang.InterruptedException: sleep interrupted
Thread[T1,5,main]离开停车位!当前空余停车位数量1
    at java.lang.Thread.sleep(Native Method)
Thread[T4,5,main]获取到停车位!
    at java.lang.Thread.sleep(Thread.java:340)
Thread[T0,5,main]离开停车位!当前空余停车位数量1
    at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
Thread[T4,5,main]离开停车位!当前空余停车位数量2
    at com.self.current.SemaphoreTest$T1.run(SemaphoreTest.java:59)

test2
Thread[T0,5,main]获取到停车位!
Thread[T1,5,main]获取到停车位!
Thread[T1,5,main]离开停车位!当前空余停车位数量2
Thread[T2,5,main]获取到停车位!
Thread[T0,5,main]离开停车位!当前空余停车位数量2
Thread[T3,5,main]获取到停车位!
Thread[T2,5,main]离开停车位!当前空余停车位数量1
Thread[T4,5,main]获取到停车位!
Thread[T3,5,main]离开停车位!当前空余停车位数量1
Thread[T4,5,main]离开停车位!当前空余停车位数量2   

示例2:

通过判断byteBuffer != null得出是否获取到了许可。
semaphore = new Semaphore(maxBufferCount);
 byteBuffer = allocator.allocate();// semaphore.acquire();
  finally {
            if (byteBuffer != null) {
                byteBuffer.clear();
                allocator.release(byteBuffer);
            }
        }
疑问:

Q:Semaphore内部排队等待资源的队列是怎么实现的,公平信号量与非公平的队列类型都是哪种的?

JUC中等待多线程完成的工具类CountDownLatch(闭锁 )——等待多线程完成后执行操作或者实现最大的并发线程数同时执行

CountDownLatch介绍

CountDownLatch称之为闭锁,它可以使一个或一批线程在闭锁上等待,等到其他线程执行完相应操作后,闭锁打开,这些等待的线程才可以继续执行。确切的说,闭锁在内部维护了一个倒计数器。通过该计数器的值来决定闭锁的状态,从而决定是否允许等待的线程继续执行。

一批线程等待闭锁一般用于同步并发,如跑步比赛时作为发令枪作用;

一个线程等待闭锁一般用于等待并发线程或资源的获取满足,用于执行收尾工作,如多任务执行完后合并结果等。

常用方法:

public CountDownLatch(int count):构造方法,count表示计数器的值,不能小于0,否者会报异常。

public void await() throws InterruptedException:调用await()会让当前线程等待,直到计数器为0的时候,方法才会返回,此方法会响应线程中断操作。

public boolean await(long timeout, TimeUnit unit) throws InterruptedException:限时等待,在超时之前,计数器变为了0,方法返回true,否者直到超时,返回false,此方法会响应线程中断操作。

public void countDown():让计数器减1

CountDownLatch(作为一个参数,锁传递到方法中)使用步骤:

  1. 创建CountDownLatch对象
  2. 调用其实例方法 await(),让当前线程等待
  3. 调用 countDown()方法,让计数器减1
  4. 当计数器变为0的时候, await()方法会返回

假如有这样一个需求,当我们需要解析一个Excel里多个sheet的数据时,可以考虑使用多线程,每个线程解析一个sheet里的数据,等到所有的sheet都解析完之后,程序需要统计解析总耗时。分析一下:解析每个sheet耗时可能不一样,总耗时就是最长耗时的那个操作。

方法一:使用join实现。

此方法会让当前线程等待被调用的线程完成之后才能继续。可以看一下join的源码,内部其实是在synchronized方法中调用了线程的wait方法,最后被调用的线程执行完毕之后,由jvm自动调用其notifyAll()方法,唤醒所有等待中的线程。这个notifyAll()方法是由jvm内部自动调用的,jdk源码中是看不到的,需要看jvm源码,有兴趣的同学可以去查一下。所以JDK不推荐在线程上调用wait、notify、notifyAll方法。

方法二:使用CountDownLatch实现。

示例:

public class CountDownLatchTest {

    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        CountDownLatch latch = new CountDownLatch(2);
        T1 t1 = new T1("sheet1", 3, latch);
        T1 t2 = new T1("sheet2", 5, latch);
        t1.start();
        t2.start();
        //调用await()会让当前线程等待,直到计数器为0的时候,方法才会返回,此方法会响应线程中断操作。
        //latch.await();
        //限时等待,在超时之前,计数器变为了0,方法返回true,否者直到超时,返回false,此方法会响应线程中断操作。
        boolean result = latch.await(4, TimeUnit.SECONDS);
        long end = System.currentTimeMillis();
        //System.out.println("主线程结束,耗时" + (end - start));
        System.out.println("主线程结束,耗时" + (end - start)+"是否返回结果:"+result);
    }

    public static class T1 extends Thread {
        private int workTime;
        private CountDownLatch countDownLatch;

        public T1(String name, int workTime, CountDownLatch countDownLatch) {
            super(name);
            this.workTime = workTime;
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            long start = System.currentTimeMillis();
            Thread t = Thread.currentThread();
            System.out.println(t.getName() + "线程开始执行!");
            try {
                TimeUnit.SECONDS.sleep(workTime);
                long end = System.currentTimeMillis();
                System.out.println(t.getName() + "结束,耗时" + (end - start));
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                countDownLatch.countDown();
            }
        }
    }
}

输出:

sheet1线程开始执行!
sheet2线程开始执行!
sheet1结束,耗时3000
主线程结束,耗时4004是否返回结果:false
sheet2结束,耗时5001
主线程中调用 countDownLatch.await();会让主线程等待,t1、t2线程中模拟执行耗时操作,最终在finally中调用了 countDownLatch.countDown();,此方法每调用一次,CountDownLatch内部计数器会减1,当计数器变为0的时候,主线程中的await()会返回,然后继续执行。注意:上面的 countDown()这个是必须要执行的方法,所以放在finally中执行。

2个CountDown结合使用的示例——跑步比赛耗时统计

有3个人参见跑步比赛,需要先等指令员发指令枪后才能开跑,所有人都跑完之后,指令员喊一声,大家跑完了,计算耗时。

示例:

public class CountDownLatchImplRacingTest {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch fireGun = new CountDownLatch(1);
        CountDownLatch latch = new CountDownLatch(3);
        new T1("刘翔", 3, fireGun, latch).start();
        new T1("拼嘻嘻", 5, fireGun, latch).start();
        new T1("蜡笔小新", 7, fireGun, latch).start();
        System.out.println("比赛 wait for ready!");
        ////主线程休眠3秒,模拟指令员准备发枪耗时操作
        TimeUnit.SECONDS.sleep(3);
        long start = System.currentTimeMillis();
        System.out.println("发令枪响,比赛开始!");
        fireGun.countDown();
        latch.await();
        long end = System.currentTimeMillis();
        System.out.println("比赛结束,总耗时" + (end - start));
    }

    public static class T1 extends Thread {
        private int workTime;
        private CountDownLatch fireGun;
        private CountDownLatch countDownLatch;


        public T1(String name, int workTime, CountDownLatch fireGun, CountDownLatch countDownLatch) {
            super(name);
            this.workTime = workTime;
            this.fireGun = fireGun;
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            try {
                fireGun.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long start = System.currentTimeMillis();
            Thread t = Thread.currentThread();
            System.out.println(t.getName() + "运动员开始赛跑!");
            try {
                //模拟耗时操作,休眠workTime秒
                TimeUnit.SECONDS.sleep(workTime);
                long end = System.currentTimeMillis();
                System.out.println(t.getName() + "跑完全程,耗时" + (end - start));
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                countDownLatch.countDown();
            }
        }
    }
}

输出:

比赛 wait for ready!
发令枪响,比赛开始!
刘翔运动员开始赛跑!
蜡笔小新运动员开始赛跑!
拼嘻嘻运动员开始赛跑!
刘翔跑完全程,耗时3000
拼嘻嘻跑完全程,耗时5002
蜡笔小新跑完全程,耗时7001
比赛结束,总耗时7001

手写一个并行处理任务的工具类

示例:

public class TaskDisposeUtils {

    private static final Integer POOL_SIZE = Integer.max(Runtime.getRuntime().availableProcessors(), 5);

    public static void main(String[] args) {
        List list = Stream.iterate(1, a -> a + 1).limit(10).collect(Collectors.toList());
        try {
            TaskDisposeUtils.dispose(list, item -> {
                long start = System.currentTimeMillis();
                //模拟耗时操作,休眠workTime秒
                try {
                    TimeUnit.SECONDS.sleep(item);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                long end = System.currentTimeMillis();
                System.out.println(item + "任务完成,耗时" + (end - start));
            });
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //上面所有任务处理完毕完毕之后,程序才能继续
        System.out.println(list + "任务全部执行完毕!");
    }

    public static  void dispose(List taskList, Consumer consumer) throws InterruptedException {
        dispose(true, POOL_SIZE, taskList, consumer);
    }

    private static  void dispose(boolean moreThread, Integer poolSize, List taskList, Consumer consumer) throws InterruptedException {
        if (CollectionUtils.isEmpty(taskList)) {
            return;
        }

        if (moreThread && poolSize > 1) {
            poolSize = Math.min(poolSize, taskList.size());
            ExecutorService executorService = null;
            try {
                executorService = Executors.newFixedThreadPool(poolSize);
                CountDownLatch latch = new CountDownLatch(taskList.size());
                for (T t : taskList) {
                    executorService.execute(() -> {
                        try {
                            consumer.accept(t);
                        } finally {
                            latch.countDown();
                        }
                    });
                }
                latch.await();
            } finally {
                if (executorService != null) {
                    executorService.shutdown();
                }
            }

        } else {
            for (T t : taskList) {
                consumer.accept(t);
            }
        }
    }
}

输出:

1任务完成,耗时1376
2任务完成,耗时2014
3任务完成,耗时3179
4任务完成,耗时4449
5任务完成,耗时5000
6任务完成,耗时6001
7任务完成,耗时7000
8任务完成,耗时8000
9任务完成,耗时9000
10任务完成,耗时10001
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]任务全部执行完毕!

在实时系统中的使用场景

  1. 实现最大的并行性:有时我们想同时启动多个线程,实现最大程度的并行性。例如,我们想测试一个单例类。如果我们创建一个初始计数为1的CountDownLatch,并让所有线程都在这个锁上等待,那么我们可以很轻松地完成测试。我们只需调用 一次countDown()方法就可以让所有的等待线程同时恢复执行。
  2. 开始执行前等待n个线程完成各自任务:例如应用程序启动类要确保在处理用户请求前,所有N个外部系统已经启动和运行了。
  3. 死锁检测:一个非常方便的使用场景是,你可以使用n个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁
疑问:

Q:这个notifyAll()方法是由jvm内部自动调用的,jdk源码中是看不到的,需要看jvm源码,有兴趣的同学可以去查一下。所以JDK不推荐在线程上调用wait、notify、notifyAll方法。 是因为没有源码所以才不推荐使用wait、notify、notifyAll方法么?还有其他缺点么?

JUC中的循环栅栏CyclicBarrier的6种使用场景

CyclicBarrier简介

CyclicBarrier通常称为循环屏障。它和CountDownLatch很相似,都可以使线程先等待然后再执行。不过CountDownLatch是使一批(一个)线程等待另一批(一个)线程执行完后再执行;而CyclicBarrier只是使等待的线程达到一定数目后再让它们继续执行。故而CyclicBarrier内部也有一个计数器,计数器的初始值在创建对象时通过构造参数指定,如下所示:

public CyclicBarrier(int parties) {
    this(parties, null);
}

每调用一次await()方法都将使阻塞的线程数+1,只有阻塞的线程数达到设定值时屏障才会打开,允许阻塞的所有线程继续执行。除此之外,CyclicBarrier还有几点需要注意的地方:

  • CyclicBarrier的计数器可以重置而CountDownLatch不行,这意味着CyclicBarrier实例可以被重复使用而CountDownLatch只能被使用一次。而这也是循环屏障循环二字的语义所在。
  • CyclicBarrier允许用户自定义barrierAction操作,这是个可选操作,可以在创建CyclicBarrier对象时指定
public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

一旦用户在创建CyclicBarrier对象时设置了barrierAction参数,则在阻塞线程数达到设定值屏障打开前,会调用barrierAction的run()方法完成用户自定义的操作。

CyclicBarrier内部相当于有个计数器(构造方法传入的),每次调用await();后,计数器会减1,并且await()方法会让当前线程阻塞,等待计数器减为0的时候,所有在await()上等待的线程被唤醒,然后继续向下执行,此时计数器又会被还原为创建时的值,然后可以继续再次使用

CountDownLatch和CyclicBarrier的区别

CountDownLatch示例

主管相当于 CountDownLatch,干活的小弟相当于做事情的线程。

老板交给主管了一个任务,让主管搞完之后立即上报给老板。主管下面有10个小弟,接到任务之后将任务划分为10个小任务分给每个小弟去干,主管一直处于等待状态(主管会调用await()方法,此方法会阻塞当前线程),让每个小弟干完之后通知一下主管(调用countDown()方法通知主管,此方法会立即返回),主管等到所有的小弟都做完了,会被唤醒,从await()方法上苏醒,然后将结果反馈给老板。期间主管会等待,会等待所有小弟将结果汇报给自己。

而CyclicBarrier是一批线程让自己等待,等待所有的线程都准备好了,所有的线程才能继续。

CountDownLatch: 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。

CyclicBrrier: N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

重复使用CyclicBarrier、自定义一个所有线程到齐后的处理动作实例:

public class CyclicBarrierTest {

    /**
     * 可以自定义一个所有线程到齐后的处理动作,再唤醒所有线程工作
     */
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(6,()->{
        System.out.println("人都到齐了,大家high起来!");
    });

    public static void main(String[] args) {
        for (int i = 0; i < 6; i++) {
            new T1("驴友"+i, i).start();
        }
    }

    public static class T1 extends Thread {
        private int workTime;

        public T1(String name, int workTime) {
            super(name);
            this.workTime = workTime;
        }

        @Override
        public void run() {
            //等待人齐吃饭
            eat();
            //等待人齐上车下一站旅游
            travel();
        }
        private void eat(){
            Thread t = Thread.currentThread();
            //System.out.println(t.getName() + "号旅客开始准备吃饭!");
            try {
                TimeUnit.SECONDS.sleep(workTime);
                long start = System.currentTimeMillis();
                cyclicBarrier.await();
                long end = System.currentTimeMillis();
                System.out.println(t.getName() + "号旅客吃饭了,sleep:"+workTime+",等待耗时" + (end - start));
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e1) {
                e1.printStackTrace();
            }
        }

        private void travel(){
            Thread t = Thread.currentThread();
            //System.out.println(t.getName() + "号旅客开始准备吃饭!");
            try {
                TimeUnit.SECONDS.sleep(workTime);
                long start = System.currentTimeMillis();
                cyclicBarrier.await();
                long end = System.currentTimeMillis();
                System.out.println(t.getName() + "号旅客上车了,sleep:"+workTime+",等待耗时" + (end - start));
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e1) {
                e1.printStackTrace();
            }
        }
    }
}

输出:

人都到齐了,大家high起来!
驴友5号旅客吃饭了,sleep:5,等待耗时0
驴友0号旅客吃饭了,sleep:0,等待耗时5002
驴友1号旅客吃饭了,sleep:1,等待耗时4000
驴友4号旅客吃饭了,sleep:4,等待耗时1000
驴友3号旅客吃饭了,sleep:3,等待耗时2000
驴友2号旅客吃饭了,sleep:2,等待耗时3000
人都到齐了,大家high起来!
驴友5号旅客上车了,sleep:5,等待耗时0
驴友4号旅客上车了,sleep:4,等待耗时999
驴友3号旅客上车了,sleep:3,等待耗时2000
驴友2号旅客上车了,sleep:2,等待耗时3000
驴友1号旅客上车了,sleep:1,等待耗时3999
驴友0号旅客上车了,sleep:0,等待耗时5000

其中一个线程被interrupt()打断实例:

public class CyclicBarrierBreakTest {

    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(6);

    public static class T1 extends Thread {
        private int workTime;

        public T1(String name, int workTime) {
            super(name);
            this.workTime = workTime;
        }

        @Override
        public void run() {
            //等待人齐吃饭
            long start = 0, end = 0;
            Thread t = Thread.currentThread();
            try {
                TimeUnit.SECONDS.sleep(workTime);
                start = System.currentTimeMillis();
                System.out.println(t.getName() + "号旅客开始准备吃饭!");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            end = System.currentTimeMillis();
            System.out.println(t.getName() + "号旅客吃饭了,sleep:" + workTime + ",等待耗时" + (end - start));
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 6; i++) {
            int sleep = 0;
            //如果线程只是在睡眠过程时,中断的就不是cyclicBarrier.await();触发的,而是 TimeUnit.SECONDS.sleep(workTime);这时候就达不到效果
            T1 t = new T1("驴友" + i, sleep);
            t.start();
            if (i == 3) {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(t.getName() + ",有点急事,我先吃了!");
                t.interrupt();
                TimeUnit.SECONDS.sleep(2);
            }
        }
    }
}

输出:

驴友2号旅客开始准备吃饭!
驴友1号旅客开始准备吃饭!
驴友3号旅客开始准备吃饭!
驴友3,有点急事,我先吃了!
驴友3号旅客吃饭了,sleep:0,等待耗时1003
驴友2号旅客吃饭了,sleep:0,等待耗时1004
驴友1号旅客吃饭了,sleep:0,等待耗时1004
java.lang.InterruptedException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(A
java.util.concurrent.BrokenBarrierException
驴友4号旅客开始准备吃饭!
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
驴友5号旅客开始准备吃饭!
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
驴友6号旅客开始准备吃饭!
    at com.self.current.CyclicBarrierBreakTest$T1.run(CyclicBarrierBreakTest.java:38)
驴友4号旅客吃饭了,sleep:0,等待耗时0
java.util.concurrent.BrokenBarrierException
驴友6号旅客吃饭了,sleep:0,等待耗时0
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
驴友5号旅客吃饭了,sleep:0,等待耗时1
java.util.concurrent.BrokenBarrierException

结论:

  1. 内部有一个人把规则破坏了(接收到中断信号),其他人都不按规则来了,不会等待了
  2. 接收到中断信号的线程,await方法会触发InterruptedException异常,然后被唤醒向下运行
  3. 其他等待中 或者后面到达的线程,会在await()方法上触发BrokenBarrierException异常,然后继续执行

其中一个线程执行cyclicBarrier.await(2, TimeUnit.SECONDS);只执行超时等待2秒:

结论:

  1. 等待超时的方法

    public int await(long timeout, TimeUnit unit) throws InterruptedException,BrokenBarrierException,TimeoutException
    
  2. 内部有一个人把规则破坏了(等待超时),其他人都不按规则来了,不会等待了

  3. 等待超时的线程,await方法会触发TimeoutException异常,然后被唤醒向下运行

  4. 其他等待中或者后面到达的线程,会在await()方法上触发BrokenBarrierException异常,然后继续执行

重建规则示例:

第一次规则被打乱了,过了一会导游重建了规则(cyclicBarrier.reset();),接着又重来来了一次模拟等待吃饭的操作,正常了。

public class CyclicBarrierResetTest {

    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(6);

    private static boolean onOrder = false;

    public static class T1 extends Thread {
        private int workTime;

        public T1(String name, int workTime) {
            super(name);
            this.workTime = workTime;
        }

        @Override
        public void run() {
            //等待人齐吃饭
            long start = 0, end = 0;
            Thread t = Thread.currentThread();
            try {
                TimeUnit.SECONDS.sleep(workTime);
                start = System.currentTimeMillis();
                System.out.println(t.getName() + "号旅客开始准备吃饭!");
                if (!onOrder) {
                    if (this.getName().equals("驴友1")) {
                        cyclicBarrier.await(2, TimeUnit.SECONDS);
                    } else {
                        cyclicBarrier.await();
                    }
                } else {
                    cyclicBarrier.await();

                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            } catch (TimeoutException e){
                e.printStackTrace();
            }
            end = System.currentTimeMillis();
            System.out.println(t.getName() + "号旅客吃饭了,sleep:" + workTime + ",等待耗时" + (end - start));
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 6; i++) {
            T1 t = new T1("驴友" + i, i);
            t.start();
        }
        //等待7秒之后,重置,重建规则
        TimeUnit.SECONDS.sleep(7);
        cyclicBarrier.reset();
        onOrder = true;
        System.out.println("---------------重新按按规则来,不遵守规则的没饭吃!------------------");
        //再来一次
        for (int i = 1; i <= 6; i++) {
            T1 t = new T1("驴友" + i, i);
            t.start();
        }
    }
}

输出:

驴友1号旅客开始准备吃饭!
驴友2号旅客开始准备吃饭!
驴友3号旅客开始准备吃饭!
java.util.concurrent.BrokenBarrierException
驴友3号旅客吃饭了,sleep:3,等待耗时3
java.util.concurrent.TimeoutException
驴友1号旅客吃饭了,sleep:1,等待耗时2005
java.util.concurrent.BrokenBarrierException
驴友2号旅客吃饭了,sleep:2,等待耗时1006
java.util.concurrent.BrokenBarrierException
驴友4号旅客开始准备吃饭!
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
驴友4号旅客吃饭了,sleep:4,等待耗时0
驴友5号旅客开始准备吃饭!
驴友5号旅客吃饭了,sleep:5,等待耗时0
java.util.concurrent.BrokenBarrierException
java.util.concurrent.BrokenBarrierException
驴友6号旅客开始准备吃饭!
驴友6号旅客吃饭了,sleep:6,等待耗时0

---------------重新按按规则来,不遵守规则的没饭吃!------------------
驴友1号旅客开始准备吃饭!
驴友2号旅客开始准备吃饭!
驴友3号旅客开始准备吃饭!
驴友4号旅客开始准备吃饭!
驴友5号旅客开始准备吃饭!
驴友6号旅客开始准备吃饭!
驴友6号旅客吃饭了,sleep:6,等待耗时0
驴友5号旅客吃饭了,sleep:5,等待耗时1000
驴友4号旅客吃饭了,sleep:4,等待耗时2000
驴友3号旅客吃饭了,sleep:3,等待耗时3000
驴友2号旅客吃饭了,sleep:2,等待耗时3999
驴友1号旅客吃饭了,sleep:1,等待耗时5000

你可能感兴趣的:(Java高并发系列——检视阅读(四))