面试官跟我扯了半小时 CountDownLatch 后,给我发 Offer?| 原力计划

百家 作者:CSDN 2020-05-24 21:30:33

作者 | 万猫学社
责编 | 王晓曼
出品 | CSDN博客
一个长头发、穿着清爽的小姐姐,拿着一个崭新的Mac笔记本向我走来,看着来势汹汹,我心想着肯定是技术大佬吧!但是我也是一个才华横溢的人,稳住我们能赢。

面试官:看你简历上有写熟悉并发编程,CountDownLatch一定用过吧,跟我说说它!
:CountDownLatch是JDK提供的一个同步工具,它可以让一个或多个线程等待,一直等到其他线程中执行完成一组操作。
面试官:CountDownLatch有哪些常用的方法?
:有CountDown方法和Await方法,CountDownLatch在初始化时,需要指定用给定一个整数作为计数器。当调用CountDown方法时,计数器会被减1;当调用Await方法时,如果计数器大于0时,线程会被阻塞,一直到计数器被CountDown方法减到0时,线程才会继续执行。计数器是无法重置的,当计数器被减到0时,调用Await方法都会直接返回。
面试官调用CountDown方法时,线程也会阻塞嘛?
:不会的,调用CountDown的线程可以继续执行,不需要等待计数器被减到0,只是调用Await方法的线程需要等待。
面试官:可以举一个使用CountDownLatch的例子吗?
:比如张三、李四和王五几个人约好去饭店一起去吃饭,这几个人都是比较绅士,要等到所有人都到齐以后才让服务员上菜。这种场景就可以用到CountDownLatch。
面试官:可以写一下吗?
我:当然可以,这是张三、李四和王五的顾客类:
package?onemore.study;

import?java.text.SimpleDateFormat;
import?java.util.Date;
import?java.util.Random;
import?java.util.concurrent.CountDownLatch;

public?class?Customer?implements?Runnable?{
????private?CountDownLatch?latch;
????private?String?name;

????public?Customer(CountDownLatch?latch,?String?name)?{
????????this.latch?=?latch;
????????this.name?=?name;
????}

????@Override
????public?void?run()?{
????????try?{
????????????SimpleDateFormat?sdf?=?new?SimpleDateFormat("HH:mm:ss.SSS");
????????????Random?random?=?new?Random();

????????????System.out.println(sdf.format(new?Date())?+?"?"?+?name?+?"出发去饭店");
????????????Thread.sleep((long)?(random.nextDouble()?*?3000)?+?1000);
????????????System.out.println(sdf.format(new?Date())?+?"?"?+?name?+?"到了饭店");
????????????latch.countDown();
????????}?catch?(Exception?e)?{
????????????e.printStackTrace();
????????}
????}
}
这是服务员类:

package?onemore.study;

import?java.text.SimpleDateFormat;
import?java.util.Date;
import?java.util.concurrent.CountDownLatch;

public?class?Waitress?implements?Runnable?{
????private?CountDownLatch?latch;
????private?String?name;

????public?Waitress(CountDownLatch?latch,?String?name)?{
????????this.latch?=?latch;
????????this.name?=?name;
????}

????@Override
????public?void?run()?{
????????try?{
????????????SimpleDateFormat?sdf?=?new?SimpleDateFormat("HH:mm:ss.SSS");
????????????System.out.println(sdf.format(new?Date())?+?"?"?+?name??+?"等待顾客");
????????????latch.await();
????????????System.out.println(sdf.format(new?Date())?+?"?"?+?name??+?"开始上菜");
????????}?catch?(Exception?e)?{
????????????e.printStackTrace();
????????}
????}
}

然后,再写一个测试类,用于模拟上面所说的场景:

package?onemore.study;

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

public?class?CountDownLatchTester?{
????public?static?void?main(String[]?args)?throws?InterruptedException?{
????????CountDownLatch?latch?=?new?CountDownLatch(3);

????????List<Thread>?threads?=?new?ArrayList<>(3);
????????threads.add(new?Thread(new?Customer(latch,?"张三")));
????????threads.add(new?Thread(new?Customer(latch,?"李四")));
????????threads.add(new?Thread(new?Customer(latch,?"王五")));
????????for?(Thread?thread?:?threads)?{
????????????thread.start();
????????}

????????Thread.sleep(100);
????????new?Thread(new?Waitress(latch,?"?小芳?")).start();
????}
}

运行以后的结果应该是这样的:

15:25:53.015?王五出发去饭店
15:25:53.015?李四出发去饭店
15:25:53.015?张三出发去饭店
15:25:53.062??小芳?等待顾客
15:25:54.341?张三到了饭店
15:25:54.358?李四到了饭店
15:25:56.784?王五到了饭店
15:25:56.784??小芳?开始上菜

可以看到,服务员小芳在调用Await方法时一直阻塞着,一直等到三个顾客都调用了CountDown方法才继续执行。
面试官:如果有一个顾客迟迟没到,饭店都打样了,也不能一直等啊,应该这么办?
:可以使用Await方法的另一个重载,传入等待的超时时间,比如服务员只等3秒钟,可以把服务员类中的:

latch.await();

改成:

latch.await(3,?TimeUnit.SECONDS);

运行结果可能是这样的:

17:24:40.915?张三出发去饭店
17:24:40.915?李四出发去饭店
17:24:40.915?王五出发去饭店
17:24:40.948??小芳?等待顾客
17:24:43.376?李四到了饭店
17:24:43.544?王五到了饭店
17:24:43.951??小芳?开始上菜
17:24:44.762?张三到了饭店

可以看到,服务员小芳在调用Await方法时虽然被阻塞了,但是时间超过3秒后,没等顾客张三调用CountDown方法就继续执行开始上菜了。
面试官:CountDownLatch的实现原理是什么?
:CountDownLatch有一个内部类叫做Sync,它继承了AbstractQueuedSynchronizer类,其中维护了一个整数state,并且保证了修改state的可见性和原子性。
创建CountDownLatch实例时,也会创建一个Sync的实例,同时把计数器的值传给Sync实例,具体是这样的:

public?CountDownLatch(int?count)?{
????if?(count?<?0)?throw?new?IllegalArgumentException("count?<?0");
????this.sync?=?new?Sync(count);
}

CountDown方法中,只调用了Sync实例的ReleaseShared方法,具体是这样的:

public?void?countDown()?{
????sync.releaseShared(1);
}

其中的ReleaseShared方法,先对计数器进行减1操作,如果减1后的计数器为0,唤醒被Await方法阻塞的所有线程,具体是这样的:

public?final?boolean?releaseShared(int?arg)?{
????if?(tryReleaseShared(arg))?{?//对计数器进行减一操作
????????doReleaseShared();//如果计数器为0,唤醒被await方法阻塞的所有线程
????????return?true;
????}
????return?false;
}

其中的TryReleaseShared方法,先获取当前计数器的值,如果计数器为0时,就直接返回;如果不为0时,使用CAS方法对计数器进行减1操作,具体是这样的:

protected?boolean?tryReleaseShared(int?releases)?{
????for?(;;)?{//死循环,如果CAS操作失败就会不断继续尝试。
????????int?c?=?getState();//获取当前计数器的值。
????????if?(c?==?0)//?计数器为0时,就直接返回。
????????????return?false;
????????int?nextc?=?c-1;
????????if?(compareAndSetState(c,?nextc))//?使用CAS方法对计数器进行减1操作
????????????return?nextc?==?0;//如果操作成功,返回计数器是否为0
????}
}

在Await方法中,只调用了Sync实例的AcquireSharedInterruptibly方法,具体是这样的:

public?void?await()?throws?InterruptedException?{
????sync.acquireSharedInterruptibly(1);
}

其中AcquireSharedInterruptibly方法,判断计数器是否为0,如果不为0则阻塞当前线程,具体是这样的:

public?final?void?acquireSharedInterruptibly(int?arg)
????????throws?InterruptedException?
{
????if?(Thread.interrupted())
????????throw?new?InterruptedException();
????if?(tryAcquireShared(arg)?<?0)//判断计数器是否为0
????????doAcquireSharedInterruptibly(arg);//如果不为0则阻塞当前线程
}

其中TryAcquireShared方法,是AbstractQueuedSynchronizer中的一个模板方法,其具体实现在Sync类中,其主要是判断计数器是否为零,如果为零则返回1,如果不为零则返回-1,具体是这样的:

protected?int?tryAcquireShared(int?acquires)?{
????return?(getState()?==?0)???1?:?-1;
}

面试官:嗯,很不错,马上给你发Offer。

版权声明:本文为CSDN博主「万猫学社」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/heihaozi/article/details/105738230

【END】

更多精彩推荐

???芯片供应被掐断,华为能否安全渡劫?

?OceanBase 十年:一群追梦人的成长史

?2 年 6 个月 11 天,外包到阿里的修仙之路!| 原力计划

?服务器软件大扫盲!

?绝悟之后再超神,腾讯30篇论文入选AI顶会ACL

?中本聪并没有出现,那真相是?

你点的每个“在看”,我都认真当成了喜欢

关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接