【多线程】学习记录七种主线程等待子线程结束之后在执行的方法

最近遇到一个问题需要主线程等待所有的子线程结束,才能开始执行,统计所有的子线程执行结果,返回,网上翻阅各种资料,最后记录一下,找到七种方案

第一种:while循环

对于“等待所有的子线程结束”的问题,最开始想到的是使用while循环进行轮询:

        //开始计时
        String start = getTheTimeInMilliseconds();
        System.out.println("start = " + start);
        Thread t = new Thread(() -> {
            //子线程进行字符串连接操作
            int num = 1000;
            String s = "";
            for (int i = 0; i < num; i++) {
                s += "Java" + i;
            }
            System.out.println("t Over s =" + s);
        });
        t.start();
        String end = "";
        //t.getState() != State.TERMINATED这两种判断方式都可以
        while(t.isAlive() == true){
            end = getTheTimeInMilliseconds();
        }
        System.out.println("end = " + end);

但是这样太消耗CPU,所以我在while循环里加入了暂停,让其歇会:

        while(t.isAlive() == true){
            end = System.currentTimeMillis();
            try {
                Thread.sleep(10);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }

这样做的结果虽然cpu消耗减少,但是数据不准确了

第二种:Thread的join()方法

将 方法1 中的while循环代码更改如下

        try {
            t.join();//注意这里
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

使用join()方法,join()方法的作用,是等待这个线程结束;(t.join()方法阻塞调用此方法的线程,直到线程t完成,此线程再继续)

第三种:synchronized 的等待唤醒机制

第二种方法的确实现了,接着我又想到了多线程的等待唤醒机制,思路是:子线程启动后主线程等待,子线程结束后唤醒主线程。于是有了下面的代码:

        //开始计时
        String start = getTheTimeInMilliseconds();
        System.out.println("start = " + start);
        Object lock = new Object();
        Thread t = new Thread(() -> {
            //子线程进行字符串连接操作
            int num = 1000;
            String s = "";
            for (int i = 0; i < num; i++) {
                s += "Java" + i;
            }
            System.out.println("t Over s =" + s);
            lock.notify();
        });
        t.start();
        try {
            lock.wait();//主线程等待
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String end = getTheTimeInMilliseconds();
        System.out.println("end = " + end);

但是这样运行结果会抛出两个异常:

由于对wait()和notify()的理解并不是很深刻,所以我最开始并不清楚为什么会出现这样的结果,因为从报错顺序来看子线程并没有提前唤醒,于是我到处翻阅资料,最后得出的结论是调用wait()方法时需要获取该对象的锁,Object文档里是这么说的:

当前线程必须拥有该对象的监视器。如果当前线程不是对象监视器的所有者,抛异常IllegalMonitorStateException。

所以上面的代码需要改成这样:

        //开始计时
        String start = getTheTimeInMilliseconds();
        System.out.println("start = " + start);
        Object lock = new Object();
        Thread t = new Thread(() -> {
            //子线程进行字符串连接操作
            int num = 1000;
            String s = "";
            for (int i = 0; i < num; i++) {
                s += "Java" + i;
            }
            System.out.println("t Over s =" + s);
            synchronized (lock) {//获取对象锁
                lock.notify();//子线程唤醒
            }
        });
        t.start();
        try {
            synchronized (lock) {//这里也是一样
                lock.wait();//主线程等待
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String end = getTheTimeInMilliseconds();
        System.out.println("end = " + end);

这样的确得出了结果,但是主线程有可能先wait子线程,在notify,也就是说,如果子线程在主线程wait前,调用了notify,会导致主线程无限等待,所以这个思路还是有一定漏洞的

第四种:CountDownLatch

第四种方式可以等待多个线程结束,就是使用java.util.concurrent包下的CountDownLatch类

简单来说,CountDownLatch类是一个计数器,可以设置初始线程数(设置后不能改变),在子线程结束时调用countDown()方法可以使线程数减一,最终为0的时候,调用CountDownLatch的成员方法wait()的线程就会取消BLOKED阻塞状态,进入RUNNABLE从而继续执行。下面上代码:

        //开始计时
        String start = getTheTimeInMilliseconds();
        System.out.println("start = " + start);
        int threadNumber = 1;
        //参数为线程个数
        final CountDownLatch cdl = new CountDownLatch(threadNumber);
        Thread t = new Thread(() -> {
            //子线程进行字符串连接操作
            int num = 1000;
            String s = "";
            for (int i = 0; i < num; i++) {
                s += "Java" + i;
            }
            System.out.println("t Over s =" + s);
            //此方法是CountDownLatch的线程数-1
            cdl.countDown();
        });
        t.start();
        try {
            //需要捕获异常,当其中线程数为0时这里才会继续运行
            cdl.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String end = getTheTimeInMilliseconds();
        System.out.println("end = " + end);

第五种:Future

又想到线程池,线程池的submit()的返回对象Future接口有一个get()方法也可以阻塞当前线程(其实该方法主要用途是获取子线程的返回值),所以第五种方法也出来了:

        //开始计时
        String start = getTheTimeInMilliseconds();
        System.out.println("start = " + start);
        ExecutorService executorService = Executors.newFixedThreadPool(1);

        Thread t = new Thread(() -> {
            //子线程进行字符串连接操作
            int num = 1000;
            String s = "";
            for (int i = 0; i < num; i++) {
                s += "Java" + i;
            }
            System.out.println("t Over s =" + s);
        });
        //子线程启动
        Future future = executorService.submit(t);
        try {
            future.get();//需要捕获两种异常
        }catch (InterruptedException e){
            e.printStackTrace();
        }catch (ExecutionException e){
            e.printStackTrace();
        }
        String end = getTheTimeInMilliseconds();
        System.out.println("end = " + end);
        executorService.shutdown();

这里, ThreadPoolExecutor 是实现了 ExecutorService的方法, sumbit的过程就是把一个Runnable接口对象包装成一个 Callable接口对象, 然后放到 workQueue里等待调度执行. 当然, 执行的启动也是调用了thread的start来做到的, 只不过这里被包装掉了. 另外, 这里的thread是会被重复利用的, 所以这里要退出主线程, 需要执行以下shutdown方法以示退出使用线程池. 扯远了.

这种方法是得益于Callable接口和Future模式, 调用future接口的get方法, 会同步等待该future执行结束, 然后获取到结果. Callbale接口的接口方法是 V call(); 是可以有返回结果的, 而Runnable的 void run(), 是没有返回结果的. 所以, 这里即使被包装成Callbale接口, future.get返回的结果也是null的.如果需要得到返回结果, 建议使用Callable接口.

第六种:BlockingQueue

同时,在concurrent包中,还提供了BlockingQueue(队列)来操作线程,BlockingQueue的主要的用法是在线程间安全有效的传递数据,因此,第六种方法也出来了:

        //开始计时
        String start = getTheTimeInMilliseconds();
        System.out.println("start = " + start);
        //数组型队列,长度为1
        BlockingQueue queue = new ArrayBlockingQueue(1);
        Thread t = new Thread(() -> {
            //子线程进行字符串连接操作
            int num = 1000;
            String s = "";
            for (int i = 0; i < num; i++) {
                s += "Java" + i;
            }
            System.out.println("t Over s =" + s);
            try {
                //在队列中加入数据
                queue.put("OK");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t.start();
        try {
            queue.take();//主线程在队列中获取数据,take()方法会阻塞队列,ps还有不会阻塞的方法
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String end = getTheTimeInMilliseconds();
        System.out.println("end = " + end);

第七种:CyclicBarrier

第七种方式,还是concurrent包,只不过这次试用CyclicBarrier类:

CyclicBarrier字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。

        //开始计时
        String start = getTheTimeInMilliseconds();
        System.out.println("start = " + start);
        //参数为线程数
        CyclicBarrier barrier = new CyclicBarrier(2);
        Thread t = new Thread(() -> {
            //子线程进行字符串连接操作
            int num = 1000;
            String s = "";
            for (int i = 0; i < num; i++) {
                s += "Java" + i;
            }
            System.out.println("t Over s =" + s);
            try {
                //阻塞
                barrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        });
        t.start();
        try {
            barrier.await();//也阻塞,并且当阻塞数量达到指定数目时同时释放
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
        String end = getTheTimeInMilliseconds();
        System.out.println("end = " + end);

实际是上面这种方法是不太严谨的,因为在子线程阻塞之后如果还有代码是会继续执行的,当然本例中后面是没有代码可执行了,可以近似理解为是子线程的运行时间。

问题

扒拉出这么方法都可以实现主线程等待子线程的方法,上个问题:

几万,几十万的数据情况下,只能一条信息一条信息发送,需要优化消费时间

保证每条数据都被消费掉,并且统计所有失败的记录,失败的原因

小弟综合实际写的代码,上demo,求各路大佬指教代码中的缺陷,因为这块不熟,面向百度编程使用的事淋漓尽致,所以老感觉有坑,但是又不知道在哪,贼尴尬

    /**
    * 业务代码精简版
    */
    private static AtomicInteger atomicInteger = new AtomicInteger(0);
    private static final CountDownLatch latch = new CountDownLatch(100);
    private static ExecutorService pool = new ThreadPoolExecutor(2, 4,
            1000, TimeUnit.SECONDS,
            new LinkedBlockingDeque<Runnable>(1000),
            new ThreadPoolExecutor.DiscardOldestPolicy());

    public static void main(String[] args) {
        System.out.println("主线程开始执行…… ……");
        // 需要统计每个数据的消费结果
        List<Map<String, Integer>> resultMap = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            pool.execute(() -> {
                try {
                    synchronized (TestThread.class){
                        Map<String, Integer> map = new Hashtable<>();
                        // 假装获取了每个数据消费结果
                        map.put("success", 0);
                        resultMap.add(map);
                        atomicInteger.getAndIncrement();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            });
        }
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("成功数量" + atomicInteger.get());
        System.out.println(resultMap);
    }
  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值