在JDK中的java.util.concurrent
包里提供了几个实用的并发工具类,下面我们一起来了解下。
CountDownLatch
功能: CountDownLatch允许一个或多个线程等待其他线程完成操作
我们来看一个查询航班的例子,用户访问APP,查询北京到上海的航班信息,后台需要同时调用三家航空公司提供的API查询数据,最后将最终查询结果汇总后返回。
示例代码如下:
public class FlightQueryDemo {
private static List<String> company = Arrays.asList("中国国际航空", "东方航空", "海南航空");
private static List<String> flightList = new ArrayList<>();
public static void main(String[] args) {
String origin = "BJ";
String dest = "SH";
Thread[] th = new Thread[company.size()];
CountDownLatch cdl = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
String name = company.get(i);
th[i] = new Thread(() -> {
System.out.printf("%s 查询从%s到%s的机票信息\n", name, origin, dest);
// 随机产生票数
int val = new Random().nextInt(10);
try {
TimeUnit.SECONDS.sleep(val);
} catch (InterruptedException e) {
e.printStackTrace();
}
flightList.add(name + "----" + val);
System.out.printf("%s公司查询成功!\n", name);
cdl.countDown();
});
th[i].start();
}
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("==============查询结果如下:================");
flightList.forEach(System.out::println);
}
}
CountDownLatch的构造函数接收一个int类型的参数作为计数器,调用CountDownLatch的countDown()
方法时,计数器就会减一,CountDownLatch的await()
方法会阻塞当前线程,直到计数器变为零。
注意:计数器必须大于等于0,另外CountDownLatch不可能重新初始化或者修改CountDownLatch对象的内部计数器的值。
CyclicBarrier
功能: 让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
我们来看一个运动员赛跑的例子,所有运动员准备好后一起开始起跑。
示例代码如下:
public class RaceDemo {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(8);
Thread[] sportsman = new Thread[8];
for (int i = 0; i < 8; i++) {
sportsman[i] = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(new Random().nextInt(10));
System.out.println(Thread.currentThread().getName() + "准备好了");
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("选手" + Thread.currentThread().getName() + "起跑");
}, "paly[" + i + "]");
sportsman[i].start();
}
}
}
CyclicBarrier默认的构造方法是CyclicBarrier(int parties)
,其参数表示屏障拦截的线程数量,每个线程调用await()
方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
CyclicBarrier还有一个构造函数CyclicBarrier(int parties, Runnable barrierAction)
,用于在线程到达屏障时,优先执行barrierAction,这里不再演示。
CyclicBarrier和CountDownLatch的区别
CountDownLatch的计数器只能使用一次,二CyclicBarrier的计数器可以使用reset()
方法重置。所以CyclicBarrier能处理更为复杂的业务场景。例如,如果执行过程中发生问题,可以重置计数器,并让线程重新执行一次。
Semaphore
功能: Semaphore是用来控制同时访问特定资源的线程数量,它通过协调各个线程,比保证合理的使用公共资源。
我们来看一个停车场停车的例子,停车场停满后,其它车就需要在停车场外等待,直到有车从停车场离开,空出位置,外面的车子才可以进来。
示例代码如下:
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(5);
Thread[] car = new Thread[10];
for (int i = 0; i < 10; i++) {
car[i] = new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "可以进停车场");
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
TimeUnit.SECONDS.sleep(new Random().nextInt(10));
// System.out.println(Thread.currentThread().getName() + "停车时间:" + new Random().nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
semaphore.release();
System.out.println(Thread.currentThread().getName() + "离开停车场");
}, "car[" + i + "]");
car[i].start();
}
}
}
Exchanger
功能: Exchanger是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。
我们来看一个校对工作的例子,例如:银行将纸质的资金流水通过人工的方式录入成电子银行流水,为了避免错误,采用AB两人同时进行录入,录入完成后需要对这两人录入的结果进行比较,看看是否录入一致。
示例代码如下:
public class ExchangerDemo {
private static final Exchanger<String> exgr = new Exchanger<String>();
private static ExecutorService pool = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
pool.execute(new Runnable() {
@Override
public void run() {
try {
String A = "统计结果A";
exgr.exchange(A);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
pool.execute(new Runnable() {
@Override
public void run() {
try {
String B = "统计结果B";
String A = exgr.exchange(B);
System.out.println("A和B的数据是否一致:" + A.equals(B) + ", A录入的是:" + A + ", B录入的是:" + B);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
如果两个线程有一个没有执行exchanger()
方法,则会一直等待。为了避免一直等待,可以使用exchange(V x, long timeout, TimeUnit unit)
设置最大等待时长。
总结
本文一共介绍了四个常用的并发工具类,每个都有其特定的使用场景,后面如果遇到类似的场景问题不妨试一试这些工具类。
参考:
Java并发编程的艺术 方腾飞 魏鹏 程晓明 著