MST

星途 面试题库

面试题:Java信号量与其他同步机制的深度对比及应用选择

对比Java中信号量(`Semaphore`)与锁(如`ReentrantLock`)、同步块(`synchronized`)、`CountDownLatch`、`CyclicBarrier`等同步机制。分析它们在原理、功能、性能、适用场景等方面的差异。假设你正在开发一个复杂的分布式系统,在不同的模块需求下,你会如何选择合适的同步机制,说明理由并给出相应的代码片段。
39.3万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

1. 原理差异

  • Semaphore(信号量):维护了一组虚拟的许可(permits),线程要访问共享资源,首先要获取许可。如果没有许可,则线程被阻塞,直到有许可可用。
  • ReentrantLock(可重入锁):基于AQS(AbstractQueuedSynchronizer)框架实现,通过一个FIFO队列来管理等待获取锁的线程。线程获取锁时,如果锁可用,则获取成功并将锁的持有计数加1;如果锁不可用,则线程进入等待队列。可重入意味着同一个线程可以多次获取同一个锁,每获取一次,持有计数增加1,释放时减少1,直到计数为0时真正释放锁。
  • synchronized:是Java的关键字,基于Monitor机制实现。每个对象都有一个Monitor,线程进入同步块时会尝试获取对象的Monitor,如果获取成功则进入同步块,否则进入阻塞状态,等待Monitor的释放。synchronized也支持可重入,线程进入同步块时,Monitor的进入数加1,退出时减1。
  • CountDownLatch:基于AQS实现,通过一个计数器来控制线程的等待和放行。线程调用await()方法时,如果计数器的值不为0,则线程进入等待状态;其他线程调用countDown()方法将计数器减1,当计数器的值减为0时,所有等待的线程被唤醒。
  • CyclicBarrier:同样基于AQS实现,它也有一个计数器,用于控制一组线程相互等待,直到所有线程都到达某个点(barrier)。线程调用await()方法时,计数器减1,当计数器减为0时,所有等待的线程被唤醒,并且计数器会重置为初始值,以便下次使用。

2. 功能差异

  • Semaphore:主要用于控制同时访问特定资源的线程数量,比如数据库连接池,限制同时使用连接的线程数。
  • ReentrantLock:与synchronized类似,用于实现线程同步,保证同一时间只有一个线程可以执行临界区代码。但ReentrantLock提供了更灵活的锁获取和释放方式,如可中断的锁获取、公平锁等。
  • synchronized:用于实现基本的线程同步,确保同一时间只有一个线程能够进入同步块。它可以作用于方法和代码块,并且在使用上相对简单。
  • CountDownLatch:用于一个或多个线程等待其他一组线程完成操作后再继续执行,例如主线程等待所有子线程完成任务后再进行汇总统计。
  • CyclicBarrier:用于一组线程相互等待,直到所有线程都准备好继续执行,比如多个线程并发处理数据,然后在某个点汇总结果。与CountDownLatch不同的是,CyclicBarrier的计数器可以重置,可重复使用。

3. 性能差异

  • Semaphore:性能取决于竞争的激烈程度,在高竞争环境下,由于频繁的许可获取和释放,可能会有一定的性能开销。
  • ReentrantLock:在高竞争环境下,由于其基于AQS的灵活实现,性能可能优于synchronizedReentrantLock可以使用非公平锁来提高吞吐量,但可能导致某些线程长时间等待。
  • synchronized:在Java 6及以后的版本中,通过各种优化(如偏向锁、轻量级锁等),性能有了很大提升。但在高竞争环境下,性能可能不如ReentrantLock
  • CountDownLatch:性能主要取决于countDown()操作的频率,在合理使用的情况下,性能开销较小。
  • CyclicBarrier:性能同样取决于await()操作的频率,由于需要等待所有线程到达,在高并发场景下可能会有一定的性能瓶颈。

4. 适用场景差异

  • Semaphore:适用于需要限制并发访问数量的场景,如资源池的管理。
  • ReentrantLock:适用于需要更灵活控制锁获取和释放的场景,如可中断的锁获取、公平锁需求等。
  • synchronized:适用于简单的线程同步场景,代码简洁,使用方便。
  • CountDownLatch:适用于一个或多个线程等待其他一组线程完成任务的场景,如初始化等待。
  • CyclicBarrier:适用于一组线程相互等待,共同完成某个阶段任务,然后继续下一个阶段的场景,如迭代计算。

5. 分布式系统中同步机制的选择及代码示例

5.1 资源池管理(Semaphore)

在分布式系统中,如果需要管理共享资源(如数据库连接、网络连接等)的并发访问数量,可以使用Semaphore

import java.util.concurrent.Semaphore;

public class DatabaseConnectionPool {
    private static final int MAX_CONNECTIONS = 10;
    private final Semaphore semaphore;

    public DatabaseConnectionPool() {
        semaphore = new Semaphore(MAX_CONNECTIONS);
    }

    public void getConnection() throws InterruptedException {
        semaphore.acquire();
        try {
            // 获取数据库连接的逻辑
            System.out.println("获取到数据库连接");
        } finally {
            semaphore.release();
            System.out.println("释放数据库连接");
        }
    }
}

5.2 简单线程同步(synchronized)

如果分布式系统中的某个模块需要简单的线程同步,确保同一时间只有一个线程执行临界区代码,可以使用synchronized

public class SimpleCounter {
    private int count = 0;

    public synchronized void increment() {
        count++;
        System.out.println("Count: " + count);
    }
}

5.3 灵活锁控制(ReentrantLock)

当需要更灵活的锁控制,如可中断的锁获取时,可以使用ReentrantLock

import java.util.concurrent.locks.ReentrantLock;

public class FlexibleLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void doSomething() {
        try {
            if (lock.tryLock()) {
                try {
                    // 临界区代码
                    System.out.println("获取到锁,执行任务");
                } finally {
                    lock.unlock();
                }
            } else {
                System.out.println("无法获取锁,放弃任务");
            }
        } catch (InterruptedException e) {
            System.out.println("线程被中断");
        }
    }
}

5.4 线程等待一组任务完成(CountDownLatch)

在分布式系统中,如果某个模块需要等待其他多个任务完成后再继续执行,可以使用CountDownLatch

import java.util.concurrent.CountDownLatch;

public class TaskCompletionExample {
    public static void main(String[] args) {
        int taskCount = 3;
        CountDownLatch latch = new CountDownLatch(taskCount);

        for (int i = 0; i < taskCount; i++) {
            new Thread(() -> {
                try {
                    // 模拟任务执行
                    Thread.sleep(1000);
                    System.out.println("任务完成");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            }).start();
        }

        try {
            latch.await();
            System.out.println("所有任务完成,继续执行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

5.5 多线程相互等待(CyclicBarrier)

如果分布式系统中的多个线程需要相互等待,共同完成某个阶段任务后再继续下一个阶段,可以使用CyclicBarrier

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int threadCount = 3;
        CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
            System.out.println("所有线程到达屏障,开始下一阶段");
        });

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    System.out.println("线程准备就绪");
                    barrier.await();
                    // 下一阶段的任务
                    System.out.println("线程继续执行下一阶段任务");
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}