Java多线程-主线程等待子线程结束

一、前言

最近有个项目需求是线程池子线程进行数据处理、入库,所有子线程运行完成之后需要汇总子线程运行数据。这里简单介绍一下主线程等待子线程结束的集中方式

二、具体实现

2.1 join()方法

t.join()方法阻塞调用此方法的线程(calling thread),直到线程t完成,此线程再继续;通常用于在main()主线程内,等待其它线程完成再结束main()主线程。例如:

package com.yss.dataMiddle.utils;
import java.util.concurrent.TimeUnit;
/**
 - @description: join()方法实现主线程等待子线程结束
 - @author: Han LiDong
 - @create: 2020/9/18 15:30
 - @update: 2020/9/18 15:30
 */
public class ThreadJoinTest implements Runnable {
    private String name;
    public ThreadJoinTest(String name) {
        this.name = name;
    }
    public void run() {
        System.out.println(name + " is start");
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(name + " is end");
    }
    public static void main(String[] args) {
        Thread thread1 = new Thread(new ThreadJoinTest("One"));
        Thread thread2 = new Thread(new ThreadJoinTest("Two"));
        thread1.start();
        thread2.start();
        try {
            thread1.join();	//mian方法里面调用join()方法,主线程会阻塞,等待thread1线程完成
            thread2.join(); //mian方法里面调用join()方法,主线程会阻塞,等待thread2线程完成
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("Main thread is finished");
    }
}

执行结果如下,可以看出两个子线程执行结束之后,主线程才继续执行

One is start
Two is start
One is end
Two is end
Main thread is finished

2.2 CountDownLatch

CountDownLatch是一个同步辅助类,在jdk5中引入,它允许一个或多个线程等待其他线程操作完成之后才执行。

实现原理:
CountDownLatch是通过计数器的方式来实现,计数器的初始值为线程的数量。每当一个线程完成了自己的任务之后,就会对计数器减1,当计数器的值为0时,表示所有线程完成了任务,此时等待在闭锁上的线程才继续执行,从而达到等待其他线程完成任务之后才继续执行的目的。

CountDownLatch实践:
司机和工人,工人必须等到司机来了才能装货上车,司机必须得等到所有工人把货物装上车了之后才能把车开走。

public class Worker implements Runnable {
 
    private String workerCode;
 
    private CountDownLatch startLatch;
    private CountDownLatch latch;
 
    Worker(CountDownLatch startLatch, CountDownLatch latch, String workerCode) {
        this.startLatch = startLatch;
        this.latch = latch;
        this.workerCode = workerCode;
    }
 
    public void run() {
        try {
            startLatch.await();
            doWork();
            latch.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    private void doWork() {
        System.out.println("Worker " + workerCode + " is loading goods...");
    }
}

司机类:

public class Driver {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch startLatch = new CountDownLatch(1);
        CountDownLatch latch = new CountDownLatch(10);
        ExecutorService executor = Executors.newFixedThreadPool(10);
 
        for(int i=0; i<10; i++) {
            executor.execute(new Worker(startLatch, latch, "worker" + i));
        }
 
        System.out.println("Driver is here.");
 
        startLatch.countDown();
 
        System.out.println("Workers get to work.");
 
        latch.await();
 
        System.out.println("Driver is ready to go.");
 
        executor.shutdown();
    }
}  

运行结果:

Driver is here.
Workers get to work.
Worker worker0 is loading goods...
Worker worker1 is loading goods...
Worker worker2 is loading goods...
Worker worker3 is loading goods...
Worker worker4 is loading goods...
Worker worker5 is loading goods...
Worker worker7 is loading goods...
Worker worker9 is loading goods...
Worker worker8 is loading goods...
Worker worker6 is loading goods...
Driver is ready to go.

2.3 Future(有返回值)

Callable有返回值,所以子线程实现需要实现Callable接口
线程池的submit()的返回对象Future接口有一个get()方法也可以阻塞当前线程(该方法主要用途是获取子线程的返回值)

public static void main(String[] args) {
		//线程池线程数
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //存储线程的返回值,其中Map<String,Object> 为返回值类型
        List<Future<Map<String,Object>>> results = new LinkedList<Future<Map<String,Object>>>();
        for (int i = 0; i < 10; i++) {
            //调用自定义的线程实现方法并传递参数name
            TaskThread task = new TaskThread(name);
            //调用submit可以获得线程的返回值
            Future<Map<String,Object>> result = executorService.submit(task);
            results.add(result);
        }
        try {
            for (Future<Map<String,Object>> result : results){
            	//result.get()会阻塞线程直到当前子线程执行结束
                Map<String,Object> threadRes =  result.get();	
                if (threadRes != null){
                	//这里threadRes类型为TaskThread 里面定义的map类型
                	String resultStr =  threadRes.get("resultStr ")); 
                }
            }
        }catch (InterruptedException | ExecutionException e){
            e.printStackTrace();
        } finally{
        	executorService.shutdown();
        }
   }

具体线程实现:

/**
 * @description: 注意implements Callable<V> V就是返回值类型
 * @author: Han LiDong
 * @create: 2020/9/10 10:36
 * @update: 2020/9/10 10:36
 */
@Slf4j
public class TaskThread implements Callable<Map<String,Object>> {
    private String name;            
    /**
     * 比对完成返回主线程结果
     */
    private Map<String,Object> result = new HashMap<>();

    public TaskThread(){}
    //初始化线程对象 传参
    public TaskThread(String name) {
        this.name = name;
    }

    @Override
    public Map<String,Object> call() throws Exception {
        log.info(+"子线程"+name +"开始运行");
        try {
        	//开始执行子线程具体业务
           TimeUnit.SECONDS.sleep(4);
            /**
             * 拼接返回主线程参数
             */
            result.put("resultStr",name);
            return result;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

2.4 isAlive()方法

while循环轮询子线程状态

Thread t = new Thread(() -> {
    //子线程进行字符串连接操作
    int num = 1000;
    String s = "";
    for (int i = 0; i < num; i++) {
        s += "Java";
    }
    System.out.println("t Over");
});
//开始计时
long start = System.currentTimeMillis();
System.out.println("start = " + start);
t.start();
long end = 0;
while(t.isAlive() == true){//t.getState() != State.TERMINATED这两种判断方式都可以
    end = System.currentTimeMillis();
    try {
        Thread.sleep(10);	//防止while循环过多消耗cpu
    }catch (InterruptedException e){
        e.printStackTrace();
    }
}
System.out.println("end = " + end);
System.out.println("end - start = " + (end - start));

2.5 CyclicBarrier

参考博客:https://www.cnblogs.com/dolphin0520/p/3920397.html
CyclicBarrier:字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。

  • CyclicBarrier类位于java.util.concurrent包下,CyclicBarrier提供2个构造器:
//parties:让多少个线程或者任务等待至barrier状态;参数barrierAction为当这些线程都达到barrier状态时会执行的内容。
public CyclicBarrier(int parties, Runnable barrierAction) {
}
//parties:让多少个线程或者任务等待至barrier状态
public CyclicBarrier(int parties) {
}
  • CyclicBarrier中最重要的方法就是await方法,它有2个重载版本:
//挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务
public int await() throws InterruptedException, BrokenBarrierException { };
//线程等待至一定的时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务
public int await(long timeout, TimeUnit unit)throws InterruptedException,BrokenBarrierException,TimeoutException { };

下面举几个例子就明白了:

  • 假若有若干个线程都要进行写数据操作,并且只有所有线程都完成写数据操作之后,这些线程才能继续做后面的事情,此时就可以利用CyclicBarrier了:
public class Test {
    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N);
        for(int i=0;i<N;i++)
            new Writer(barrier).start();
    }
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
 
        @Override
        public void run() {
            System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
            try {
                Thread.sleep(5000);      //以睡眠来模拟写入数据操作
                System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println("所有线程写入完毕,继续处理其他任务...");
        }
    }
}

执行结果:

线程Thread-0正在写入数据...
线程Thread-3正在写入数据...
线程Thread-2正在写入数据...
线程Thread-1正在写入数据...
线程Thread-2写入数据完毕,等待其他线程写入完毕
线程Thread-0写入数据完毕,等待其他线程写入完毕
线程Thread-3写入数据完毕,等待其他线程写入完毕
线程Thread-1写入数据完毕,等待其他线程写入完毕
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...

从上面输出结果可以看出,每个写入线程执行完写数据操作之后,就在等待其他线程写入操作完毕。

当所有线程线程写入操作完毕之后,所有线程就继续进行后续的操作了。

  • 如果说想在所有线程写入操作完之后,进行额外的其他操作可以为CyclicBarrier提供Runnable参数:
public class Test {
    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N,new Runnable() {
            @Override
            public void run() {
                System.out.println("当前线程"+Thread.currentThread().getName());   
            }
        });
         
        for(int i=0;i<N;i++)
            new Writer(barrier).start();
    }
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
 
        @Override
        public void run() {
            System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
            try {
                Thread.sleep(5000);      //以睡眠来模拟写入数据操作
                System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println("所有线程写入完毕,继续处理其他任务...");
        }
    }
}

运行结果:

线程Thread-0正在写入数据...
线程Thread-1正在写入数据...
线程Thread-2正在写入数据...
线程Thread-3正在写入数据...
线程Thread-0写入数据完毕,等待其他线程写入完毕
线程Thread-1写入数据完毕,等待其他线程写入完毕
线程Thread-2写入数据完毕,等待其他线程写入完毕
线程Thread-3写入数据完毕,等待其他线程写入完毕
当前线程Thread-3
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...

从结果可以看出,当四个线程都到达barrier状态后,会从四个线程中选择一个线程去执行Runnable。

  • 下面看一下为await指定时间的效果:
public class Test {
    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N);
         
        for(int i=0;i<N;i++) {
            if(i<N-1)
                new Writer(barrier).start();
            else {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                new Writer(barrier).start();
            }
        }
    }
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
 
        @Override
        public void run() {
            System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
            try {
                Thread.sleep(5000);      //以睡眠来模拟写入数据操作
                System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
                try {
                    cyclicBarrier.await(2000, TimeUnit.MILLISECONDS);
                } catch (TimeoutException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"所有线程写入完毕,继续处理其他任务...");
        }
    }
}

执行结果:

线程Thread-0正在写入数据...
线程Thread-2正在写入数据...
线程Thread-1正在写入数据...
线程Thread-2写入数据完毕,等待其他线程写入完毕
线程Thread-0写入数据完毕,等待其他线程写入完毕
线程Thread-1写入数据完毕,等待其他线程写入完毕
线程Thread-3正在写入数据...
java.util.concurrent.TimeoutException
Thread-1所有线程写入完毕,继续处理其他任务...
Thread-0所有线程写入完毕,继续处理其他任务...
    at java.util.concurrent.CyclicBarrier.dowait(Unknown Source)
    at java.util.concurrent.CyclicBarrier.await(Unknown Source)
    at com.cxh.test1.Test$Writer.run(Test.java:58)
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(Unknown Source)
    at java.util.concurrent.CyclicBarrier.await(Unknown Source)
    at com.cxh.test1.Test$Writer.run(Test.java:58)
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(Unknown Source)
    at java.util.concurrent.CyclicBarrier.await(Unknown Source)
    at com.cxh.test1.Test$Writer.run(Test.java:58)
Thread-2所有线程写入完毕,继续处理其他任务...
java.util.concurrent.BrokenBarrierException
线程Thread-3写入数据完毕,等待其他线程写入完毕
    at java.util.concurrent.CyclicBarrier.dowait(Unknown Source)
    at java.util.concurrent.CyclicBarrier.await(Unknown Source)
    at com.cxh.test1.Test$Writer.run(Test.java:58)
Thread-3所有线程写入完毕,继续处理其他任务...

上面的代码在main方法的for循环中,故意让最后一个线程启动延迟,因为在前面三个线程都达到barrier之后,等待了指定的时间发现第四个线程还没有达到barrier,就抛出异常并继续执行后面的任务。

  • 另外CyclicBarrier是可以重用的,看下面这个例子:
public class Test {
    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N);
         
        for(int i=0;i<N;i++) {
            new Writer(barrier).start();
        }
         
        try {
            Thread.sleep(25000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
         
        System.out.println("CyclicBarrier重用");
         
        for(int i=0;i<N;i++) {
            new Writer(barrier).start();
        }
    }
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
 
        @Override
        public void run() {
            System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
            try {
                Thread.sleep(5000);      //以睡眠来模拟写入数据操作
                System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
             
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"所有线程写入完毕,继续处理其他任务...");
        }
    }
}

执行结果:

线程Thread-0正在写入数据...
线程Thread-1正在写入数据...
线程Thread-3正在写入数据...
线程Thread-2正在写入数据...
线程Thread-1写入数据完毕,等待其他线程写入完毕
线程Thread-3写入数据完毕,等待其他线程写入完毕
线程Thread-2写入数据完毕,等待其他线程写入完毕
线程Thread-0写入数据完毕,等待其他线程写入完毕
Thread-0所有线程写入完毕,继续处理其他任务...
Thread-3所有线程写入完毕,继续处理其他任务...
Thread-1所有线程写入完毕,继续处理其他任务...
Thread-2所有线程写入完毕,继续处理其他任务...
CyclicBarrier重用
线程Thread-4正在写入数据...
线程Thread-5正在写入数据...
线程Thread-6正在写入数据...
线程Thread-7正在写入数据...
线程Thread-7写入数据完毕,等待其他线程写入完毕
线程Thread-5写入数据完毕,等待其他线程写入完毕
线程Thread-6写入数据完毕,等待其他线程写入完毕
线程Thread-4写入数据完毕,等待其他线程写入完毕
Thread-4所有线程写入完毕,继续处理其他任务...
Thread-5所有线程写入完毕,继续处理其他任务...
Thread-6所有线程写入完毕,继续处理其他任务...
Thread-7所有线程写入完毕,继续处理其他任务...

从执行结果可以看出,在初次的4个线程越过barrier状态后,又可以用来进行新一轮的使用。而CountDownLatch无法进行重复使用。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值