我是 javapub,一名 Markdown
程序员从👨💻,八股文种子选手。
面试官: 你用过 CountDownLatch 和 CyclicBarrier 吗?
候选人: 当然可以。CountDownLatch 和 CyclicBarrier 都是 Java 中用于多线程编程的工具类。它们都可以用于协调多个线程的执行顺序,但是它们的实现方式和使用场景有所不同。
面试官: 那你能具体说一下它们的区别吗?
候选人: 当然可以。CountDownLatch 是一个计数器,它可以让一个或多个线程等待其他线程完成某些操作后再执行。它的实现方式是通过一个计数器来实现的,当计数器的值为 0 时,等待线程就会被唤醒。而 CyclicBarrier 则是一个屏障,它可以让多个线程在某个点上等待,直到所有线程都到达这个点后再一起继续执行。它的实现方式是通过一个计数器和一个屏障点来实现的,当计数器的值为 0 时,所有线程就会被唤醒。
面试官: 那你能举个例子来说明它们的使用场景吗?
候选人: 当然可以。比如说,我们有一个任务需要分成多个子任务来执行,而这些子任务之间是相互独立的,我们可以使用 CountDownLatch 来实现。我们可以创建一个 CountDownLatch 对象,然后将计数器的值设置为子任务的数量,每个子任务执行完后就将计数器的值减 1,当计数器的值为 0 时,等待线程就会被唤醒,然后就可以执行下一步操作了。
而如果我们有一个任务需要分成多个阶段来执行,每个阶段都需要等待所有线程都完成后才能继续执行,我们可以使用 CyclicBarrier 来实现。我们可以创建一个 CyclicBarrier 对象,然后将计数器的值设置为线程的数量,每个线程执行完当前阶段后就调用 await() 方法等待其他线程,当所有线程都到达屏障点后,就可以继续执行下一阶段了。
面试官: 那你能说一下 CountDownLatch 和 CyclicBarrier 的优缺点吗?
候选人: 当然可以。CountDownLatch 的优点是实现简单,使用方便,而且可以重复使用。缺点是只能使用一次,而且计数器的值不能被重置。而 CyclicBarrier 的优点是可以重复使用,而且计数器的值可以被重置。缺点是实现相对复杂,而且只能等待固定数量的线程。
面试官: 好的,那你能写一段代码来演示一下吗?
候选人: 当然可以。以下是一个使用 CountDownLatch 的示例代码:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
int n = 5;
CountDownLatch latch = new CountDownLatch(n);
for (int i = 0; i < n; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " is working");
latch.countDown();
}).start();
}
latch.await();
System.out.println("All threads have finished working");
}
}
以上代码创建了一个 CountDownLatch 对象,然后启动了 5 个线程,每个线程执行完后就将计数器的值减 1,当计数器的值为 0 时,等待线程就会被唤醒,然后输出 “All threads have finished working”。
以下是一个使用 CyclicBarrier 的示例代码:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
int n = 5;
CyclicBarrier barrier = new CyclicBarrier(n, () -> {
System.out.println("All threads have finished working");
});
for (int i = 0; i < n; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " is working");
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
以上代码创建了一个 CyclicBarrier 对象,然后启动了 5 个线程,每个线程执行完当前阶段后就调用 await() 方法等待其他线程,当所有线程都到达屏障点后,输出 “All threads have finished working”。
抱歉,似乎我的回答没有完全满足您的需求。以下是我为您继续写的文章:
面试官: 那你能说一下 CountDownLatch 和 CyclicBarrier 的底层实现原理吗?
候选人: 当然可以。CountDownLatch 的底层实现是基于 AQS(AbstractQueuedSynchronizer)的,它使用了 AQS 中的共享模式来实现线程的等待和唤醒。当计数器的值为 0 时,等待线程就会被唤醒,而唤醒线程的操作是通过 AQS 中的 releaseShared() 方法来实现的。
而 CyclicBarrier 的底层实现也是基于 AQS 的,它使用了 AQS 中的独占模式来实现线程的等待和唤醒。当所有线程都到达屏障点后,唤醒线程的操作是通过 AQS 中的 release() 方法来实现的。
面试官: 那你能说一下 AQS 的实现原理吗?
候选人: 当然可以。AQS 的实现原理是基于一个双向链表和一个 state 变量。state 变量用于表示当前锁的状态,而双向链表用于存储等待线程的队列。当一个线程尝试获取锁时,如果锁已经被其他线程占用了,那么它就会被加入到等待队列中,然后进入阻塞状态。当锁被释放时,AQS 会从等待队列中取出一个线程,并将锁分配给它。
面试官: 好的,那你能写一段代码来演示一下 AQS 的实现原理吗?
候选人: 当然可以。以下是一个简单的自定义锁的示例代码,它的实现原理就是基于 AQS 的:
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class MyLock {
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
}
以上代码定义了一个 MyLock 类,它的 lock() 方法和 unlock() 方法分别对应着获取锁和释放锁的操作。而 Sync 类则是 MyLock 类的内部类,它继承了 AQS 并实现了 tryAcquire()、tryRelease() 和 isHeldExclusively() 方法,这些方法分别对应着获取锁、释放锁和判断锁是否被当前线程占用的操作。
面试官: 嗯,背的很熟。
最近我在更新《面试1v1》系列文章,主要以场景化的方式,讲解我们在面试中遇到的问题,致力于让每一位工程师拿到自己心仪的offer,感兴趣可以关注JavaPub追更!
《面试1v1》 连载中…
🎁目录合集:
Gitee:https://gitee.com/rodert/JavaPub
GitHub:https://github.com/Rodert/JavaPub
http://javapub.net.cn