Java 并发基础(二):主线程等待子线程结束

你好,我是看山。

在很多时候,我们期望实现这么一种功能:在主线程中启动一些子线程,等待所有子线程执行结束后,主线程再继续执行。比如:老板分配任务,众多工人开始工作,等所有工人完成工作后,老板进行检查。

解决方法分析:

  1. 主线程通过 join 等待所有子线程完成后,继续执行;
  2. 主线程知道子线程的数量、未完成子线程数量,主线程等待所有子线程完成后,才继续执行。

通过 join 实现

第一种方式,可以直接调用 Java API 中关于线程的 join 方法等待该线程终止,可以直接实现。

每个工人都是一个线程,其 run 方法是工作的实现,每个工人通过名字进行区分,具体代码如下:

import java.util.Random;
import java.util.concurrent.TimeUnit;

public class Worker extends Thread {
    private String workerName;

    public Worker(String workerName) {
        this.workerName = workerName;
    }

    /**
        * @see java.lang.Thread#run()
        */
    @Override
    public void run() {
        System.out.println(this.workerName + "正在干活。..");
        try {
            TimeUnit.SECONDS.sleep(new Random().nextInt(10));
        } catch (InterruptedException e) {
        }
        System.out.println(this.workerName + "活干完了!");
    }

    public String getWorkerName() {
        return workerName;
    }
}

老板招收工人,然后安排工人工作,之后等待工人工作完,检查工人的工作成果(资本家啊。),具体代码如下:

import java.util.List;

public class Boss {
    private List<Worker> workers;

    public Boss(List<Worker> workers) {
        System.out.println("老板招收工人。");
        this.workers = workers;
    }

    public void work() {
        System.out.println("老板开始安排工人工作。..");
        for (Worker worker : workers) {
            System.out.println("老板安排" + worker.getWorkerName() + "的工作");
            worker.start();
        }
        System.out.println("老板安排工作结束。..");

        System.out.println("老板正在等所有的工人干完活。.....");
        for (Worker w : workers) {
            try {
                w.join();
            } catch (InterruptedException e) {
            }
        }
        System.out.println("工人活都干完了,老板开始检查了!");
    }
}

现在写 main 方法进行测试:

import java.util.ArrayList;
import java.util.List;

public class JoinDemo {
    public static void main(String[] args) {
        Worker w1 = new Worker("张三");
        Worker w2 = new Worker("李四");
        Worker w3 = new Worker("王五");

        List<Worker> workers = new ArrayList<Worker>();
        workers.add(w1);
        workers.add(w2);
        workers.add(w3);

        Boss boss = new Boss(workers);
        boss.work();

        System.out.println("main 方法结束");
    }
}

执行结果为:

老板招收工人。
老板开始安排工人工作。..
老板安排张三的工作
老板安排李四的工作
张三正在干活。..
老板安排王五的工作
李四正在干活。..
老板安排工作结束。..
老板正在等所有的工人干完活。.....
王五正在干活。..
王五活干完了!
张三活干完了!
李四活干完了!
工人活都干完了,老板开始检查了!
main 方法结束

通过 CountDownLatch 实现

第二种方式可以自己实现一种计数器,用于统计子线程总数、未完成线程数,当未完成线程数大约 0,主线程等待;当未完成线程数等于 0,主线程继续执行。

当然,既然我们现在想到这种方式,Java API 的团队当然也会想到,JDK 1.5 提供了 CountDownLatch 用于实现上述方法。

于是对上述的工人方法进行修改:

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class Worker extends Thread {
    private CountDownLatch downLatch;
    private String workerName;

    public Worker(CountDownLatch downLatch, String workerName) {
        this.downLatch = downLatch;
        this.workerName = workerName;
    }

    public void run() {
        System.out.println(this.workerName + "正在干活。..");
        try {
            TimeUnit.SECONDS.sleep(new Random().nextInt(10));
        } catch (InterruptedException ie) {
        }
        System.out.println(this.workerName + "活干完了!");
        this.downLatch.countDown();
    }

    public String getWorkerName() {
        return workerName;
    }
}

latch.countDown(),是用于在子线程执行结束后计数器减一,即未完成子线程数减一。

老板类也得做出相应的修改:

import java.util.List;
import java.util.concurrent.CountDownLatch;

public class Boss {
    private List<Worker> workers;
    private CountDownLatch downLatch;

    public Boss(List<Worker> workers, CountDownLatch downLatch) {
        this.workers = workers;
        this.downLatch = downLatch;
    }

    public void work() {
        System.out.println("老板开始安排工人工作。..");
        for (Worker worker : workers) {
            System.out.println("老板安排" + worker.getWorkerName() + "的工作");
            worker.start();
        }
        System.out.println("老板安排工作结束。..");

        System.out.println("老板正在等所有的工人干完活。.....");
        try {
            this.downLatch.await();
        } catch (InterruptedException e) {
        }
        System.out.println("工人活都干完了,老板开始检查了!");
    }

}

latch.await(),是等待子线程结束。

编写 main 方法进行验证:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(3);

        Worker w1 = new Worker(latch, "张三");
        Worker w2 = new Worker(latch, "李四");
        Worker w3 = new Worker(latch, "王五");

        List<Worker> workers = new ArrayList<Worker>();
        workers.add(w1);
        workers.add(w2);
        workers.add(w3);

        Boss boss = new Boss(workers, latch);

        boss.work();

        System.out.println("main 方法结束");
    }
}

执行结果为:

    老板开始安排工人工作。..
    老板安排张三的工作
    老板安排李四的工作
    张三正在干活。..
    老板安排王五的工作
    李四正在干活。..
    老板安排工作结束。..
    老板正在等所有的工人干完活。.....
    王五正在干活。..
    王五活干完了!
    李四活干完了!
    张三活干完了!
    工人活都干完了,老板开始检查了!
    main 方法结束

使用循环栅栏 CyclicBarrier 实现

还有一种实现,这种方式不会阻塞主线程,但是会监听所有子线程结束。此处在上述的工人老板的场景中使用的话,代码如下:

工人类:

import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;

public class Worker extends Thread {
    private String workerName;
    private CyclicBarrier barrier;

    public Worker(String workerName, CyclicBarrier barrier) {
        this.workerName = workerName;
        this.barrier = barrier;
    }

    @Override
    public void run() {
        System.out.println(this.workerName + "正在干活。..");
        try {
            TimeUnit.SECONDS.sleep(new Random().nextInt(10));
        } catch (InterruptedException e) {
        }
        System.out.println(this.workerName + "活干完了!");

        try {
            barrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }

    public String getWorkerName() {
        return workerName;
    }
}

老板类:

import java.util.List;

public class Boss {
    private List<Worker> workers;

    public Boss(List<Worker> workers) {
        this.workers = workers;
    }

    public void work() {
        System.out.println("老板开始安排工人工作。..");
        for (Worker worker : workers) {
            System.out.println("老板安排" + worker.getWorkerName() + "的工作");
            worker.start();
        }
        System.out.println("老板安排工作结束。..");

        System.out.println("老板正在等所有的工人干完活。.....");
    }

}

main 方法测试:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(3, new Runnable() {
            @Override
            public void run() {
                System.out.println("工人活都干完了,老板开始检查了!");
            }
        });

        Worker w1 = new Worker("张三", barrier);
        Worker w2 = new Worker("李四", barrier);
        Worker w3 = new Worker("王五", barrier);

        List<Worker> workers = new ArrayList<Worker>();
        workers.add(w1);
        workers.add(w2);
        workers.add(w3);

        Boss boss = new Boss(workers);
        boss.work();

        System.out.println("main 方法结束");
    }
}

执行结果为:

老板开始安排工人工作。..
老板安排张三的工作
老板安排李四的工作
张三正在干活。..
老板安排王五的工作
李四正在干活。..
老板安排工作结束。..
老板正在等所有的工人干完活。.....
王五正在干活。..
main 方法结束
李四活干完了!
王五活干完了!
张三活干完了!
工人活都干完了,老板开始检查了!

通过结果分析可以很清楚的看出,boss 对象的 work 方法执行结束后,main 方法即开始执行。(此处“老板正在等所有的工人干完活。…..”打印之后是“王五正在干活。..”,然后才是“main 方法结束”,是因为对于 cpu 的抢占,甚至有一定的概率是“main 方法结束”会在最后打印。)

假设有这么一个功能,我们需要向数据库批量写入一些记录,然后记录这个操作使用的时间,但是我们又不想影响其他操作(即不想阻塞主线程),这个时候 CyclicBarrier 就派上用场了。

推荐阅读


你好,我是看山,公众号:看山的小屋,10 年老猿,开源贡献者。游于码界,戏享人生。

个人主页:https://www.howardliu.cn
个人博文:Java 并发基础(二):主线程等待子线程结束
CSDN 主页:http://blog.csdn.net/liuxinghao
CSDN 博文:Java 并发基础(二):主线程等待子线程结束

公众号:看山的小屋