首页 大数据

吊打面试官:Java 并发工具类核心剖析与避坑指南

分类:大数据
字数: (9506)
阅读: (5743)
内容摘要:吊打面试官:Java 并发工具类核心剖析与避坑指南,

在高并发场景下,仅仅了解 synchronizedvolatile 是远远不够的。面试时,面试官经常会考察你对 Java 并发工具类的掌握程度,比如 CountDownLatchCyclicBarrierSemaphoreExchanger,以及 BlockingQueue 家族等。本文将通过生活案例和代码示例,带你彻底掌握这些工具类,轻松应对并发编程的挑战。

1. CountDownLatch:倒计时器

生活案例:

想象一下,公司年会,老板要等所有人都到齐了才能开始抽奖。CountDownLatch 就相当于年会主持人,初始化时设定一个计数器,每来一个人,计数器就减一,直到计数器变为零,主持人宣布抽奖开始。

底层原理:

CountDownLatch 内部维护一个计数器,通过 await() 方法阻塞线程,直到计数器变为零。通过 countDown() 方法递减计数器。计数器一旦变为零,就不能再重置。

吊打面试官:Java 并发工具类核心剖析与避坑指南

代码示例:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CountDownLatchExample {

    public static void main(String[] args) throws InterruptedException {
        int totalThreads = 3; // 模拟三个线程
        CountDownLatch latch = new CountDownLatch(totalThreads);

        ExecutorService executor = Executors.newFixedThreadPool(totalThreads);

        for (int i = 0; i < totalThreads; i++) {
            executor.execute(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 线程开始执行");
                    Thread.sleep((long) (Math.random() * 3000)); // 模拟任务执行时间
                    System.out.println(Thread.currentThread().getName() + " 线程执行完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown(); // 线程执行完毕,计数器减一
                }
            });
        }

        latch.await(); // 主线程等待所有子线程执行完毕
        System.out.println("所有线程执行完毕,主线程继续执行");

        executor.shutdown();
    }
}

实战避坑:

  • 计数器只能减不能增CountDownLatch 的计数器一旦变为零,就无法重置,因此要确保初始值设置正确。
  • 防止死锁:如果某个线程在 countDown() 之前发生异常,导致计数器无法递减到零,可能会导致其他线程永久阻塞在 await() 方法上。可以使用 try-finally 块确保 countDown() 总是被执行。

2. CyclicBarrier:循环栅栏

生活案例:

朋友们一起自驾游,约定好每到一个景点都集合拍照,所有人到齐了才能前往下一个景点。CyclicBarrier 就相当于这个集合点,每到达一个人,就等待其他人,直到所有人都到达,大家一起出发。

底层原理:

CyclicBarrier 允许多个线程互相等待,直到所有线程都到达一个屏障点。与 CountDownLatch 的区别在于,CyclicBarrier 的计数器可以重置,因此可以循环使用。可以理解为Nginx的反向代理服务器,接收到一定量的并发请求,转发给后面的服务器进行处理,处理完毕后返回结果,然后重置等待下一次的并发。

吊打面试官:Java 并发工具类核心剖析与避坑指南

代码示例:

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CyclicBarrierExample {

    public static void main(String[] args) {
        int parties = 3; // 模拟三个线程
        CyclicBarrier barrier = new CyclicBarrier(parties, () -> {
            System.out.println("所有线程已到达屏障点,开始执行下一步操作");
        });

        ExecutorService executor = Executors.newFixedThreadPool(parties);

        for (int i = 0; i < parties; i++) {
            final int threadNum = i;
            executor.execute(() -> {
                try {
                    System.out.println("线程 " + threadNum + " 开始执行");
                    Thread.sleep((long) (Math.random() * 3000)); // 模拟任务执行时间
                    System.out.println("线程 " + threadNum + " 到达屏障点,等待其他线程");
                    barrier.await(); // 等待其他线程到达屏障点
                    System.out.println("线程 " + threadNum + " 继续执行");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }

        executor.shutdown();
    }
}

实战避坑:

  • BrokenBarrierException:如果某个线程在等待过程中中断,会导致其他线程抛出 BrokenBarrierException 异常。需要捕获并处理该异常。
  • 避免死锁:确保所有参与者都能到达屏障点,否则会导致死锁。
  • 栅栏动作(Barrier Action):可以在所有线程到达屏障点后执行一个栅栏动作,通常用于合并结果或进行下一步准备工作。

3. Semaphore:信号量

生活案例:

停车场有 5 个停车位,Semaphore 就相当于停车场管理员,初始化时设定 5 个许可证,每来一辆车,管理员就发一个许可证,当许可证用完时,后面的车只能等待。当车辆离开时,管理员收回许可证,其他车辆才能进入。

底层原理:

Semaphore 用于控制对有限资源的访问。内部维护一个计数器,表示可用资源的数量。通过 acquire() 方法获取许可证,如果许可证数量为零,则阻塞线程。通过 release() 方法释放许可证,增加可用资源的数量。 类似于 Nginx 的并发连接数的控制。

吊打面试官:Java 并发工具类核心剖析与避坑指南

代码示例:

import java.util.concurrent.Semaphore;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SemaphoreExample {

    public static void main(String[] args) {
        int permits = 5; // 模拟 5 个许可证
        Semaphore semaphore = new Semaphore(permits);

        ExecutorService executor = Executors.newFixedThreadPool(10); // 模拟 10 个线程竞争资源

        for (int i = 0; i < 10; i++) {
            final int threadNum = i;
            executor.execute(() -> {
                try {
                    semaphore.acquire(); // 获取许可证
                    System.out.println("线程 " + threadNum + " 获取到许可证,开始执行");
                    Thread.sleep((long) (Math.random() * 3000)); // 模拟任务执行时间
                    System.out.println("线程 " + threadNum + " 执行完毕,释放许可证");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release(); // 释放许可证
                }
            });
        }

        executor.shutdown();
    }
}

实战避坑:

  • 忘记释放许可证:如果线程在获取许可证后发生异常,导致 release() 方法没有被执行,可能会导致其他线程一直阻塞。可以使用 try-finally 块确保 release() 总是被执行。
  • 重复释放许可证:如果线程重复释放许可证,可能会导致可用资源数量超过初始值,造成资源竞争问题。要确保 acquire()release() 方法成对出现。

4. Exchanger:数据交换器

生活案例:

两个快递员需要交换包裹,Exchanger 就相当于一个交换站,两个快递员分别将包裹送到交换站,然后互相交换包裹。

底层原理:

Exchanger 允许两个线程交换数据。每个线程调用 exchange() 方法时,如果另一个线程已经到达交换点,则交换数据,否则阻塞等待另一个线程到达。

吊打面试官:Java 并发工具类核心剖析与避坑指南

代码示例:

import java.util.concurrent.Exchanger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExchangerExample {

    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();

        ExecutorService executor = Executors.newFixedThreadPool(2);

        executor.execute(() -> {
            try {
                String data1 = "线程 1 的数据";
                System.out.println("线程 1 准备交换数据:" + data1);
                String data2 = exchanger.exchange(data1); // 交换数据
                System.out.println("线程 1 交换后的数据:" + data2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        executor.execute(() -> {
            try {
                String data2 = "线程 2 的数据";
                System.out.println("线程 2 准备交换数据:" + data2);
                String data1 = exchanger.exchange(data2); // 交换数据
                System.out.println("线程 2 交换后的数据:" + data1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        executor.shutdown();
    }
}

实战避坑:

  • 超时等待exchange() 方法可以设置超时时间,避免线程永久阻塞。
  • 数据类型一致:确保两个线程交换的数据类型一致,否则会导致类型转换异常。

5. BlockingQueue:阻塞队列

生活案例:

餐厅的厨房和前台,厨房生产食物,放入队列中,前台从队列中取出食物提供给顾客。如果队列为空,前台需要等待;如果队列已满,厨房需要等待。

底层原理:

BlockingQueue 提供阻塞的 put()take() 方法。当队列满时,put() 方法会阻塞直到队列有空闲空间;当队列为空时,take() 方法会阻塞直到队列中有元素。BlockingQueue 家族有很多实现类,比如 ArrayBlockingQueueLinkedBlockingQueuePriorityBlockingQueue 等。

代码示例:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ArrayBlockingQueue;

public class BlockingQueueExample {

    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(3); // 容量为 3 的阻塞队列

        // 生产者线程
        new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    String data = "数据 " + i;
                    queue.put(data); // 放入队列,如果队列已满则阻塞
                    System.out.println("生产者放入数据:" + data);
                    Thread.sleep((long) (Math.random() * 1000));
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        // 消费者线程
        new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    String data = queue.take(); // 从队列取出数据,如果队列为空则阻塞
                    System.out.println("消费者取出数据:" + data);
                    Thread.sleep((long) (Math.random() * 1000));
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

实战避坑:

  • 队列容量:选择合适的队列容量,避免队列过小导致生产者阻塞,或队列过大浪费内存。
  • 阻塞超时:可以使用带超时时间的 offer()poll() 方法,避免线程永久阻塞。
  • 中断处理:正确处理 InterruptedException 异常,避免线程无法正常退出。

掌握这些 Java 并发工具类,不仅能让你在面试中游刃有余,更能在实际工作中编写出高效、稳定的并发程序。记住,理解底层原理和实战经验才是关键,祝你在并发编程的道路上越走越远!记住,要合理的使用线程池,比如通过宝塔面板的监控,观测服务器的CPU,内存等指标,来评估线程池的大小。

吊打面试官:Java 并发工具类核心剖析与避坑指南

转载请注明出处: 代码旅行家

本文的链接地址: http://m.acea1.store/blog/222829.SHTML

本文最后 发布于2026-04-07 17:03:00,已经过了20天没有更新,若内容或图片 失效,请留言反馈

()
您可能对以下文章感兴趣
评论
  • 咸鱼翻身 4 天前
    CountDownLatch 那里,防止死锁的建议很实用,之前就踩过坑。