1.线程池的拒绝策略有哪些?
Java中的线程池提供了几种不同的拒绝策略,当线程池无法处理新的任务时(比如因为线程池已满并且工作队列也满了),这些策略会决定如何处理新提交的任务。ThreadPoolExecutor类中定义了以下四种内置的拒绝策略:
- AbortPolicy:这是默认的拒绝策略。当有新任务提交且线程池无法处理时,它会抛出一个RejectedExecutionException异常。
- CallerRunsPolicy:如果任务被拒绝,则该策略会在调用线程(即提交任务的线程)中执行被拒绝的任务。这将降低新任务的提交速度,并减轻系统过载的压力。注意,如果调用线程是池中的一个工作线程,那么这个策略可能会导致额外的任务执行,从而可能进一步延迟其他任务的执行。
- DiscardPolicy:此策略会悄悄地丢弃被拒绝的任务,不会给调用者任何通知。因此,使用此策略时应特别小心,因为它可能导致任务丢失而没有警告。
- DiscardOldestPolicy:此策略会抛弃工作队列中最老的任务(即在队列中等待最久的任务),然后尝试重新提交当前被拒绝的任务。请注意,这个操作可能会重复进行,直到有空间可以接受新任务或者任务再次被拒绝并触发其他的拒绝策略。
2.说说你对JMM内存模型的理解?为什么需要JMM?
Java内存模型(Java Memory Model, JMM)是Java编程语言中定义的一组规则或规范,这些规则决定了线程之间的交互行为以及如何和共享变量进行交互。JMM主要关注于程序中的读、写操作与内存之间发生的可见性、原子性和有序性问题。理解JMM对于编写正确的并发程序非常重要。
为什么需要JMM?
-
硬件架构的复杂性:现代计算机系统通常包含多个处理器核心,每个核心都有自己的高速缓存。当多线程程序运行时,不同的线程可能在不同的核心上执行,这导致了缓存一致性的问题。即一个线程对共享变量的更新可能不会立即被其他线程看到,因为它们查看的是本地缓存副本。JMM通过定义一套规则来确保不同线程间的数据可见性,即使是在异构硬件平台上。
-
编译器优化:为了提高性能,编译器可能会对代码指令重新排序,只要这种重排序不会改变单线程程序的行为。然而,在多线程环境中,这样的优化可能会导致难以预测的行为。JMM规定了哪些编译器优化是可以接受的,并且确保程序员能够预期到多线程程序的行为。
-
提供同步机制:JMM为Java提供了同步机制的基础,比如volatile关键字、synchronized块/方法等,这些都依赖于内存模型来保证跨线程的数据一致性和正确性。
JMM的关键概念
- 原子性:指一个或多个操作作为一个不可分割的整体执行,要么全部执行,要么都不执行。例如,long和double类型的非原子性协定赋值就是一个例子,因为它们的操作可能跨越64位边界并因此不是原子性的。
- 可见性:当一个线程修改了一个共享变量的值,这个新值应该对其他线程是可见的。volatile变量可以保证这一点,同时synchronized和锁机制也能保证可见性。
- 有序性:程序代码的执行顺序并不总是按照源代码中的顺序执行,编译器、处理器和运行时环境都可以对指令进行重排序以优化性能。happens-before关系是JMM用来保证某些操作必须看起来是按特定顺序发生的一种手段,它确保了某些操作的执行结果对于所有线程都是可见的。
总之,JMM是一个抽象的概念框架,用于帮助开发者理解和预测多线程程序的行为,尤其是在涉及到内存访问的时候。它使得开发者能够在不知道底层硬件细节的情况下,编写出正确且高效的并发程序。
3.多线程有什么用?
多线程编程在现代软件开发中扮演着重要角色,它为应用程序带来了诸多好处,特别是在提升性能和响应性方面。以下是多线程的主要用途和优势:
- 提高CPU利用率:在多核处理器环境中,多线程可以充分利用每个核心的能力,使得程序能够并行执行多个任务,从而显著提高处理速度和效率。
- 改善用户体验:通过将耗时操作(如文件读写、网络请求等)放到后台线程中执行,可以让用户界面保持流畅,不会因为等待这些操作完成而卡顿或无响应。这对于图形用户界面(GUI)应用尤为重要。
- 实现并发性:多线程允许一个程序同时进行多项活动。例如,在服务器端应用中,一个线程可以监听新的客户端连接,而其他线程则负责处理已建立的连接上的请求。
- 简化复杂问题的解决:对于一些逻辑上天然适合并行处理的问题,比如图像处理、科学计算、大规模数据分析等,使用多线程可以更直观地建模解决方案,并且更容易管理和维护代码。
- 资源复用:线程共享进程的资源(如内存地址空间),这减少了创建新进程的开销。相比于启动一个新的进程,创建一个新线程通常更快,消耗的系统资源也更少。
- 增强应用程序的灵活性和可扩展性:利用线程池等高级特性,可以根据需要动态调整正在运行的任务数量,以适应不同的负载情况,进而优化整体性能。
- 分布式系统的构建基础:在分布式计算框架中,多线程常用于管理与远程节点之间的通信,以及协调本地和远程任务的执行。
然而,需要注意的是,虽然多线程提供了很多优势,但它也引入了诸如死锁、竞态条件等问题,这些问题需要开发者仔细设计和调试来避免。此外,不当的多线程使用可能会导致性能下降而不是提升。因此,正确理解和合理运用多线程技术是非常重要的。
4.说说CyclicBarrier和CountDownLatch的区别?
CyclicBarrier 和 CountDownLatch 都是 Java 并发包(java.util.concurrent)中提供的同步辅助类,用于协调多个线程之间的操作。然而,它们在使用场景和行为上有显著的区别。
CountDownLatch
- 初始化时设定计数:CountDownLatch 在创建时需要指定一个计数值(count),这个值代表了需要等待的操作数量。
- 一次性使用:一旦计数器减至零,它就不能再被重置,也就是说,CountDownLatch 是不可循环使用的。所有等待该锁存器的线程将会被释放,并且后续对 await() 方法的调用将立即返回。
- 典型应用场景:常用于一个或多个线程等待其他一组线程完成某些操作后再继续执行的情况。例如,主线程等待所有工作线程完成任务后才继续。
CountDownLatch latch = new CountDownLatch(3); // 创建一个计数为3的锁存器
// 其他线程完成任务后调用 latch.countDown();
latch.await(); // 主线程等待直到计数归零
CyclicBarrier
- 可重复使用:与 CountDownLatch 不同的是,CyclicBarrier 可以在达到屏障点后被重置并再次使用。这意味着它可以用来管理一系列需要反复执行的任务。
- 屏障动作:可以在创建 CyclicBarrier 时提供一个 Runnable 对象作为参数,当所有参与的线程都到达屏障点时,会先执行这个动作。
- 典型应用场景:适用于多个线程需要相互等待到达某个状态点才能继续执行的情况,比如迭代式的计算过程,或者多线程之间进行数据交换等场景。
CyclicBarrier barrier = new CyclicBarrier(3, new Runnable() {
public void run() {
// 当所有线程都到达屏障点时执行的动作
}
});
// 线程在执行完各自的任务后调用 barrier.await();
总结
- 如果你需要一次性的等待机制,那么 CountDownLatch 是合适的选择;而如果你的需求涉及到多次重复的等待/通知模式,则应该考虑使用 CyclicBarrier。
- CyclicBarrier 提供了更灵活的行为,包括可以定义屏障到达时执行的动作,并且能够重置以供下一轮使用。
- 选择哪种工具取决于具体的应用场景和需求。
5.什么是AQS?
AQS,全称 AbstractQueuedSynchronizer(抽象队列同步器),是 Java 并发包 (java.util.concurrent) 中的一个核心组件。它提供了一种机制来实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关的同步器(如信号量、事件等)。AQS 是许多同步工具类(如 ReentrantLock、Semaphore、CountDownLatch 和 CyclicBarrier)的基础构建块。
AQS 的核心概念
- 状态(State):AQS 使用一个 volatile int 类型的状态变量来表示同步状态。这个状态可以被原子性地更新,用于表示资源是否可用或被占用的程度。例如,在 ReentrantLock 中,这个状态代表了锁的持有次数;在 Semaphore 中,它则表示剩余许可的数量。
- 等待队列:AQS 维护了一个 CLH(Craig, Landin, and Hagersten locks)类型的 FIFO 队列,用于管理那些试图获取同步状态但未能成功的线程。每个节点(Node)代表一个等待中的线程,并且这些节点按照加入顺序排列形成链表结构。
- 独占式 vs 共享式:AQS 支持两种模式的同步——独占式(Exclusive)和共享式(Shared)。独占式意味着同一时刻只能有一个线程成功获取到同步状态;而共享式允许多个线程同时获得同步状态,只要它们符合特定条件。
- 模板方法模式:AQS 采用模板方法模式,定义了一些公共的方法供子类使用,但是具体的同步逻辑需要由子类自行实现。比如 tryAcquire(int)、tryRelease(int) 等方法就需要根据具体的需求来定制实现。
AQS 的主要功能
- acquire() 和 release():用于以独占方式尝试获取或释放同步状态。如果无法立即获取,则将当前线程添加到等待队列中并使其进入等待状态。
- acquireShared() 和 releaseShared():用于以共享方式尝试获取或释放同步状态。与独占模式不同的是,这里允许多个线程同时持有同步状态。
- ConditionObject:AQS 还提供了条件队列的支持,通过 ConditionObject 可以实现类似于 Object Monitor 的 wait/notify 功能。
为什么使用 AQS?
AQS 提供了一个高效且灵活的框架来实现复杂的同步控制逻辑,极大地简化了自定义同步器的开发难度。开发者只需要关注如何管理同步状态以及何时允许线程获取同步状态,而不必关心线程调度、等待队列管理等底层细节。这使得我们可以更专注于业务逻辑本身,同时也保证了代码的质量和性能。
总之,AQS 是 Java 并发编程的重要组成部分,为构建高性能、高可靠的并发程序提供了坚实的基础。