全网最全Java并发面试题(25秋招走起~)

线程池

1.为什么要使用线程池?

  1. 资源管理:线程池可以管理和复用线程,而不是为每个任务都创建新线程。这降低了线程创建和销毁的开销,节省了系统资源,尤其是内存和 CPU 时间。
  2. 提高性能:线程池可以根据需要动态地调整线程的数量,确保系统中的线程数量在合理范围内,避免了过多线程导致的性能下降(例如上下文切换成本增加)。
  3. 控制并发度:线程池允许您限制并发执行的任务数量,这有助于避免资源竞争、死锁和过度并发等问题。通过调整线程池的大小,可以根据系统资源和负载来控制并发度。
  4. 提高代码可维护性:线程池将任务的创建和执行分离开来,使代码更易于维护。任务的执行逻辑独立于线程管理,可以专注于任务本身的逻辑。
  5. 避免线程泄漏:线程池可以确保线程的正确释放和回收,避免了线程泄漏问题,因为线程池会自动回收不再使用的线程。
  6. 提高系统稳定性:通过合理配置线程池,可以避免过度消耗系统资源和系统崩溃等问题,提高系统的稳定性。

2.创建线程池ThreadPoolExecutor有哪几种方式?

  1. 基本构造函数
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
这是 ThreadPoolExecutor 的基本构造函数,它允许你手动指定线程池的核心线程数、最大线程数、线程空闲时间、时间单位以及工作队列。

这个构造函数提供了最大的灵活性,可以根据需要自定义线程池的各个参数。
  1. Executors** 工厂方法**:Java 还提供了 Executors 类,它包含一些静态工厂方法,可以更方便地创建不同类型的线程池。例如:
    • Executors.newFixedThreadPool(int n):创建一个固定大小的线程池,核心线程数和最大线程数都是 n,没有空闲线程。
    • Executors.newCachedThreadPool():创建一个可缓存的线程池,核心线程数为 0,最大线程数为 Integer.MAX_VALUE,适用于短期异步任务。
    • Executors.newSingleThreadExecutor():创建一个单线程的线程池,核心线程数和最大线程数都是 1,用于顺序执行任务。
  1. Executors.newScheduledThreadPool(int corePoolSize):这个工厂方法创建一个定时执行任务的线程池,可以执行定时任务和周期性任务。核心线程数是指定的 corePoolSize,最大线程数为 Integer.MAX_VALUE。
  2. Executors.newWorkStealingPool():这个工厂方法创建一个工作窃取线程池,用于执行大量耗时任务。这个线程池会根据需要自动增加或减少线程数,以充分利用多核处理器。

3.Executors 类创建三种类型的线程池有哪些缺陷?

Executors 类提供了三种类型的线程池:newFixedThreadPool、newCachedThreadPool 和 newSingleThreadExecutor。各自缺陷如下:

newFixedThreadPool(固定大小线程池)的缺陷:

  • 资源浪费: 固定大小线程池创建了固定数量的线程,这意味着即使在任务较少的情况下,线程池的所有线程也会一直存在,造成资源浪费。
  • 任务排队问题: 如果任务提交速度大于线程池处理速度,任务会积压在队列中,可能导致队列溢出(OutOfMemoryError)。此外,由于线程数量是固定的,可能无法应对突发的任务量。

newCachedThreadPool(缓存线程池)的缺陷:

  • 线程无限增长: 缓存线程池可以无限制地创建新线程,如果任务量非常大,可能导致线程数量过多,耗尽系统资源,最终导致应用程序崩溃(OutOfMemoryError)。
  • 线程销毁问题: 线程池中的线程默认会在空闲一定时间后被销毁,但如果任务一直持续不断地到达,线程池中的线程将不会被销毁,可能导致线程资源的浪费。

newSingleThreadExecutor(单线程线程池)的缺陷:

  • 性能问题: 单线程线程池只有一个线程,不能并行执行多个任务。如果有大量任务需要并行执行,使用单线程线程池可能会导致性能瓶颈。
  • 无法应对任务失败: 如果线程在执行任务时出现未捕获的异常而终止,线程池会创建一个新线程来替代,但这可能会导致不断重复相同的失败。

4.线程池都有哪些状态

  1. RUNNING(运行中):线程池处于正常工作状态,可以接受并处理任务。在这种状态下,线程池会根据需要创建新的线程或者重用空闲线程来执行任务。
  2. SHUTDOWN(关闭中):线程池不再接受新的任务,但会继续处理已经提交的任务。在这种状态下,ThreadPoolExecutor 不会再创建新的线程,而是等待现有线程执行完任务。
  3. STOP(停止):线程池停止接受新任务,并且会尝试中断所有正在执行的任务。这个状态表示线程池已经停止工作,不再处理任何任务。
  4. TIDYING(整理中):线程池正在进行清理工作,清理工作包括中断已经停止的工作线程并回收资源。在线程池处于这个状态时,可能会有一些线程正在执行清理工作。
  5. TERMINATED(终止):线程池的所有任务已经完成,线程池已经被彻底终止。线程池不再可用。

5.线程池各个核心参数的含义是什么?

corePoolSize(核心线程数)

  • 含义:corePoolSize 是线程池中维护的核心线程数量,即线程池在任何时候都会保持这么多线程处于活动状态,即使它们没有正在执行任务。
  • 作用:核心线程用于处理任务队列中的任务,当有新任务提交时,如果核心线程数尚未达到 corePoolSize,线程池会创建新线程来处理任务,而不是将任务放入队列中。

maximumPoolSize(最大线程数)

  • 含义:maximumPoolSize 是线程池允许的最大线程数量,包括核心线程和非核心线程。
  • 作用:当任务队列中的任务数量超过 corePoolSize 且小于 maximumPoolSize 时,线程池会创建新的非核心线程来处理任务。这个参数限制了线程池的最大并发执行能力。

keepAliveTime(线程空闲时间)

  • 含义:keepAliveTime 是非核心线程的空闲时间,即当非核心线程在处理完任务后空闲超过这个时间时,它们可能会被终止并从线程池中移除。
  • 作用:通过设置合适的 keepAliveTime 可以控制非核心线程的生命周期,避免无限制地保持空闲线程。

unit(时间单位)

  • 含义:unit 是用于表示 keepAliveTime 的时间单位,可以是毫秒、秒、分钟等。
  • 作用:确定了 keepAliveTime 参数的时间单位,确保正确设置线程的空闲时间。

workQueue(工作队列)

  • 含义:工作队列用于存储待执行的任务,它可以是一个阻塞队列或其他类型的队列,比如 LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue 等。
  • 作用:工作队列决定了线程池的任务调度策略。不同类型的队列具有不同的特性,如有界或无界,按照先进先出或优先级等。

threadFactory(线程工厂)

  • 含义:ThreadFactory 用于创建线程池中的线程对象,它允许您自定义线程的创建过程,例如为线程设置有意义的名称、设置线程优先级等。
  • 作用:通过自定义 ThreadFactory,可以更好地识别线程池中的线程,方便调试和监控。

handler(拒绝策略)

  • 含义:当线程池已经达到最大线程数且任务队列已满时,新提交的任务将会被拒绝执行。拒绝策略定义了在线程池无法接受新任务时的处理方式。
  • 作用:选择合适的拒绝策略可以避免任务丢失或影响系统稳定性。Java 提供了几种内置的拒绝策略,例如 ThreadPoolExecutor.AbortPolicy(默认策略,抛出异常)、ThreadPoolExecutor.DiscardPolicy(丢弃任务)等,同时您也可以自定义拒绝策略。

6.线程池的拒绝策略有哪些?

当线程池在面临无法处理新任务时,可以使用不同的拒绝策略来处理这种情况。以下是常见的线程池拒绝策略:

AbortPolicy(默认策略)

  • 描述:当线程池无法处理新任务时,抛出 RejectedExecutionException 异常,不执行新任务。
  • 使用场景:这是默认的拒绝策略,它会在任务被拒绝时立即抛出异常,通常用于要求严格的任务处理。

CallerRunsPolicy(调用者运行策略)

  • 描述:当线程池无法处理新任务时,将任务交给调用线程来执行,而不会启动新线程。
  • 使用场景:如果不能接受任务被丢弃,可以选择这个策略,但请注意,如果调用线程也忙于处理任务,可能会导致任务处理速度变慢。

DiscardPolicy(丢弃策略)

  • 描述:当线程池无法处理新任务时,直接丢弃掉这个任务,不做任何处理。
  • 使用场景:如果对任务的处理要求不高,可以选择这个策略,但需要注意任务丢失的可能性。

DiscardOldestPolicy(丢弃最旧策略)

  • 描述:当线程池无法处理新任务时,丢弃队列中最旧的任务(即最先进入队列的任务),然后尝试执行新任务。
  • 使用场景:如果希望保留最新提交的任务,而丢弃较旧的任务,可以选择这个策略。

自定义策略(实现 RejectedExecutionHandler 接口):

  • 描述:您可以自定义拒绝策略,实现 RejectedExecutionHandler 接口,并在实现中定义自己的拒绝逻辑。例如,您可以将任务记录到日志中或将其放入其他队列中等等。

7.向线程池中提交任务有几种方式?他们的区别是什么?

  • 使用 execute() 方法提交任务
Executor executor = Executors.newFixedThreadPool(5); // 创建一个固定大小的线程池
executor.execute(new RunnableTask()); // 提交一个实现了 Runnable 接口的任务
  • 使用 submit() 方法提交任务并获取 Future 对象:
ExecutorService executorService = Executors.newCachedThreadPool(); // 创建一个可缓存的线程池
Future<Integer> future = executorService.submit(new CallableTask()); // 提交一个实现了 Callable 接口的任务,并返回 Future 对象

区别如下:

返回值类型

  • execute() 方法没有返回值,它用于提交实现了 Runnable 接口的任务,这些任务不返回结果,通常用于执行一些无返回值的操作。
  • submit() 方法返回一个 Future 对象,您可以使用这个 Future 对象来获取任务的执行结果,取消任务的执行,或者检查任务是否已经完成。这对于需要获取任务执行结果的情况非常有用,因为 Future 可以包含任务的执行结果或异常信息。

异常处理

  • execute() 方法不会抛出任务执行过程中的异常,如果任务内部抛出异常,线程池将捕获并记录异常,但不会将其传递给调用方。这可能导致难以诊断任务执行中发生的问题。
  • submit() 方法可以捕获并返回任务执行过程中抛出的异常。您可以使用 Future 对象的 get() 方法来获取任务的结果,如果任务抛出了异常,get() 方法将抛出该异常,使得调用方能够更容易地处理任务执行中的异常。

适用场景

  • execute() 方法适用于不需要获取任务执行结果的情况,比如并发执行一组操作,或者简单的线程执行任务。
  • submit() 方法更适用于需要获取任务执行结果、取消任务或处理任务异常的情况,比如需要执行一个有返回值的任务,或者需要监控任务的执行状态。

如果您只是需要执行一些简单的操作,不需要关心任务的返回结果或异常处理,那么 execute() 可能更方便。但如果您需要获取任务的结果、进行异常处理,或者取消任务的执行,那么 submit() 更适合。

8.线程池满了,往线程池里提交任务会发生什么样的情况

如果你使用的LinkedBlockingQueue(阻塞队列),也就是无界队列的话,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务;如果你使用的是有界队列比方说ArrayBlockingQueue的话,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy

9.线程池的线程数量怎么确定?

分为计算密集型和IO密集型

计算密集型

这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。

IO密集型

这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 : 核心线程数 = CPU核心数量 * 2。

volatile

10.volatile作用

可见性

  • 当一个变量被声明为 volatile 时,它保证了变量的写操作对于其他线程是可见的。具体来说,如果一个线程修改了 volatile 变量的值,其他线程将立即看到这个变化,而不会使用本地缓存的值。
  • 这个特性对于多线程之间共享的状态非常有用。例如,一个线程修改了一个共享的标志位,其他线程可以使用 volatile 修饰的变量来检测这个标志位的状态,以协调各个线程的操作。而如果不使用 volatile,可能会出现线程之间的数据不一致性问题。

禁止指令重排序

  • volatile 变量的读写操作会禁止编译器和处理器对其进行指令重排序。这确保了在多线程环境下,volatile 变量的赋值操作不会被重排序到其他指令之前或之后。
  • 这个特性对于确保代码的执行顺序是如预期一样的非常重要。在不使用 volatile 的情况下,编译器和处理器可能会对指令进行重排序,导致代码的执行顺序不同于代码中的编写顺序,从而引发意外的行为。

需要注意的是,volatile 只能用于修饰成员变量(类的属性)以及静态成员变量(类的静态属性),而不能用于修饰局部变量。此外,虽然 volatile 可以保证可见性和禁止指令重排序,但它并不能保证原子性。

11.i++是线程安全的吗?

分为两种情况:

  1. 局部变量肯定是线程安全的(原因:方法内局部变量是线程私有的)
  2. 成员变量多个线程共享时,就不是线程安全的。因为它不是一个原子操作,而是由多个步骤组成的操作,包括读取变量的当前值、增加这个值,然后将结果写回变量。在多线程环境下,这个操作可能会出现竞态条件,导致不确定的结果。比如:
    1. 线程 A 读取变量 i 的值为 1。
    2. 线程 B 读取变量 i 的值也为 1。
    3. 线程 A 增加 i 的值并计算结果,得到 2。
    4. 线程 B 也增加 i 的值并计算结果,得到 2。
    5. 线程 A 将结果 2 写回变量 i。
    6. 线程 B 也将结果 2 写回变量 i。

12.在 Java 中有多少种方法可以实现可见性?

  • 使用volatile关键字: 声明一个变量为volatile可以确保该变量的读写操作对所有线程都是可见的。volatile变量的值在一个线程中被修改后,会立即刷新到主内存,以便其他线程能够看到最新的值。
private volatile boolean flag = false;
  • 使用**synchronized**关键字: 使用synchronized关键字来对代码块或方法进行同步,确保只有一个线程可以访问同步块内的代码。这不仅保证了互斥性,还确保了可见性,因为进入同步块之前的所有修改都会被刷新到主内存,而在同步块内的操作都会从主内存中读取最新的值。
synchronized (lockObject) {
    // 访问共享变量
}
  • 使用**java.util.concurrent**工具: Java提供了一些并发工具,如java.util.concurrent包中的Atomic类(如AtomicInteger、AtomicBoolean等),这些类提供了原子性操作,确保对变量的修改对所有线程都是可见的。
private AtomicInteger count = new AtomicInteger(0);

// 原子性的增加操作
count.incrementAndGet();
  • 使用**Lock**接口: 使用java.util.concurrent.locks.Lock接口及其实现类(如ReentrantLock)可以实现精细的线程控制,同时也确保了可见性。在进入Lock的临界区之前,线程会将本地工作内存中的修改刷新到主内存,从而保证其他线程能够看到最新的值。
Lock lock = new ReentrantLock();
lock.lock();
try {
    // 访问共享变量
} finally {
    lock.unlock();
}
  • 使用**java.util.concurrent**工具中的高级类: Java的并发库还提供了许多高级工具,如CountDownLatch、CyclicBarrier、Semaphore等,这些工具可以帮助线程之间协同工作,确保可见性。
  • 使用**Thread.join()**方法: 当一个线程调用另一个线程的join()方法时,它会等待被调用线程执行完毕,从而确保对共享数据的修改在后续操作中可见。

13.正确使用 volatile 的条件是什么?

您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

  1. 对变量的写操作不依赖于当前值。
  2. 该变量没有包含在具有其他变量的不变式中。

实际上,这两个条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作(x++ )看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操作需要使 x的值在操作期间保持不变,而 volatile 变量无法实现这点。(然而,如果将值调整为只从单个线程写入,那么可以忽略第一个条件。

Synchronized

14.Synchronized 的实现原理

对象在内存中分为对象头,实例数据和对齐填充三个区域。在对象头中保存了锁标志位和指向 Monitor 对象的起始地址。当 Monitor 被某个线程占用后就会处于锁定状态。 synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 synchronized 修饰的方法使用是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

15.JDK1.6 之后的synchronized 关键字底层做了哪些优化

从JDK1.6版本之后,synchronized本身也在不断优化锁的机制,有些情况下他并不会是一个很重量级的锁了。优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。

锁的状态从低到高依次为无锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到高,降级在一定条件也是有可能发生的。

  1. 适应性自旋在早期的版本中,synchronized 锁在发生竞争时会导致线程进入阻塞状态,这会引入较大的性能开销。从 JDK 1.6 开始,引入了适应性自旋的概念。当线程尝试获取锁时,如果锁没有被其他线程占用,它会自旋一段时间,而不会立即阻塞。自旋的时间会逐渐增加或逐渐减少,以便更好地适应锁的竞争情况。
  2. 锁膨胀JDK 1.6 以后的版本中,JVM 对连续的 synchronized 块进行了锁膨胀优化。如果多个 synchronized 块之间没有竞争关系,JVM 可以将它们合并成一个更大的同步块,从而减少锁的粒度,提高性能。
  3. 锁消除如果分析器确定某些锁不会发生竞争,JVM 可以在运行时将这些锁消除掉,以减少不必要的同步操作。这个优化主要针对局部变量,因为局部变量的作用范围较小,通常不会被多个线程访问。
  4. 偏向锁JDK 1.6 引入了偏向锁的概念,用于提高单线程场景下的性能。偏向锁会在锁对象的对象头中记录下拥有锁的线程标识,当线程再次访问这个锁时,无需进行竞争,而是直接获取锁。这对于短暂的同步操作非常有用,因为它减少了竞争。
  5. 轻量级锁JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。

16.Synchronized与Lock的区别

实现方式

  • synchronized 是 Java 语言内置的关键字,它的实现是由 Java 虚拟机自动处理的,不需要程序员手动释放锁。使用 synchronized 时,锁的获取和释放都由 JVM 隐式管理。
  • Lock 是一个接口,它在 java.util.concurrent.locks 包中定义。Lock 的实现需要程序员显式地获取和释放锁,这给了程序员更多的灵活性和控制权。

锁的粒度

  • synchronized 锁的粒度较粗,通常用于对整个方法或代码块进行同步。
  • Lock 可以实现更细粒度的锁定,允许程序员选择在代码中的特定部分加锁,从而提高并发性能。

可中断性

  • synchronized 不支持锁的中断。一旦一个线程进入了 synchronized 块,其他线程无法中断它,只能等待锁的释放。
  • Lock 支持锁的中断。在使用 Lock 时,其他线程可以通过调用 lockInterruptibly() 方法来中断等待锁的线程。

超时等待

  • Lock 可以支持锁的超时等待。通过 tryLock(long time, TimeUnit unit) 方法,可以设置等待锁的超时时间。
  • synchronized 不支持锁的超时等待。

多条件(Condition)

  • Lock 可以支持多个条件(Condition),每个条件可以实现不同的等待和通知机制。
  • synchronized 不支持多条件等待,它只提供了单一的等待和通知机制。

性能

  • 在低竞争情况下,synchronized 通常性能较好,因为它由 JVM 紧密控制,不需要额外的线程切换和上下文切换。
  • 在高度竞争的多线程环境中,Lock 可能表现得更好,因为它允许更细粒度的控制,减少了锁争用。

17.乐观锁和悲观锁的区别?

悲观锁

  • 思想:悲观锁认为在并发环境下,数据很可能会被其他线程修改,因此它在访问共享资源之前会先获取锁,并假设其他线程会对数据进行修改。如果锁被其他线程持有,当前线程会被阻塞,直到获取到锁才能访问数据。
  • 适用场景:适用于写操作较多的情况,因为悲观锁可以确保数据的一致性和安全性。常见的悲观锁包括数据库中的行级锁和表级锁。
  • 缺点:悲观锁可能导致并发性能下降,因为它在访问数据之前需要获取锁,可能会引发大量的阻塞。

乐观锁(Optimistic Locking)

  • 思想:乐观锁认为在并发环境下,数据修改的冲突较少,因此它不会立即获取锁,而是直接进行操作。当更新数据时,它会检查数据的版本号或标记,如果在操作过程中数据被其他线程修改,则操作会失败,需要重试。乐观锁相信冲突较少,因此不会立即对数据进行加锁。
  • 适用场景:适用于读操作较多的情况,因为乐观锁不会引起阻塞,只在冲突发生时进行回滚和重试。常见的乐观锁实现包括版本号控制和 CAS(Compare and Swap)操作。
  • 缺点:乐观锁需要处理冲突,可能需要进行多次尝试,因此在高并发写入的情况下,可能会导致性能下降。

18.什么是自旋锁?它有什么优缺点?

自旋锁(Spin Lock)是一种基于忙等待的锁,它不会让线程阻塞,而是会让线程在一定的时间内反复尝试获取锁。自旋锁的主要思想是,如果锁已经被其他线程占用,那么当前线程将自旋等待,不断尝试获取锁,直到成功为止。自旋锁通常适用于锁被占用的时间非常短暂的情况,以减少线程切换和上下文切换的开销。

自旋锁的优点:

  1. 低延迟**:** 自旋锁不会让线程阻塞,因此不需要进行线程的挂起和恢复操作,避免了线程切换的开销。对于锁的占用时间很短的情况,自旋锁的性能往往比较好。
  2. 简单轻量**:** 自旋锁的实现通常比较简单,不依赖于操作系统的特性,因此比较轻量。
  3. 可避免死锁**:** 自旋锁不会造成死锁,因为线程在获取锁失败后会一直尝试,直到成功为止。

自旋锁的缺点:

  1. 高 CPU 开销**:** 自旋锁需要不断地尝试获取锁,如果锁的占用时间较长,那么自旋的线程会浪费大量的 CPU 时间,导致系统的 CPU 使用率升高。
  2. 无法处理线程数过多**:** 当并发线程数非常大时,自旋锁可能不适用,因为过多的线程在自旋等待锁时会竞争 CPU 时间,降低系统的整体性能。

因此,自旋锁适用于锁的占用时间短、线程竞争不激烈的情况,但在其他情况下,可能需要考虑使用其他类型的锁,如互斥锁或读写锁。

19.什么是适应性自旋锁?相比自旋锁它有什么优点?

适应性自旋锁是一种自旋锁的变种,它具有动态调整自旋等待时间的能力。适应性自旋锁旨在解决自旋锁在不同场景下的性能问题,以提高锁的效率。

适应性自旋锁的主要思想是根据锁的争用情况来动态调整自旋等待的时间。它通常会跟踪自旋等待的次数,当锁经常被争用时,自旋等待的时间会逐渐延长,以减少无效的自旋,降低 CPU 的浪费。当锁很少被争用时,自旋等待的时间会逐渐减少,以提高响应速度。

相比自旋锁,适应性自旋锁的优点有如下几个:

  1. 适应性: 适应性自旋锁能够根据实际的锁争用情况自动调整自旋等待时间,从而更好地适应不同的工作负载和并发情况。
  2. 降低 CPU 开销: 通过动态调整自旋等待时间,适应性自旋锁能够降低 CPU 的浪费,提高系统的性能。
  3. 提高响应速度: 当锁很少被争用时,适应性自旋锁会减少自旋等待时间,从而提高对锁的竞争响应速度。

自旋锁的开启

  • JDK1.6 中-XX:+UseSpinning 开启; -XX:PreBlockSpin=10 为自旋次数;
  • JDK1.7 后,去掉此参数,由 jvm 控制;

20.对比 synchronized ,Lock 有哪些优势?

更灵活的锁定方式

  • synchronized 关键字只支持一种锁定方式,即排他锁。而 Lock 接口的实现类(如 ReentrantLock)提供了更多的灵活性,可以实现不同种类的锁定方式,包括可重入锁、读写锁、公平锁、非公平锁等,可以根据具体需求选择合适的锁。

更强大的线程等待/通知机制

  • Lock 接口提供了条件变量(Condition),可以实现更复杂的线程等待和通知机制。这使得线程可以更精细地控制等待和唤醒的条件,而 synchronized 关键字只支持 wait() 和 notify(),相对较简单。

可中断的锁

  • Lock 接口的锁可以响应中断,即在一个线程等待锁的过程中,其他线程可以通过调用 interrupt() 方法中断等待线程。这在处理死锁等情况时很有用,而 synchronized 无法中断等待线程。

超时获取锁

  • Lock 接口允许线程尝试获取锁一段时间后放弃,而不是无限期等待。这可以防止线程因无法获取锁而一直阻塞。

可替代性

  • Lock 接口的实现类提供了可替代 synchronized 的锁定方式,因此可以用于替代旧的代码中的 synchronized 关键字,而不需要改变整体的代码结构。

性能优化

  • 在高并发情况下,Lock 的实现类通常具有更好的性能,因为它们采用了一些优化技术,如分段锁、自旋锁等,以减少线程竞争和上下文切换。

21.什么是ReadWriteLock?

ReadWriteLock 是 Java 中用于支持读写锁机制的接口。

读写锁允许多个线程同时读取共享资源,但在写操作时需要排他锁,即只有一个线程可以写入。这种机制适用于读多写少的场景,可以提高并发性能,因为多个线程可以同时读取共享数据,而写操作仍然是互斥的。

ReadWriteLock 接口定义了两个基本方法:

  1. readLock()****: 返回一个读锁,多个线程可以同时持有读锁,只要没有线程持有写锁。读锁允许并发读取操作,不会阻塞其他读线程,只有在有写线程持有写锁时才会阻塞。
  2. writeLock()****: 返回一个写锁,写锁是独占的,只有一个线程可以持有写锁。当有线程持有写锁时,其他线程无法获取读锁或写锁,从而保证了写操作的互斥性。

AQS

22.什么是AQS?

AQS(AbstractQueuedSynchronizer)是 Java 中用于构建同步器的抽象基类,它提供了一种基于队列的、可扩展的同步机制的框架。AQS是Java并发包(java.util.concurrent)中重要的一部分,它被广泛用于实现各种同步工具,如锁、信号量、倒计数器、读写锁等。

AQS的主要作用是提供了一个框架,使得开发者可以相对容易地构建自定义的同步器。它实现了底层的线程排队和等待机制,开发者只需关注同步状态的管理和线程的排队等待即可,无需重复编写底层的线程调度逻辑。

AQS的核心思想是将线程排队在一个FIFO(先进先出)的队列中,每个线程都是一个Node节点,线程需要获得同步资源时,会加入队列,并且以一种非常高效的方式排队。当同步资源可用时,AQS会按照FIFO的顺序唤醒队列中的线程。

AQS的子类需要实现以下两种操作来管理同步状态:

  1. 获取(acquire):当一个线程请求获取同步资源时,如果资源不可用,线程会进入等待状态,并排队在AQS的等待队列中,直到资源可用。获取操作通常会涉及到线程的排队和等待。
  2. 释放(release):当一个线程释放同步资源时,AQS会负责唤醒等待队列中的线程,并将同步资源分配给其中一个线程。释放操作通常会导致等待线程中的某一个被唤醒并获得资源。

23.AQS 支持哪几种同步方式?

独占锁(Exclusive Locking):

  • 通过独占锁方式,只有一个线程能够获得锁,其他线程需要等待该线程释放锁才能获取。
  • ReentrantLock 是 AQS 最常见的独占锁的实现。

共享锁(Shared Locking):

  • 共享锁方式允许多个线程同时获取锁,常用于读多写少的场景,可以提高并发性。
  • ReadWriteLock 是 AQS 提供的支持共享锁的接口,ReentrantReadWriteLock 是其常见的实现。

CAS

24.CAS的原理呢?

CAS(Compare and Swap,比较并交换)主要是通过处理器的指令来保证操作的原子性。

CAS操作有三个操作数:

  1. 变量内存地址,V表示
  2. 旧的预期值,A表示
  3. 准备设置的新值,B表示

当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。

CAS操作是一种乐观锁机制,它可以在不使用锁的情况下实现线程安全。

CAS操作的原子性是由硬件提供的,通常是利用处理器的原子指令实现的,这确保了在多线程环境中,只有一个线程能够成功执行CAS操作,其他线程需要重试。CAS操作通常用于实现无锁数据结构,如并发队列、计数器、自旋锁等。

25.CAS有什么缺点吗?

  1. ABA问题:ABA问题是CAS的一个主要缺点。如果一个共享变量的值从A变为B,然后又从B变回A,在CAS中无法检测到这种变化,因为只比较了最新值与期望值。这可能导致错误的结果,特别是在一些数据结构中,如无锁栈或队列,ABA问题可能会破坏数据结构的一致性。为了解决ABA问题,可以使用版本号、时间戳等方式来增加额外的检查。
  2. 循环时间长开销大:CAS操作在失败时需要重试,这会导致线程进入自旋等待状态,浪费CPU资源。如果CAS操作经常失败,自旋的开销可能会变得很大。
  3. 只能保证一个共享变量的原子性:CAS操作通常只能应用于单个共享变量的原子操作。如果需要在多个变量之间执行复合操作,CAS可能不够强大。
  4. 容易造成性能问题:过度使用CAS可能会导致性能问题。在高并发环境下,大量线程重试CAS操作可能会导致竞争激烈,性能下降。

ThreadLocal

26.ThreadLocal的核心原理是什么?

ThreadLocal 提供了一种线程本地存储的机制,允许每个线程拥有自己独立的变量副本。 其核心原理如下:

每个线程拥有自己的变量副本

当你创建一个 ThreadLocal 变量时,实际上创建了一个线程局部变量。每个线程都有自己独立的变量副本,这个副本存储在线程的线程栈中。这意味着不同线程可以同时访问同一个 ThreadLocal 变量,但它们访问的是各自线程内部的副本。

ThreadLocalMap 存储变量副本

ThreadLocal 内部维护了一个 ThreadLocalMap,它是一个哈希表,用于存储线程本地变量的副本。ThreadLocal 对象是哈希表的键,而每个线程的变量副本则存储在哈希表的值中。这样可以确保每个线程访问 ThreadLocal 变量时都能获得自己的副本。

线程获取变量时使用 ThreadLocal 的引用

当线程访问 ThreadLocal 变量时,实际上是通过 ThreadLocal 对象的引用来获取自己线程内部的变量副本。这个引用充当了键,用于在 ThreadLocalMap 中查找对应的线程本地变量副本。

自动内存管理

ThreadLocal 变量的生命周期受到线程的生命周期的限制。一旦线程终止,与之关联的线程本地变量副本也会被垃圾回收,从而避免内存泄漏。

27.ThreadLocal什么时候会出现OOM的情况?为什么?

  1. 长时间不清理: 如果你在 ThreadLocal 中存储大量数据,而且不在使用完后调用 remove() 方法来清理线程局部变量,这些数据可能会一直存活在线程的上下文中,导致内存泄漏。
  2. 线程池中的线程: 当使用线程池时,线程在执行完任务后可能会被重新用于执行其他任务,但 ThreadLocal 中的数据不会自动清理。如果线程池中的线程长时间存在,并且 ThreadLocal 中的数据不断累积,可能会导致内存泄漏。
  3. 使用过多的ThreadLocal: 如果在应用程序中滥用 ThreadLocal,为每个线程创建大量的 ThreadLocal 变量,可能会导致内存消耗过大,最终触发 OutOfMemoryError。
  4. 没有及时清理线程局部变量: 如果你没有在使用完 ThreadLocal 变量后及时调用 remove() 方法,那么线程局部变量中的数据将保持活动状态,可能会导致内存泄漏。

为避免 ThreadLocal 导致内存泄漏或 OutOfMemoryError,应该遵循以下最佳实践:

  1. 在使用完 ThreadLocal 变量后调用 remove() 方法: 在适当的时候,通常是在任务执行结束后,手动调用 ThreadLocal 的 remove() 方法来清理线程局部变量,以释放资源。
  2. 避免滥用 ThreadLocal: 不要过度使用 ThreadLocal,只在有必要的情况下使用,确保每个线程只创建少量的 ThreadLocal 变量。
  3. 小心使用线程池: 如果使用线程池,请确保在执行线程任务前清理线程局部变量,以防止线程池中的线程重用导致的数据混乱。
  4. 使用弱引用(WeakReference): 如果你需要在某些情况下存储大量数据,可以考虑使用弱引用来管理 ThreadLocal 变量,以便更容易进行垃圾回收。

并发工具类

28.CyclicBarrier 和 CountDownLatch 的区别

用途不同:

  • CountDownLatch 用于一个或多个线程等待其他线程完成一组操作,然后继续执行。通常,一个或多个线程等待一个计数器减到零。
  • CyclicBarrier 用于一组线程相互等待,直到所有线程都达到某个同步点,然后继续执行。它通常用于一组线程分阶段地执行某个任务,每个阶段都需要等待所有线程完成,然后进入下一个阶段。

计数方式不同:

  • CountDownLatch 是一种单次计数器,一旦计数达到零,就不能再次使用。每次调用countDown()方法减少计数。
  • CyclicBarrier 是一种可重用的计数器,可以在重置后再次使用。当所有线程到达栅栏点后,栅栏会自动重置,计数器重新开始计数。

异常处理不同:

  • CountDownLatch 不提供异常处理机制。如果等待线程在计数减为零之前抛出异常,它将无法被捕获。
  • CyclicBarrier 可以选择在构造时传递一个Runnable,当栅栏打破时,该Runnable会被执行,可以用于处理异常情况。

适用场景不同:

  • CountDownLatch 适用于一组线程中的某个线程需要等待其他线程执行完特定任务后再执行的场景,或者用于等待某些资源初始化完成。
  • CyclicBarrier 适用于一组线程需要在某个同步点相互等待,然后一起继续执行的场景,常用于多阶段任务的并行化执行。

使用方式不同:

  • CountDownLatch 中的等待线程通过调用await()方法等待计数器减为零。
  • CyclicBarrier 中的线程通过调用await()方法告诉栅栏它已经到达,然后等待其他线程也到达,当所有线程都到达后,栅栏会自动打破,线程继续执行。

Future

29.什么是 Future?

Future 是 Java 中的一个接口,用于表示一个异步计算的结果。它允许你在一个线程中提交一个任务,然后在其他线程中等待该任务的执行结果。Future 提供了一种异步编程的方式,可以在执行计算任务的同时执行其他操作,而不必等待计算完成。

30.什么是FutureTask?

FutureTask 是 Java 中一个用于表示一个异步计算任务的实用类。它实现了 Future 接口,同时也可以被用作 Runnable,因此它可以作为一个可执行的任务被提交给线程池执行,也可以用于获取异步计算的结果。

FutureTask 的主要作用有两个:

  1. 表示异步计算的结果: 你可以将一个计算任务封装到 FutureTask 中,然后将 FutureTask 提交给一个线程池或线程执行器来执行。通过 FutureTask,你可以在任务执行完成后获取到任务的执行结果。
  2. 提供了异步计算的控制: FutureTask 本身实现了 Runnable 接口,因此可以被提交给线程池执行。你可以使用 isDone() 方法来检查任务是否已完成,使用 get() 方法来等待并获取计算结果,或者使用 cancel() 方法来取消任务的执行。

Java 并发编程 面试题及答案整理,最新面试题

分类:最新,面试题及答案整理 标签:Java并发编程

Java中的volatile关键字有什么作用?

volatile关键字在Java中的作用包括:

1、保证可见性: 确保变量的修改对其他线程立即可见。

2、防止指令重排: 防止编译器对操作进行重排序,保证代码的执行顺序。

3、非原子性: volatile变量的单次读/写操作是原子的,但复合操作(如i++)不是原子的。

Java中的synchronized关键字及其工作原理。

synchronized关键字在Java中的作用及其工作原理:

1、互斥锁: synchronized提供了一种锁机制,能够确保同一时刻只有一个线程执行某段代码。

2、对象锁和类锁: 可以锁定对象实例(方法或代码块)或整个类(静态方法)。

3、内存可见性: 保证了锁内操作对其他线程的可见性。

4、锁升级: 在JVM中,synchronized可能经历偏向锁、轻量级锁和重量级锁的升级。

Java中的CAS操作是什么?它如何实现无锁编程?

CAS(Compare-And-Swap)操作在Java中的含义及无锁编程实现:

1、原子操作: CAS是一种基于比较和交换的原子操作,用于实现无锁编程。

2、实现方式: 通过循环比较当前值和预期值,如果相同则更新为新值。

3、无锁优势: 减少线程阻塞,提高系统吞吐量。

4、ABA问题: CAS可能面临ABA问题,可以通过版本号等机制解决。

讲述Java中的Lock接口及其与synchronized的区别。

Java中的Lock接口及其与synchronized的区别:

1、显式锁定: Lock是一个接口,提供了比synchronized更灵活的锁定机制。

2、可中断锁定: Lock允许尝试非阻塞地获取锁,或者在锁定期间响应中断。

3、公平性选择: Lock提供了选择公平锁或非公平锁的能力。

4、性能差异: 在不同情况下,Lock和synchronized的性能表现有所不同。

什么是线程池?在Java中如何使用线程池?

线程池及其在Java中的使用:

1、线程复用: 线程池是一种限制和管理线程数量的机制,可以复用线程。

2、减少开销: 减少创建和销毁线程的性能开销。

3、使用方式: 通过Executor框架中的Executors类创建,例如Executors.newFixedThreadPool()。

4、任务提交: 将实现了Runnable或Callable接口的任务提交给线程池执行。

Java内存模型中的happens-before原则是什么?

Java内存模型中的happens-before原则:

1、定义: 是一种保证内存可见性和有序性的规则。

2、作用: 确保在一个线程中的操作对另一个线程可见。

3、实例: 如对一个volatile变量的写操作,happens-before于随后对这个变量的读操作。

Java中synchronized和ReentrantLock有什么区别?

Java中synchronized和ReentrantLock的区别主要体现在以下几个方面:

1、锁的实现方式: synchronized是Java内置的关键字,JVM层面实现;ReentrantLock是Java类库中的一个API。

2、锁的公平性: synchronized不保证公平性;而ReentrantLock可以通过构造函数设置公平性。

3、锁的灵活性: ReentrantLock提供更灵活的锁操作,它支持中断锁的获取、超时获取锁等高级功能。

4、条件变量支持: ReentrantLock可以与Condition类配合,实现分组唤醒等复杂的线程协作。

Java中volatile关键字的作用是什么?

volatile关键字在Java中的作用包括:

1、保证变量的可见性: 当一个共享变量被volatile修饰后,它会确保线程对这个变量的读写都是直接操作内存,而不是缓存。

2、防止指令重排序: volatile还可以防止指令重排序,保证程序的执行顺序。

Java中的CAS操作。

CAS(Compare-And-Swap)操作在Java中是一种重要的并发原语,其工作机制如下:

1、原子性操作: CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相同,那么处理器会自动将该位置值更新为新值。

2、无锁优化: CAS提供了一种无锁操作的方式,可以在不使用传统锁的情况下实现线程安全。

3、ABA问题: CAS操作可能会遇到ABA问题,即值原来是A,变成了B,后又变回A,CAS会误认为值没有改变。

Java中CountDownLatch和CyclicBarrier有什么区别?

CountDownLatch和CyclicBarrier在Java中的区别包括:

1、用途差异: CountDownLatch主要用于一个或多个线程等待其他线程完成操作;而CyclicBarrier主要用于多个线程间相互等待,直到所有线程都达到公共屏障点。

2、可重用性: CountDownLatch是一次性的,计数器达到零后不能重置;CyclicBarrier可以重置,因此可以重复使用。

3、方法差异: CountDownLatch主要用countDown()和await()方法;CyclicBarrier主要通过await()方法使线程在屏障处等待。

Java程序中正确地停止一个线程?

在Java程序中正确停止一个线程的方法:

1、使用中断: 调用线程的interrupt()方法来设置线程的中断状态;线程需要定期检查自身的中断状态,并相应地响应中断。

2、使用标志位: 设置一个需要线程检查的标志位,线程周期性地检查该标志,以决定是否停止运行。

3、避免使用stop()方法: 不建议使用Thread类的stop()方法来停止线程,因为它是不安全的。

Java中线程池的作用是什么?它是如何提高效率的?

线程池在Java中的作用及其效率提升机制:

1、资源重用: 线程池通过重复使用预创建的线程来减少线程创建和销毁的开销。

2、控制资源消耗: 线程池可以限制系统中并发执行线程的数量,有效控制系统资源的消耗。

3、提高响应速度: 预创建的线程可以立即执行任务,无需等待线程创建。

4、提供更多高级功能: 线程池还提供任务排队、定时任务执行、线程池监控等高级功能。

Java中的ThreadLocal变量及其使用场景。

ThreadLocal在Java中的作用及使用场景:

1、作用: ThreadLocal提供线程局部变量,这种变量在每个线程中都是独立的,一个线程对ThreadLocal变量的修改不会影响其他线程。

2、使用场景: 常用于实现线程安全,尤其是在多线程环境中对于那些需要避免共享的变量。例如,在Web应用中存储用户的会话信息、数据库连接等。

Java中的CountDownLatch和它的用途。

CountDownLatch在Java中的用途和解释:

1、同步辅助类: CountDownLatch是一个同步辅助类,用于在完成一组正在其他线程中执行的操作之前,允许一个或多个线程等待。

2、计数器: 它维护一个计数器,初始化时设定计数,调用countDown()方法会减少计数器,而await()方法会阻塞,直到计数器为零。

3、用途: 用于控制一组线程等待某个事件发生后再全部同时继续执行。

什么是CyclicBarrier,它与CountDownLatch有什么区别?

CyclicBarrier的定义及与CountDownLatch的区别:

1、定义: CyclicBarrier是一个同步辅助类,它允许一组线程相互等待,直到所有线程都达到了公共屏障点(Barrier)。

2、可重用性: CyclicBarrier与CountDownLatch的一个主要区别是它可以重用,当所有等待线程都释放后,可以重置屏障再次使用。

3、用途: CyclicBarrier通常用于一组线程互相等待至某个状态之后再全部同时执行。

4、动作执行: CyclicBarrier可以在所有线程到达屏障时优先执行一个预定义的动作。

Java中的Semaphore及其主要用途。

Semaphore在Java中的解释及主要用途:

1、定义: Semaphore是一个计数信号量,用来控制同时访问某个特定资源的操作数量,主要用于实现资源的并发限制。

2、使用方法: 通过acquire()方法获取一个许可,如果无可用许可,acquire()将会阻塞直到有许可;release()方法释放许可。

3、主要用途: 用于控制资源的访问数量,如限制文件的同时读写数量。

Java中如何实现线程的安全终止?

在Java中实现线程安全终止的方法:

1、中断机制: 使用interrupt()方法设置线程的中断状态,线程可以定期检查这个状态并优雅地关闭自己。

2、标志变量: 设置一个标志变量,线程通过检查这个变量来决定是否退出。

3、使用Future.cancel() 如果线程是通过ExecutorService提交的,可以使用Future.cancel()方法来安全地终止线程。

Java中的ReentrantLock和synchronized有什么不同?

ReentrantLock与synchronized在Java中的不同点:

1、灵活性: ReentrantLock提供比synchronized更多的灵活性,可以尝试非阻塞地获取锁、获取可中断锁,以及超时尝试锁。

2、公平性: ReentrantLock可以配置为公平锁,而synchronized总是非公平的。

3、条件变量: ReentrantLock提供Condition类,可以分开管理不同的等待线程集。

4、锁状态查询: ReentrantLock可以查询锁是否被持有。

Java中的ReadWriteLock是什么?它如何提高应用程序的性能?

ReadWriteLock在Java中的定义及性能提升方式:

1、定义: ReadWriteLock维护了一对相关的锁 —— 一个用于只读操作的共享锁和一个用于写操作的排他锁。

2、读写分离: 允许多个读线程同时访问,但写线程访问时,所有的读线程和其他写线程都会被阻塞。

3、性能提升: 在读多写少的场景中,ReadWriteLock可以提高程序性能,因为它允许多个线程同时读取数据,而不是像synchronized那样互斥访问。

Java中的ThreadLocal是什么,它是如何工作的?

ThreadLocal在Java中的定义及工作原理:

1、定义: ThreadLocal用于创建线程局部变量,每个访问该变量的线程都有一个独立初始化的副本。

2、工作原理: ThreadLocal为每个线程提供独立的变量副本,实现线程间数据的隔离。

3、用途: 常用于保存线程私有数据,如用户身份信息、事务状态等。

4、内存泄漏问题: 使用不当时可能会导致内存泄漏,应确保及时调用remove()方法清理资源。

Java并发编程中的synchronized和volatile关键字的不同用途。

在Java并发编程中,synchronized和volatile关键字的不同用途:

1、synchronized: 提供互斥锁功能,确保只有一个线程可以执行某段代码,主要用于实现线程间的同步和防止竞争条件。

2、volatile: 保证变量的内存可见性,确保一个线程修改的变量值对其他线程立即可见,用于轻量级的同步场景,但不解决原子性问题。

Java中的Thread.join()方法有什么作用?

Thread.join()方法在Java中的作用:

1、等待线程终止: join()方法允许一个线程等待另一个线程完成其执行。

2、确保顺序: 使用join()可以确保程序中线程的执行顺序。调用线程将会在join()调用的线程完成执行后继续执行。

Java中如何使用wait()和notify()方法?

在Java中使用wait()和notify()方法的方式:

1、wait(): 调用wait()使当前线程等待,直到其他线程调用此对象的notify()或notifyAll()方法。

2、notify(): 唤醒在此对象监视器上等待的单个线程。

3、同步块内使用: wait()和notify()必须在同步块或同步方法内部使用。

4、释放锁: 调用wait()会释放锁,而notify()不会释放锁。

Java并发编程中的Exchanger是什么?

Java并发编程中的Exchanger:

1、定义: Exchanger是一个用于线程间协作的工具类,用于进行线程间的数据交换。

2、用途: 它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。

Java中的StampedLock有什么特点?

Java中的StampedLock的特点:

1、读写锁: StampedLock提供了一种读写锁的实现。

2、锁升级和降级: 支持从读锁升级到写锁,以及写锁降级到读锁。

3、优化读性能: 提供了一种乐观读锁的机制,可以提升并发性能。

Java并发编程中使用Future和Callable有什么好处?

在Java并发编程中使用Future和Callable的好处:

1、异步执行: Callable代表一个有返回值的任务,Future可以用来获取这个任务的结果,实现异步执行。

2、获取结果: Future.get()方法用来获取Callable任务的执行结果。

3、异常处理: Future.get()会抛出执行中的异常,方便异常处理。

4、超时控制: Future.get()方法提供了超时控制的机制,防止无限等待。

Java中的Thread类和Runnable接口有什么区别?

Java中Thread类和Runnable接口的区别:

1、继承与实现: Thread是一个类,继承它需要使用extends关键字;Runnable是一个接口,实现它需要使用implements关键字。

2、多重继承: Java不支持多重继承。如果一个类已经继承了其他类,就不能再继承Thread,但它可以实现Runnable接口。

3、资源共享: 实现Runnable接口的方式更适合多个线程共享同一资源的情况。

Java中的内存模型以及其对并发编程的影响。

Java内存模型(JMM)及其对并发编程的影响:

1、可见性: JMM定义了线程如何和何时可以看到其他线程写入的共享变量的值,关键字如volatile在此起作用。

2、原子性: JMM定义了哪些操作是原子性的,即不可分割的。

3、顺序性: JMM通过happens-before原则,定义了一个线程对共享数据的写入何时对其他线程可见。

Java中的Executor框架及其优点。

Java中的Executor框架及其优点:

1、简化线程管理: Executor框架通过提供线程池管理机制,简化了线程的创建和分配。

2、资源优化: 通过线程复用,减少了线程创建和销毁的性能开销。

3、提供调度和控制: Executor框架提供了灵活的线程调度和执行控制机制。

Java并发编程中,如何保证操作的原子性?

在Java并发编程中保证操作的原子性的方法:

1、synchronized关键字: 通过同步代码块或方法来保证操作的原子性。

2、Lock接口: 使用ReentrantLock等锁实现提供的锁机制。

3、原子变量: 使用java.util.concurrent.atomic包中的原子变量类。

Java中的并发集合类有哪些,它们是如何实现线程安全的?

Java中的并发集合类及其线程安全实现:

1、ConcurrentHashMap: 使用分段锁技术提高并发访问效率。

2、CopyOnWriteArrayList: 写操作时复制数据,读操作无锁,适用于读多写少的场景。

3、BlockingQueue: 提供阻塞的插入和移除方法,用于生产者消费者模式。

Java中的死锁以及如何避免。

Java中的死锁及其避免方法:

1、死锁定义: 多个线程因互相等待对方持有的锁而无法继续执行。

2、避免方法:

  • 避免一个线程同时获取多个锁。
  • 设置锁获取的超时时间。
  • 按顺序申请资源。

Java中使用wait()和notify()方法?

在Java中使用wait()和notify()方法:

1、wait(): 调用该方法的线程释放锁并进入等待状态,直到其他线程调用同一对象的notify()或notifyAll()。

2、notify()/notifyAll(): 唤醒在该对象上等待的单个线程(notify())或所有线程(notifyAll())。

3、同步块: wait()和notify()必须在同步块或同步方法内部调用。

Java中的AQS是什么,它是如何工作的?

Java中的AQS(AbstractQueuedSynchronizer)及其工作原理:

1、AQS定义: AQS是一个用于构建锁和同步器的框架,提供了一个基于FIFO队列的完整的阻塞锁实现。

2、工作原理:

  • 状态管理:AQS内部维护一个状态变量来表示同步状态。
  • 独占和共享:AQS支持两种同步模式:独占模式和共享模式。
  • 队列维护:线程获取同步状态失败后,会被加入到一个等待队列中,依次获取锁。

Java中的Fork/Join框架是什么?它是如何工作的?

Java中的Fork/Join框架及其工作原理:

1、定义: Fork/Join框架是Java 7引入的一种并行执行任务的框架,主要用于递归任务和大数据集的处理。

2、工作原理: 它基于"分而治之"的原则,将大任务分解(fork)成小任务,再并行执行这些任务,最后将小任务结果合并(join)成大任务结果。

3、工作窃取算法: 使用工作窃取算法优化任务的执行,空闲线程可以从其他线程的任务队列中偷取任务来执行。

Java中的阻塞队列及其如何用于并发编程。

Java中的阻塞队列及其在并发编程中的应用:

1、定义: 阻塞队列是一种支持两个附加操作的队列,这两个操作包括:在队列为空时阻塞获取元素的操作和在队列满时阻塞插入元素的操作。

2、用途: 常用于生产者-消费者模式,其中生产者不能向队列添加元素如果队列满了(会阻塞),消费者不能从队列取元素如果队列空了(也会阻塞)。

Java中的AQS(AbstractQueuedSynchronizer)是什么?

Java中的AQS(AbstractQueuedSynchronizer):

1、定义: AQS是提供了一套用于开发锁和同步器的框架,它利用了一个int成员变量表示同步状态,以及一个FIFO队列来管理线程。

2、作用: AQS是实现锁和其他同步组件的基础,例如ReentrantLock, Semaphores, CountDownLatch等。

Java中的并发集合类及其优势。

Java中的并发集合类及其优势:

1、定义: 并发集合类是专门设计用于多线程环境下的集合类,如ConcurrentHashMap, CopyOnWriteArrayList等。

2、优势: 这些集合类在多线程环境下提供了更好的性能,同时保证了线程安全和高效的数据操作。

Java中的ThreadLocalRandom是什么?它解决了什么问题?

Java中的ThreadLocalRandom及其解决的问题:

1、定义: ThreadLocalRandom是Java并发包中的一个用于生成随机数的类,它是对java.util.Random的改进。

2、解决的问题: 解决了在多线程环境下使用普通Random实例时可能出现的竞争条件问题,提高了性能。

Java并发编程中的SynchronousQueue是什么?

Java并发编程中的SynchronousQueue:

1、定义: SynchronousQueue是一个没有存储空间的阻塞队列,每一个put操作必须等待一个take操作,反之亦然。

2、用途: 常用于传递数据,非常适合传递性的场景,如任务分发。

Java中的CompletableFuture及其优势。

Java中的CompletableFuture及其优势:

1、定义: CompletableFuture是Java 8引入的一个类,用于编写异步代码。

2、优势: 提供了非阻塞的方式来完成操作并获得结果,支持函数式编程,可以通过lambda表达式轻松管理异步任务。

3、组合式异步编程: 允许将多个异步操作串联或合并,提高代码的可读性和可维护性。

Java并发编程中的Phaser是什么,它与CyclicBarrier和CountDownLatch有何区别?

Java并发编程中的Phaser及其与CyclicBarrier和CountDownLatch的区别:

1、Phaser: 一个可重用的同步屏障,与CyclicBarrier类似,但提供更灵活的控制。支持动态地增加或减少线程。

2、CyclicBarrier vs Phaser: CyclicBarrier用于固定数量的线程等待彼此,而Phaser可以适应动态数量的线程。

3、CountDownLatch vs Phaser: CountDownLatch是一次性的,用于一个或多个线程等待其他线程完成操作,而Phaser可以在多个阶段重用。

Java中的Executor框架是什么?

Java中的Executor框架:

1、定义: Executor框架是Java 5中引入的一种基于线程池的解决方案,用于管理线程资源,提高线程的管理效率和性能。

2、组成: 包括Executor接口及其子接口ExecutorService,以及各种实现类,如ThreadPoolExecutor和ScheduledThreadPoolExecutor。

Java中的LockSupport是什么?它是如何工作的?

Java中的LockSupport及其工作原理:

1、定义: LockSupport是一个提供线程阻塞和唤醒的工具类,它是构建锁和其他同步类的基础。

2、工作原理: 主要提供park()和unpark()方法,park()用于阻塞线程,unpark()用于唤醒指定线程。

Java并发中的CopyOnWriteArrayList和Vector有何不同?

CopyOnWriteArrayList与Vector在Java并发中的不同点:

1、CopyOnWriteArrayList: 在进行修改操作(添加、删除等)时,它会先复制一个新的数组,然后在新数组上修改,修改完毕后,再将原数组引用指向新数组。

2、Vector: 所有操作几乎都是同步的,使用synchronized关键字,每个操作都是线程安全的,但在高并发环境下性能较低。

Java中,什么是线程饥饿?如何防止线程饥饿?

线程饥饿及其预防方法在Java中的定义:

1、线程饥饿: 当线程长时间无法获得所需的资源时,比如CPU时间片或锁,导致无法正常执行。

2、预防方法: 使用公平锁、优先级调度、确保长时间运行的线程能释放资源等方法来防止线程饥饿。

Java中的Thread.yield()方法有什么作用?

Thread.yield()方法在Java中的作用:

1、定义: yield()是一种静态方法,用于提示线程调度器当前线程愿意放弃其当前的时间片。

2、作用: 该方法用于指示线程调度器当前线程愿意让出对处理器的占用,以便其他线程可以更快地执行,但这只是一个提示,没有任何机制保证它会被遵守。

Java中的原子类及其用途。

Java中的原子类及其用途:

1、定义: 原子类是一组在并发环境下保证了原子性操作的类,例如AtomicInteger, AtomicLong, AtomicBoolean等。

2、用途: 提供了一种无锁的方式来进行高性能的线程安全操作,常用于计数器、标志、状态更新等场景。

Java中的synchronized关键字的工作原理。

Java中synchronized关键字的工作原理:

1、锁的获取与释放: synchronized用于方法或代码块,当线程进入synchronized标记的方法或代码块时,它会自动获取锁,并在退出时释放锁。

2、内部锁(监视器锁): synchronized使用对象内部的锁(也称为监视器锁)来实现同步。

3、锁的状态: 当一个线程拥有锁时,其他尝试进入同步代码块的线程将被阻塞,直到锁被释放。

Java中的StampedLock是什么,与ReadWriteLock有何不同?

Java中StampedLock与ReadWriteLock的区别:

1、StampedLock: StampedLock是Java 8引入的,提供了三种模式的读写控制:写锁、悲观读锁和乐观读。

2、ReadWriteLock: ReadWriteLock是传统的读写锁,提供了两种模式:读锁和写锁。

3、乐观读: StampedLock支持乐观读模式,这是ReadWriteLock不支持的。

4、锁降级和升级: StampedLock允许锁的降级和升级,而ReadWriteLock则不支持。

Java中如何正确地使用wait和notify机制?

正确使用Java中的wait和notify机制:

1、在循环中调用wait: 应该在循环中调用wait方法,以避免虚假唤醒。

2、同步块: wait和notify必须在同步块或方法中使用。

3、正确的对象锁: 使用wait和notify时,必须确保线程持有正确对象的锁。

4、notifyAll vs notify: 通常优先使用notifyAll,以唤醒所有等待线程,避免死锁。

Java并发编程中的LockSupport类。

Java并发编程中的LockSupport类:

1、线程阻塞工具: LockSupport提供工具,用于挂起和恢复线程,不需要同步块。

2、park和unpark: park()用于挂起线程,unpark(Thread thread)用于恢复线程。

3、不需要锁和条件: LockSupport的操作不依赖于锁或条件变量,提供更灵活的线程控制。

Java并发编程中,什么是线程饥饿和线程活锁?如何避免它们?

线程饥饿和线程活锁:

1、线程饥饿: 当线程无法获得足够的CPU时间执行任务时发生,通常是由于线程优先级不当或者长时间持有资源导致。

2、线程活锁: 线程不断重试操作,但总是失败,因为其他线程也在做相同的操作。

3、避免方法:

  • 调整线程优先级。
  • 使用公平锁。
  • 避免长时间持有锁。
  • 为重试操作添加随机或指数退避。

Java中Future和CompletableFuture有什么区别?

Future与CompletableFuture的区别:

1、Future: Future提供了对异步操作结果的引用,但它不允许直接对这些结果进行操作。

2、CompletableFuture: CompletableFuture是Java 8中引入的,它实现了Future和CompletionStage接口,提供了方法链和组合式异步编程的能力。

3、方法丰富: CompletableFuture提供了丰富的方法,如thenApply、thenCombine等,使得异步编程更灵活。

Java中的Semaphore是什么,它通常用于哪些场景?

Java中的Semaphore及其使用场景:

1、Semaphore: Semaphore是一个计数信号量,用于控制同时访问特定资源的线程数量。

2、使用场景:

  • 限制资源使用的并发数。
  • 实现资源池,如数据库连接池。

Java中的CountDownLatch和如何使用它。

Java中的CountDownLatch及其使用:

1、CountDownLatch: 一个同步辅助工具类,允许一个或多个线程等待直到在其他线程中进行的一组操作完成。

2、使用方式:

  • 初始化CountDownLatch指定计数。
  • 在等待完成的线程中调用await()。
  • 在完成一个任务的线程中调用countDown(),计数器减一。
  • 当计数器达到零时,等待的线程被释放继续执行。


张洪亮

再就是尼恩 直接看八股文就行

📎极致经典(卷1):Java高并发核心编程(卷1 加强版) -尼恩Java硬核架构班- 特供v15-release(1) (1).pdf📎极致经典(卷2):Java高并发核心编程(卷2 加强版) -尼恩Java硬核架构班- 特供v21-release(1).pdf📎极致经典(卷3):Java高并发核心编程(卷3 加强版) -尼恩Java硬核架构班- 特供v21-release(1).pdf

小林Coding的八股文也整理过来

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值