腾讯面试居然跟我扯了半小时的CountDownLatch

文章持续更新,微信搜索「 万猫学社 」第一时间阅读。
关注后回复「 电子书 」,免费获取12本Java必读技术书籍。

一个长头发、穿着清爽的小姐姐,拿着一个崭新的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();

        for (Thread thread : threads) {
            thread.join();
        }
    }
}

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

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。

本故事纯属虚构,如有雷同实属巧合

文章持续更新,微信搜索「 万猫学社 」第一时间阅读。
关注后回复「 电子书 」,免费获取12本Java必读技术书籍。

以下是一个可能的面试题及答案: 问题:请简要介绍一下CountDownLatch。 答案:CountDownLatchJava中的一个并发工具类,用于协调多个线程之间的执行顺序。它通过一个计数器来实现,计数器初始值为线程数,每个线程执行完任务后,计数器减1。当计数器为0时,所有线程开始执行下一步操作。常用于等待多个线程完成某个操作后再执行下一步操作。 问题:CountDownLatch的使用场景有哪些? 答案:CountDownLatch主要用于等待多个线程完成某个操作后再执行下一步操作。例如,当多个线程需要同时进行某个计算任务时,可以使用CountDownLatch来等待所有线程完成任务后再进行下一步操作。同时,CountDownLatch还可以用于多线程测试,例如测试多个线程同时执行某个任务的性能。 问题:CountDownLatch的实现原理是什么? 答案:CountDownLatch的实现原理是通过一个计数器来实现的。计数器初始值为线程数,每个线程执行完任务后,计数器减1。主线程可以调用CountDownLatch的await()方法来等待计数器归零,await()方法会一直阻塞直到计数器为0。 问题:CountDownLatch有哪些特点? 答案:CountDownLatch有以下几个特点: 1. CountDownLatch是一个计数器,初始值为指定的数值,用于表示需要等待的线程数量。 2. 当一个线程完成了它的任务后,调用CountDownLatchcountDown()方法将计数器减1。 3. 主线程可以调用CountDownLatch的await()方法来等待计数器归零,await()方法会一直阻塞直到计数器为0。 4. CountDownLatch是一次性的,一旦计数器归零,就不能再次使用。 5. CountDownLatch是线程安全的。 问题:CountDownLatch和CyclicBarrier有什么区别? 答案:CountDownLatch和CyclicBarrier都可以用于协调多个线程之间的执行顺序,但是它们的使用场景有所不同。CountDownLatch主要用于等待多个线程完成某个操作后再执行下一步操作,而CyclicBarrier主要用于等待多个线程到达某个屏障点后再同时执行下一步操作。此外,CountDownLatch是一次性的,一旦计数器归零,就不能再次使用,而CyclicBarrier可以重复使用。
评论 490
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

万猫学社

您的鼓励将是我创作的最大动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值