java常见面试考点
往期文章推荐:
java常见面试考点(三十一):连接池的作用
java常见面试考点(三十二):诊断生产环境服务器变慢
java常见面试考点(三十三):常用的JVM监控和性能分析工具
java常见面试考点(三十四):Github骚操作
java常见面试考点(三十五):UDP和TCP的区别及应用场景
【版权申明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权);
本博客的内容来自于:java常见面试考点(三十六):CountDownLatch和CyclicBarrier的区别;
学习、合作与交流联系q384660495;
本博客的内容仅供学习与参考,并非营利;
一、CountDownLatch用法
CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。
public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
//lambda中只能只用final的变量
final int times = i;
new Thread(() -> {
try {
System.out.println("子线程" + Thread.currentThread().getName() + "正在赶路");
Thread.sleep(1000 * times);
System.out.println("子线程" + Thread.currentThread().getName() + "到公司了");
//调用latch的countDown方法使计数器-1
latch.countDown();
System.out.println("子线程" + Thread.currentThread().getName() + "开始工作");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
try {
System.out.println("门卫等待员工上班中...");
//主线程阻塞等待计数器归零
latch.await();
System.out.println("员工都来了,门卫去休息了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
实验结果如下:
子线程Thread-0正在赶路
子线程Thread-2正在赶路
子线程Thread-0到公司了
子线程Thread-0开始工作
子线程Thread-1正在赶路
门卫等待员工上班中...
子线程Thread-4正在赶路
子线程Thread-9正在赶路
子线程Thread-5正在赶路
子线程Thread-6正在赶路
子线程Thread-7正在赶路
子线程Thread-8正在赶路
子线程Thread-3正在赶路
子线程Thread-1到公司了
子线程Thread-1开始工作
子线程Thread-2到公司了
子线程Thread-2开始工作
子线程Thread-3到公司了
子线程Thread-3开始工作
子线程Thread-4到公司了
子线程Thread-4开始工作
子线程Thread-5到公司了
子线程Thread-5开始工作
子线程Thread-6到公司了
子线程Thread-6开始工作
子线程Thread-7到公司了
子线程Thread-7开始工作
子线程Thread-8到公司了
子线程Thread-8开始工作
子线程Thread-9到公司了
子线程Thread-9开始工作
员工都来了,门卫去休息了
可以看到子线程并没有因为调用latch.countDown而阻塞,会继续进行该做的工作,只是通知计数器-1,即完成了我们如上说的场景,只需要在所有进程都进行到某一节点后才会执行被阻塞的进程.如果我们想要多个线程在同一时间进行就要用到CyclicBarrier了
二、CyclicBarrier用法
字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。
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是可以重用的,而CountDownLatch无法进行重复使用。
三、Semaphore用法
Semaphore翻译成字面意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
public class Test {
public static void main(String[] args) {
int N = 8; //工人数
Semaphore semaphore = new Semaphore(5); //机器数目
for(int i=0;i<N;i++)
new Worker(i,semaphore).start();
}
static class Worker extends Thread{
private int num;
private Semaphore semaphore;
public Worker(int num,Semaphore semaphore){
this.num = num;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println("工人"+this.num+"占用一个机器在生产...");
Thread.sleep(2000);
System.out.println("工人"+this.num+"释放出机器");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
执行结果:
工人0占用一个机器在生产...
工人1占用一个机器在生产...
工人2占用一个机器在生产...
工人4占用一个机器在生产...
工人5占用一个机器在生产...
工人0释放出机器
工人2释放出机器
工人3占用一个机器在生产...
工人7占用一个机器在生产...
工人4释放出机器
工人5释放出机器
工人1释放出机器
工人6占用一个机器在生产...
工人3释放出机器
工人7释放出机器
工人6释放出机器
案例
如我们平时开发中典型的数据库操作,这是一个密集IO 操作,我们可以启动很多线程但是数据库的连接池是有限制的,假设我们设置允许五个链接,如果我们开启太多线程直接操作则会出现异常,这时候我们可以通过信号量来控制,让一直最多只有五个线程来获取连接。代码如下:
/*
Semaphore 是信号量, 可以用来控制线程的并发数,可以协调各个线程,以达到合理的使用公共资源
*/
public static void main(String[] args) {
//创建10个容量的线程池
final ExecutorService service = Executors.newFixedThreadPool(100);
//设置信号量的值5 ,也就是允许五个线程来执行
Semaphore s = new Semaphore(5);
for (int i = 0; i < 100; i++) {
service.submit(() ->{
try {
s.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println("数据库耗时操作"+Thread.currentThread().getName());
TimeUnit.MILLISECONDS.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在执行....");
s.release();
});
}
}
如上代码,创建了一个容量100的线程池,模拟我们程序中大量的线程,添加一百个任务,让线程池执行。创建了一个容量为5的信号量,在线程中我们调用 acquire 来获得信号量的许可,只有获得了才能只能下面的内容不然阻塞。当执行完后释放该许可,通过 release 方法,
通过上面的演示,有没有觉得非常眼熟,对,就是和我们之前接触过的锁很相似,只是锁是只允许一个线程访问,那我们能不能将信号量的容量设置为1呢? 这当然是可以的,当我们设置为1时其实就和我们的锁的功能是一致的。
下面对上面说的三个辅助类进行一个总结:
1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:
CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。
2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。
四、Exchanger用法
Exchanger(交换者)是一个用于线程间协作的工具类,它主要用于进行线程间数据的交换,它有一个同步点,当两个线程到达同步点时可以将各自的数据传给对方,如果一个线程先到达同步点则会等待另一个到达同步点,到达同步点后调用 exchange 方法可以传递自己的数据并且获得对方的数据。
我们假设现在需要录入一些重要的账单信息,为了保证准备,让两个人分别录入,之后再进行对比后是否一致,防止错误繁盛。下面通过代码来演示:
public class ExchangerTest {
/*
Exchanger 交换, 用于线程间协作的工具类,可以交换线程间的数据,
其提供一个同步点,当线程到达这个同步点后进行数据间的交互,遗传算法可以如此来实现,
以及校对工作也可以如此来实现
*/
public static void main(String[] args) {
/*
模拟 两个工作人员录入记录,为了防止错误,两者录的相同内容,程序仅从校对,看是否有错误不一致的
*/
//开辟两个容量的线程池
final ExecutorService service = Executors.newFixedThreadPool(2);
Exchanger<InfoMsg> exchanger = new Exchanger<>();
service.submit(() ->{
//模拟数据 线程 A的
InfoMsg infoMsg = new InfoMsg();
infoMsg.content="这是线程A";
infoMsg.id ="10001";
infoMsg.desc = "1";
infoMsg.message = "message";
System.out.println("正在执行其他...");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
final InfoMsg exchange = exchanger.exchange(infoMsg);
System.out.println("线程A 交换数据====== 得到"+ exchange);
if (!exchange.equals(infoMsg)){
System.out.println("数据不一致~~请稽核");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
service.submit(() ->{
//模拟数据 线程 B的
InfoMsg infoMsg = new InfoMsg();
infoMsg.content="这是线程B";
infoMsg.id ="10001";
infoMsg.desc = "1";
infoMsg.message = "message";
System.out.println("正在执行其他...");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
final InfoMsg exchange = exchanger.exchange(infoMsg);
System.out.println("线程B 交换数据====== 得到"+ exchange);
if (!exchange.equals(infoMsg)){
System.out.println("数据不一致~~请稽核");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
service.shutdown();
}
static class InfoMsg{
String id;
String name;
String message;
String content;
String desc;
@Override
public String toString() {
return "InfoMsg{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", message='" + message + '\'' +
", content='" + content + '\'' +
", desc='" + desc + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
InfoMsg infoMsg = (InfoMsg) o;
return Objects.equals(id, infoMsg.id) &&
Objects.equals(name, infoMsg.name) &&
Objects.equals(message, infoMsg.message) &&
Objects.equals(content, infoMsg.content) &&
Objects.equals(desc, infoMsg.desc);
}
@Override
public int hashCode() {
return Objects.hash(id, name, message, content, desc);
}
}
}
运行结果如下:
上面代码运行可以看到,当我们线程 A/B 到达同步点即调用 exchange 后进行数据的交换,拿到对方的数据再与自己的数据对比可以做到稽核 的效果
Exchanger 同样可以用于遗传算法中,选出两个对象进行交互两个的数据通过交叉规则得到两个混淆的结果。
Exchanger 中嗨提供了一个方法 public V exchange(V x, long timeout, TimeUnit unit) 主要是用来防止两个程序中一个一直没有执行 exchange 而导致另一个一直陷入等待状态,这是可以用这个方法,设置超时时间,超过这个时间则不再等待。
五、参考资料
Java并发编程:CountDownLatch、CyclicBarrier和Semaphore
CountdownLatch和CyclicBarrier的区别使用场景与具体实现
CountDownLatch、CyclicBarrier、Semaphore、Exchanger 的详细解析