Java并发面试题&知识点总结(中篇)

2023-11-08 09:31:36 浏览数 (2)

大家好,我是栗筝i,从 2022 年 10 月份开始,我便开始致力于对 Java 技术栈进行全面而细致的梳理。这一过程,不仅是对我个人学习历程的回顾和总结,更是希望能够为各位提供一份参考。因此得到了很多读者的正面反馈。 而在 2023 年 10 月份开始,我将推出 Java 面试题/知识点系列内容,期望对大家有所助益,让我们一起提升。 今天与您分享的,是 Java 并发知识面试题系列的总结篇(中篇),我诚挚地希望它能为您带来启发,并在您的职业生涯中起到助益作用。衷心感谢每一位朋友的关注与支持。


1、Java并发面试题问题
1.1、Java 线程池
  • 问题 21. 简述什么是 Java 线程池
  • 问题 22. 简述 Java 线程池的核心参数
  • 问题 23. 简述 Java 线程池执行流程
  • 问题 24. 简述 Java 线程池拒绝策略
  • 问题 25. 简述 Java 线程池状态
  • 问题 26. 简述 Java 线程池创建方法
  • 问题 27. 简述 Executor 框架
  • 问题 28. 简述 Executor 框架的继承关系
1.2、ThreadLocal
  • 问题 29. 什么是 ThreadLocal?它是如何工作的
  • 问题 30. 介绍一下 InheritableThreadLocal
  • 问题 31. ThreadLocal 为什么会引起内存泄漏,我们该如何预防
1.3、CAS
  • 问题 32. 简述 Java 乐观锁悲观锁的概念
  • 问题 33. 什么是 CAS 操作?什么是 ABA 问题?
1.4、Synchronized
  • 问题 34. 简述 Synchronized 的概念
  • 问题 35. 简述 Synchronized 的使用场景
  • 问题 36. 简述 Synchronized 的锁升级过程
  • 问题 37. 简述 自旋锁与自适应自旋锁
  • 问题 38. 简述 锁膨胀,锁粗化,锁消除
  • 问题 39. 简述 synchronized 和 volatile 的区别
  • 问题 40. 简述 synchronized 与 ReentrantLock 的区别

2、Java并发面试题解答
2.1、Java 线程池
  • 问题 21. 简述什么是 Java 线程池

解答:

Java 线程池是一种用于管理和复用线程的机制。它包含一个线程池和一个任务队列,可以将任务提交给线程池执行。线程池会根据需要创建新的线程,或者复用空闲的线程来执行任务,从而避免了频繁创建和销毁线程的开销。

Java 线程池的主要优点包括:

  1. 提高性能:线程池可以控制并发线程的数量,避免了线程过多导致的资源竞争和上下文切换开销,从而提高了系统的性能。
  2. 提高资源利用率:线程池可以复用线程,避免了频繁创建和销毁线程的开销,提高了系统的资源利用率。
  3. 提供任务队列:线程池可以接收并存储任务,当线程池中的线程空闲时,可以从任务队列中取出任务执行,从而实现任务的异步执行。
  4. 提供线程管理和监控:线程池可以管理线程的生命周期,包括线程的创建、销毁和状态监控等。

Java 线程池的实现类是 java.util.concurrent.ThreadPoolExecutor,它提供了一系列的构造方法和配置参数,可以根据需求来创建不同类型的线程池。常用的线程池类型包括固定大小线程池、缓存线程池和定时任务线程池等。

  • 问题 22. 简述 Java 线程池的核心参数

解答:

Java 线程池的核心参数包括以下几个:

  1. 核心线程数(corePoolSize):线程池中保持的最小线程数。即使线程处于空闲状态,核心线程也不会被销毁。默认情况下,核心线程数为 0。
  2. 最大线程数(maximumPoolSize):线程池中允许的最大线程数。当任务数量超过核心线程数并且任务队列已满时,线程池会创建新的线程来执行任务,直到达到最大线程数。超过最大线程数的任务将被拒绝执行。
  3. 任务队列(workQueue):用于存储待执行任务的队列。当线程池中的线程都在执行任务时,新的任务会被放入任务队列中等待执行。常见的任务队列类型包括有界队列(如 ArrayBlockingQueue)和无界队列(如 LinkedBlockingQueue)。
  4. 线程存活时间(keepAliveTime):当线程池中的线程数量超过核心线程数时,空闲线程的存活时间。超过存活时间的空闲线程将被销毁,以控制线程池的大小。
  5. 拒绝策略(rejectedExecutionHandler):当任务无法被线程池执行时的处理策略。常见的拒绝策略包括抛出异常、丢弃任务、丢弃队列中最旧的任务或在调用者线程中执行任务。

这些核心参数可以通过线程池的构造方法或 setter 方法进行配置。根据具体的需求和场景,可以调整这些参数来优化线程池的性能和行为。

  • 问题 23. 简述 Java 线程池执行流程

解答:

Java 线程池的执行流程如下:

  1. 当有任务提交给线程池时,线程池会首先检查核心线程数是否已满。如果还有空闲的核心线程,则会立即创建一个核心线程来执行任务。
  2. 如果核心线程数已满,线程池会将任务放入任务队列中等待执行。任务队列可以是有界队列或无界队列,根据具体的线程池配置而定。
  3. 如果任务队列已满,且线程池中的线程数还未达到最大线程数,线程池会创建新的线程来执行任务。
  4. 如果线程池中的线程数已达到最大线程数,并且任务队列也已满,根据设定的拒绝策略来处理无法执行的任务。常见的拒绝策略包括抛出异常、丢弃任务、丢弃队列中最旧的任务或在调用者线程中执行任务。
  5. 当线程池中的线程执行完任务后,会继续从任务队列中获取新的任务来执行。如果任务队列为空,空闲的线程会等待新的任务到来。
  6. 如果线程池中的线程空闲时间超过设定的存活时间(keepAliveTime),则这些空闲线程会被销毁,以控制线程池的大小。

通过合理配置线程池的核心参数,可以实现任务的异步执行、线程的复用和资源的合理利用,从而提高系统的性能和响应能力。

  • 问题 24. 简述 Java 线程池拒绝策略

解答:

Java 线程池的拒绝策略用于处理无法执行的任务。当线程池中的线程数已达到最大线程数,并且任务队列也已满时,线程池会根据设定的拒绝策略来处理无法执行的任务。以下是常见的拒绝策略:

  1. AbortPolicy(默认):抛出 RejectedExecutionException 异常,表示拒绝执行该任务。
  2. CallerRunsPolicy:将任务返回给提交任务的线程执行。也就是说,如果线程池无法执行任务,它会将任务退回给调用者线程来执行。
  3. DiscardPolicy:直接丢弃无法执行的任务,不做任何处理。
  4. DiscardOldestPolicy:丢弃队列中最旧的任务,然后尝试执行新的任务。

可以根据具体的业务需求选择合适的拒绝策略。例如,如果对任务的执行顺序没有特殊要求,可以选择 DiscardPolicy 或 DiscardOldestPolicy 来忽略无法执行的任务。如果希望调用者线程来执行任务,可以选择 CallerRunsPolicy。如果希望在任务无法执行时抛出异常并通知调用者,可以选择 AbortPolicy。

拒绝策略可以通过线程池的构造方法或setter方法进行配置。在创建线程池时,可以根据具体的业务场景和需求来选择合适的拒绝策略。

  • 问题 25. 简述 Java 线程池状态

解答:

Java线程池有几种不同的状态,用于表示线程池的当前状态。以下是Java线程池的状态:

  1. RUNNING(运行状态):线程池处于正常运行状态,可以接收新的任务,并且可以处理任务队列中的任务。
  2. SHUTDOWN(关闭状态):线程池不再接收新的任务,但仍然会处理任务队列中的任务。已提交但尚未执行的任务可能会被丢弃。
  3. STOP(停止状态):线程池不再接收新的任务,并且会中断正在执行的任务。已提交但尚未执行的任务可能会被丢弃。
  4. TIDYING(整理状态):所有的任务都已经终止,工作线程数量为0,线程池正在进行最终的清理工作。
  5. TERMINATED(终止状态):线程池已经完全终止,不再接收新的任务,也不再处理任务队列中的任务。

线程池的状态会随着不同的操作而发生变化。例如,当调用线程池的shutdown()方法时,线程池的状态会从RUNNING变为SHUTDOWN;当所有任务执行完毕后,线程池的状态会从SHUTDOWN变为TIDYING,最终变为TERMINATED。

了解线程池的状态可以帮助我们更好地管理和监控线程池的运行情况,以及正确地使用线程池的各种方法和操作。

  • 问题 26. 简述 Java 线程池创建方法

解答:

Java 线程池的创建方法主要有两种:使用 ThreadPoolExecutor 类的构造方法和使用 Executors 工厂类的静态方法。

  1. 使用 ThreadPoolExecutor 类的构造方法:ThreadPoolExecutor 是 Java 线程池的实现类,可以通过其构造方法来创建线程池。构造方法的参数包括核心线程数、最大线程数、线程存活时间、任务队列等。例如:
代码语言:javascript复制
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize, // 核心线程数
    maximumPoolSize, // 最大线程数
    keepAliveTime, // 线程存活时间
    TimeUnit.MILLISECONDS, // 存活时间单位
    new LinkedBlockingQueue<Runnable>() // 任务队列
);
  1. 使用 Executors 工厂类的静态方法:Executors 类提供了一些静态方法来创建不同类型的线程池。这些方法隐藏了 ThreadPoolExecutor 的复杂性,提供了一些常用的线程池配置。例如:
  • 创建固定大小的线程池:
代码语言:javascript复制
ExecutorService executor = Executors.newFixedThreadPool(nThreads);
  • 创建单线程的线程池:
代码语言:javascript复制
ExecutorService executor = Executors.newSingleThreadExecutor();
  • 创建可缓存的线程池:
代码语言:javascript复制
ExecutorService executor = Executors.newCachedThreadPool();
  • 创建定时任务的线程池:
代码语言:javascript复制
ScheduledExecutorService executor = Executors.newScheduledThreadPool(corePoolSize);

这些方法返回的是 ExecutorService 或 ScheduledExecutorService 接口的实例,可以用于提交任务和管理线程池。

根据具体的需求和场景,选择合适的创建方法来创建线程池。需要注意的是,使用 Executors 工厂类创建的线程池可能不适合所有的场景,因为它们的一些默认配置可能不符合特定的需求。在使用线程池时,应根据具体情况进行配置和调整。

  • 问题 27. 简述 Executor 框架

解答:

Executor 框架是 Java 提供的一个用于管理和调度线程的框架。它位于 java.util.concurrent 包中,提供了一组接口和类,用于执行异步任务和管理线程池。

Executor 框架的核心接口是 Executor,它定义了一个用于执行任务的方法 execute(Runnable command)。通过实现 Executor 接口,我们可以自定义任务的执行方式。

ExecutorService 接口继承自 Executor 接口,它提供了更丰富的任务管理功能。ExecutorService 可以提交任务并返回一个 Future 对象,用于获取任务的执行结果。它还提供了管理线程池的方法,如动态调整线程池大小、关闭线程池等。

ThreadPoolExecutorExecutorService 接口的一个实现类,它是一个线程池的具体实现。通过 ThreadPoolExecutor,我们可以创建一个线程池,并指定线程池的核心线程数、最大线程数、线程空闲时间等参数。

除了以上核心接口和类,Executor 框架还提供了一些辅助类,如 ScheduledExecutorService 用于执行定时任务,CompletionService 用于获取多个任务的执行结果等。

Executor 框架的优点是简化了线程的管理和调度,提供了高效的线程池实现,可以更好地控制线程的创建和销毁,避免了频繁创建和销毁线程的开销。它还提供了丰富的任务管理功能,可以方便地提交任务、获取任务的执行结果,并支持任务的定时执行。

总之,Executor 框架是 Java 中用于管理和调度线程的重要工具,可以提高多线程编程的效率和可靠性。

  • 问题 28. 简述 Executor 框架的继承关系

解答:

Executor 框架的继承关系如下:

  1. Executor 接口:是 Executor 框架的核心接口,定义了一个用于执行任务的方法 execute(Runnable command)
  2. ExecutorService 接口:继承自 Executor 接口,提供了更丰富的任务管理功能。它定义了一系列提交任务、管理线程池的方法,如 submit(Callable<T> task)shutdown() 等。
  3. AbstractExecutorService 抽象类:实现了 ExecutorService 接口的一部分方法,提供了一些默认的实现。它是 ExecutorService 接口的一个方便的基类,可以用来自定义线程池的实现。
  4. ThreadPoolExecutor 类:是 ExecutorService 接口的一个具体实现类,用于创建和管理线程池。它继承自 AbstractExecutorService 抽象类,并实现了 ExecutorService 接口的所有方法。
  5. ScheduledExecutorService 接口:继承自 ExecutorService 接口,提供了执行定时任务的功能。它定义了一系列提交定时任务的方法,如 schedule(Runnable command, long delay, TimeUnit unit)
  6. ScheduledThreadPoolExecutor 类:是 ScheduledExecutorService 接口的一个具体实现类,用于创建和管理定时任务的线程池。它继承自 ThreadPoolExecutor 类,并实现了 ScheduledExecutorService 接口的所有方法。

继承关系可以总结为:Executor 接口是顶层接口,ExecutorService 接口继承自 Executor 接口,AbstractExecutorService 抽象类实现了 ExecutorService 接口的一部分方法,ThreadPoolExecutor 类继承自 AbstractExecutorService 抽象类,ScheduledExecutorService 接口继承自 ExecutorService 接口,ScheduledThreadPoolExecutor 类继承自 ThreadPoolExecutor 类。

2.2、ThreadLocal
  • 问题 29. 什么是 ThreadLocal?它是如何工作的

解答:

ThreadLocal 是 Java 中的一个线程局部变量,它提供了一种线程封闭的机制,使得每个线程都可以独立地访问自己的变量副本,互不干扰。

ThreadLocal 的工作原理如下:

  1. 每个 ThreadLocal 对象都维护了一个线程私有的变量副本,这个副本存储在 Thread 对象中的 ThreadLocalMap 中。
  2. 当通过 ThreadLocal 对象的 get() 方法获取变量时,它会首先获取当前线程的 Thread 对象,然后从 Thread 对象的 ThreadLocalMap 中根据 ThreadLocal 对象获取对应的变量副本。
  3. 如果当前线程没有对应的变量副本,ThreadLocal 会调用 initialValue() 方法创建一个初始值,并将其存储在 ThreadLocalMap 中。
  4. 当通过 ThreadLocal 对象的 set() 方法设置变量时,它会首先获取当前线程的 Thread 对象,然后将变量存储在 Thread 对象的 ThreadLocalMap 中。
  5. 当线程结束时,ThreadLocalMap 中的变量副本会被自动回收,避免了内存泄漏。

ThreadLocal 的使用场景包括但不限于以下情况:

  1. 在多线程环境下,每个线程需要独立地维护自己的变量副本,例如线程池中的线程需要处理不同的任务。
  2. 在 Web 应用中,每个请求需要独立地维护自己的变量副本,例如保存用户的登录信息。

需要注意的是,由于 ThreadLocal 的特性,它可能导致内存泄漏问题。如果 ThreadLocal 对象长时间不被使用,但是变量副本却一直存在于 ThreadLocalMap 中,这会导致变量无法被垃圾回收。因此,在使用 ThreadLocal 时,需要及时清理不再使用的 ThreadLocal 对象,可以通过调用 remove() 方法来清理 ThreadLocalMap 中的变量副本。

总之,ThreadLocal 提供了一种线程封闭的机制,使得每个线程都可以独立地访问自己的变量副本,避免了线程间的数据共享和竞争条件,但需要注意内存泄漏问题。

  • 问题 30. 介绍一下 InheritableThreadLocal

解答:

InheritableThreadLocal 是 ThreadLocal 的一个子类,它提供了一种特殊的 ThreadLocal 变量,可以在子线程中继承父线程的变量副本。

InheritableThreadLocal 的工作原理与 ThreadLocal 类似,但它在创建子线程时,会将父线程的变量副本复制到子线程中。这样,子线程就可以访问父线程的变量副本,实现了变量的继承。

使用 InheritableThreadLocal 时,需要注意以下几点:

  1. 在父线程中设置 InheritableThreadLocal 变量时,子线程会继承父线程的变量副本。
  2. 子线程可以通过 InheritableThreadLocal 的 get() 方法获取父线程的变量副本。
  3. 子线程可以通过 InheritableThreadLocal 的 set() 方法设置自己的变量副本,而不会影响父线程的变量副本。
  4. 子线程可以通过 InheritableThreadLocal 的 remove() 方法移除自己的变量副本,而不会影响父线程的变量副本。

InheritableThreadLocal 的使用场景与 ThreadLocal 类似,适用于需要在父子线程之间传递变量的情况。例如,在一个线程池中,父线程提交任务时设置了 InheritableThreadLocal 变量,子线程可以继承这个变量并在任务执行过程中使用。

需要注意的是,InheritableThreadLocal 会增加线程间的耦合性,因为子线程依赖于父线程的变量副本。同时,使用 InheritableThreadLocal 也可能导致内存泄漏问题,需要及时清理不再使用的 InheritableThreadLocal 对象。

总之,InheritableThreadLocal 是 ThreadLocal 的一个子类,提供了在子线程中继承父线程的变量副本的功能。它可以在父子线程之间传递变量,适用于需要在多个线程间共享变量的场景。

  • 问题 31. ThreadLocal 为什么会引起内存泄漏,我们该如何预防

解答:

ThreadLocal 可能引起内存泄漏的原因是,当 ThreadLocal 对象被垃圾回收时,如果对应的线程仍然存活,那么线程中的 ThreadLocalMap 中的 Entry 对象仍然持有对 ThreadLocal 对象的强引用,导致 ThreadLocal 对象无法被回收,从而造成内存泄漏。

为了预防 ThreadLocal 内存泄漏,可以采取以下措施:

及时清理:在使用完 ThreadLocal 后,及时调用 remove() 方法将其从 ThreadLocalMap 中移除。可以通过在使用 ThreadLocal 的代码块最后添加 ThreadLocal.remove() 来确保清理操作。

使用弱引用:可以使用 WeakReference 包装 ThreadLocal 对象,使其成为弱引用。这样,在 ThreadLocal 对象没有其他强引用时,垃圾回收器可以回收它。

使用线程池时注意清理:如果在线程池中使用 ThreadLocal,需要特别注意清理操作。在线程池中,线程会被重复使用,如果不及时清理 ThreadLocal,可能会导致线程复用时的数据污染。

避免过多的 ThreadLocal 对象:过多的 ThreadLocal 对象会增加内存消耗和管理成本,因此应该避免滥用 ThreadLocal,只在必要的情况下使用。

使用 try-finally 块:在使用 ThreadLocal 时,可以使用 try-finally 块确保在使用完毕后清理 ThreadLocal。例如:

代码语言:javascript复制
ThreadLocal<String> threadLocal = new ThreadLocal<>();
try {
    // 使用 threadLocal
} finally {
    threadLocal.remove();
}

通过以上措施,可以有效预防 ThreadLocal 内存泄漏问题。但需要注意,使用 ThreadLocal 时仍然需要谨慎,确保正确地使用和清理 ThreadLocal 对象,以避免潜在的内存泄漏风险。

2.3、CAS
  • 问题 32. 简述 Java 乐观锁悲观锁的概念

解答:

Java 中的乐观锁和悲观锁是并发编程中常用的两种锁机制,用于解决多线程环境下的数据竞争和并发访问的问题。

  1. 悲观锁(Pessimistic Locking)是一种保守的锁策略,它假设在并发环境下会发生冲突,因此在访问共享资源之前,会先获取锁,确保其他线程无法同时访问该资源。悲观锁的典型应用是使用 synchronized 关键字或 ReentrantLock 类来实现,它们都是独占锁,一次只允许一个线程访问被锁定的资源。悲观锁的特点是保证数据的一致性和安全性,但可能会导致线程的阻塞和等待。
  2. 乐观锁(Optimistic Locking)是一种乐观的锁策略,它假设在并发环境下不会发生冲突,因此在访问共享资源之前不会获取锁,而是在更新资源时检查是否发生了冲突。乐观锁的典型应用是使用版本号或时间戳来实现,每个线程在读取数据时会记录一个版本号或时间戳,当要更新数据时,会检查当前的版本号或时间戳是否与之前读取的一致,如果一致则更新成功,否则表示发生了冲突,需要进行相应的处理。乐观锁的特点是避免了线程的阻塞和等待,提高了并发性能,但可能会导致数据的不一致。

乐观锁和悲观锁各有优缺点,选择使用哪种锁策略取决于具体的应用场景和需求。悲观锁适用于对数据一致性要求较高的场景,而乐观锁适用于对数据一致性要求较低,但并发性能要求较高的场景。

需要注意的是,乐观锁和悲观锁并不是绝对的对立关系,可以根据具体情况结合使用。例如,在读多写少的场景中,可以使用乐观锁来提高并发性能,而在写多读少的场景中,可以使用悲观锁来保证数据的一致性和安全性。

  • 问题 33. 什么是 CAS 操作?什么是 ABA 问题?

解答:

CAS(Compare and Swap)操作是一种并发编程中常用的原子操作,用于实现乐观锁。CAS 操作包含三个操作数:内存位置(或称为变量)、期望值和新值。它的执行过程如下:

  1. 首先,读取内存位置的当前值,记为当前值 A。
  2. 比较当前值 A 是否等于期望值,如果相等,则将内存位置的值更新为新值。
  3. 如果当前值 A 不等于期望值,则说明其他线程已经修改了内存位置的值,CAS 操作失败,不进行更新。

CAS 操作是原子的,即在执行过程中不会被其他线程中断。它利用了硬件的原子性操作,可以实现非阻塞的并发算法,避免了使用锁带来的线程阻塞和上下文切换的开销。

ABA 问题是在使用 CAS 操作时可能出现的一个问题。假设线程 A 读取了内存位置的值为 A,然后线程 B 修改了内存位置的值为 B,最后线程 B 又将内存位置的值修改回 A,此时线程 A 再次执行 CAS 操作时,会发现内存位置的值仍然等于 A,认为没有被修改过,导致 CAS 操作成功。但实际上,内存位置的值已经发生了变化,只是经历了一个 ABA 的过程。

为了解决 ABA 问题,可以使用版本号或时间戳等方式来增加额外的信息。每次修改内存位置的值时,都更新版本号或时间戳,这样在执行 CAS 操作时,不仅比较值是否相等,还需要比较版本号或时间戳是否一致,从而避免了 ABA 问题的发生。

需要注意的是,ABA 问题只在某些特定场景下才会出现,例如在使用 CAS 操作进行数据结构的修改时。在一般的并发编程中,ABA 问题的影响较小,可以通过增加版本号或时间戳等方式来解决。

2.4、Synchronized
  • 问题 34. 简述 Synchronized 的概念

解答:

Synchronized 是 Java 中用于实现线程同步的关键字,它提供了一种独占锁的机制,用于保护共享资源的访问,确保多个线程之间的互斥和可见性。

Synchronized 的概念如下:

  1. 互斥性:Synchronized 保证了同一时刻只有一个线程可以执行被 Synchronized 修饰的代码块或方法。当一个线程获取到锁时,其他线程将被阻塞,直到锁被释放。
  2. 可见性:Synchronized 保证了共享变量的可见性。当一个线程释放锁时,会将对共享变量的修改刷新到主内存中,使得其他线程可以看到最新的值。

Synchronized 可以用于以下几种方式:

Synchronized 代码块:使用 synchronized 关键字修饰的代码块,通过指定一个对象作为锁,只有获取到该对象的线程才能执行该代码块。例如:

代码语言:javascript复制
synchronized (lock) {
    // 需要同步的代码块
}

Synchronized 方法:使用 synchronized 关键字修饰的方法,整个方法都被视为一个同步代码块,锁对象是当前对象(即 this)。例如:

代码语言:javascript复制
public synchronized void method() {
    // 需要同步的方法体
}

静态 Synchronized 方法:使用 synchronized 关键字修饰的静态方法,整个静态方法都被视为一个同步代码块,锁对象是当前类的 Class 对象。例如:

代码语言:javascript复制
public static synchronized void staticMethod() {
    // 需要同步的静态方法体
}

Synchronized 的使用可以确保线程安全,保护共享资源的一致性。但需要注意以下几点:

  1. Synchronized 是独占锁,可能会导致线程的阻塞和等待,影响程序的性能。
  2. Synchronized 仅保护代码块或方法内的共享资源,对于类的其他非同步方法或非同步代码块无法提供保护。
  3. Synchronized 不能解决所有的并发问题,有些复杂的并发场景可能需要使用其他的同步机制,如 Lock、Condition、Atomic 类等。

总之,Synchronized 是 Java 中用于实现线程同步的关键字,通过互斥性和可见性保证了共享资源的安全访问。它是一种简单而有效的线程同步机制,但需要注意性能和使用的范围。

  • 问题 35. 简述 Synchronized 的使用场景

解答:

Synchronized 的使用场景包括但不限于以下情况:

  1. 保护共享资源:当多个线程需要同时访问和修改共享资源时,可以使用 Synchronized 来保护共享资源的一致性和安全性。通过在访问共享资源的代码块或方法上添加 Synchronized 关键字,确保同一时刻只有一个线程可以访问共享资源。
  2. 实现线程安全的类:当设计一个多线程环境下的类时,可以使用 Synchronized 来实现线程安全。通过在类的方法上添加 Synchronized 关键字,确保多个线程对该类的实例进行操作时的线程安全。
  3. 实现线程间的协调与通信:Synchronized 可以用于实现线程间的协调与通信。通过使用 Synchronized 关键字配合 wait()、notify()、notifyAll() 方法,可以实现线程的等待和唤醒,实现线程间的协调与通信。
  4. 实现线程的顺序执行:有时候需要确保多个线程按照特定的顺序执行,可以使用 Synchronized 来实现线程的顺序执行。通过在不同线程的代码块或方法上添加 Synchronized 关键字,并使用共享的对象作为锁,可以实现线程的有序执行。

需要注意的是,Synchronized 的使用应该遵循以下原则:

  1. 尽量减小同步范围:只在必要的代码块或方法上使用 Synchronized,避免过多的同步操作,以提高并发性能。
  2. 避免死锁:当使用多个锁时,需要注意锁的获取顺序,避免出现死锁的情况。
  3. 考虑性能影响:Synchronized 是独占锁,可能会导致线程的阻塞和等待,影响程序的性能。在高并发场景下,可以考虑使用其他的同步机制,如 Lock、Condition、Atomic 类等。

总之,Synchronized 的使用场景包括保护共享资源、实现线程安全的类、线程间的协调与通信以及线程的顺序执行。在使用 Synchronized 时,需要根据具体的需求和场景进行合理的选择和使用。

  • 问题 36. 简述 Synchronized 的锁升级过程

解答:

在 Java 中,Synchronized 的锁升级过程涉及到三种不同的锁状态,即无锁状态、偏向锁状态和轻量级锁状态。

  1. 无锁状态(Unlocked):当一个线程访问一个同步代码块时,如果没有其他线程竞争该锁,那么该线程会直接进入临界区执行代码,此时处于无锁状态。
  2. 偏向锁状态(Biased Locking):当一个线程获取到锁并进入临界区执行代码后,JVM 会将该锁标记为偏向锁。此时,如果其他线程也想要获取该锁,会发现该锁已经被偏向线程占有,但是由于只有一个线程获取到锁,其他线程不会竞争锁,而是直接进入临界区执行代码。这样可以减少锁的竞争,提高性能。
  3. 轻量级锁状态(Lightweight Locking):当多个线程竞争同一个锁时,偏向锁会升级为轻量级锁。在轻量级锁状态下,JVM 会通过 CAS 操作尝试获取锁,如果成功获取到锁,则进入临界区执行代码。如果获取锁失败,表示有其他线程竞争锁,此时会进一步升级为重量级锁。
  4. 重量级锁状态(Heavyweight Locking):当多个线程竞争同一个锁且轻量级锁获取失败时,锁会升级为重量级锁。在重量级锁状态下,竞争锁的线程会进入阻塞状态,等待锁的释放。当持有锁的线程执行完临界区代码并释放锁时,其他线程会竞争锁,获取到锁的线程进入临界区执行代码。

锁的升级过程是由 JVM 自动完成的,根据竞争情况和线程的执行情况来决定是否升级锁的状态。锁的升级过程是为了在不同的竞争情况下提供更好的性能和资源利用。

需要注意的是,锁的升级过程是逐级升级的,即从无锁状态到偏向锁状态,再到轻量级锁状态,最后到重量级锁状态。而且锁的升级过程是不可逆的,一旦锁升级为重量级锁,就无法再降级为轻量级锁或偏向锁。

锁的升级过程是 JVM 内部的实现细节,对于开发者来说,只需要关注正确使用 Synchronized 关键字来保证线程安全即可,无需过多关注锁的升级过程。

  • 问题 37. 简述 自旋锁与自适应自旋锁

解答:

自旋锁和自适应自旋锁都是在并发编程中用于解决线程竞争的锁机制,它们都属于乐观锁的一种实现方式。

  1. 自旋锁(Spin Lock):自旋锁是一种忙等待的锁机制,当一个线程尝试获取锁时,如果发现锁已经被其他线程占用,它会一直循环(自旋)等待,直到锁被释放。自旋锁适用于锁的持有时间很短,且线程竞争不激烈的情况。自旋锁的优点是避免了线程的阻塞和切换,减少了线程上下文切换的开销,但如果锁的持有时间较长或线程竞争激烈,会导致自旋等待时间过长,浪费 CPU 资源。
  2. 自适应自旋锁(Adaptive Spin Lock):自适应自旋锁是一种根据线程竞争情况动态调整自旋等待时间的锁机制。在自适应自旋锁中,系统会根据之前获取锁的线程的自旋等待时间和锁的释放情况,来决定当前线程的自旋等待时间。如果之前获取锁的线程自旋等待时间较长且成功获取到锁,那么当前线程会增加自旋等待时间;如果之前获取锁的线程自旋等待时间较短或未成功获取到锁,那么当前线程会减少自旋等待时间。通过动态调整自旋等待时间,自适应自旋锁可以更好地适应不同的线程竞争情况,提高并发性能。

自适应自旋锁是在 JDK 6 中引入的一种优化机制,通过减少自旋等待时间来提高并发性能。在 JDK 9 中,默认情况下,自适应自旋锁是开启的,但也可以通过 JVM 参数来控制自适应自旋锁的行为。

需要注意的是,自旋锁和自适应自旋锁适用于不同的场景。自旋锁适用于锁的持有时间短、线程竞争不激烈的情况;而自适应自旋锁适用于锁的持有时间不确定、线程竞争较激烈的情况。在实际应用中,可以根据具体的场景和需求选择合适的锁机制。

  • 问题 38. 简述 锁膨胀,锁粗化,锁消除

解答:

锁膨胀(Lock Escalation)、锁粗化(Lock Coarsening)和锁消除(Lock Elimination)是在编译器和运行时优化中常见的锁优化技术。

  1. 锁膨胀(Lock Escalation):锁膨胀指的是当一个线程多次获取同一个锁时,锁的粒度逐渐扩大,从细粒度锁升级为粗粒度锁。例如,当一个线程多次获取某个对象的锁时,如果发现该对象的锁已经被其他线程竞争,那么锁膨胀机制会将该对象的锁升级为更高层次的锁,如类锁或全局锁。锁膨胀的目的是减少锁的竞争和细粒度锁带来的开销,提高并发性能。
  2. 锁粗化(Lock Coarsening):锁粗化指的是将多个连续的细粒度锁合并为一个粗粒度锁。当编译器发现一系列的连续的加锁和解锁操作,且没有其他线程竞争这些锁时,它会将这些细粒度锁合并为一个粗粒度锁,减少加锁和解锁的次数,从而提高性能。
  3. 锁消除(Lock Elimination):锁消除指的是在编译器优化阶段,通过静态分析和逃逸分析等技术,判断某些锁是多余的,可以被消除。当编译器发现某个锁对象只被一个线程访问,并且不会被其他线程访问时,它会将该锁消除,从而避免了不必要的加锁和解锁操作,提高性能。

锁膨胀、锁粗化和锁消除都是为了优化锁的使用,减少锁带来的开销,提高并发性能。它们的具体应用和效果取决于具体的编译器和运行时环境。在实际开发中,可以通过合理的代码设计和编写,以及运行时环境的配置,来充分利用这些锁优化技术,提高程序的性能和并发能力。

  • 问题 39. 简述 synchronized 和 volatile 的区别

解答:

synchronized 和 volatile 是 Java 中用于实现线程同步和共享变量可见性的关键字,它们有以下几个主要区别:

  1. 作用范围:synchronized 可以用于修饰代码块、方法和静态方法,可以实现对代码块或方法的同步控制;而 volatile 只能修饰成员变量,用于实现对共享变量的可见性。
  2. 实现机制:synchronized 是一种独占锁的机制,它通过获取锁来保证同一时刻只有一个线程可以执行被 synchronized 修饰的代码块或方法;而 volatile 是一种轻量级的同步机制,它通过内存屏障和禁止指令重排序来保证共享变量的可见性。
  3. 原子性:synchronized 可以保证代码块或方法的原子性,即同一时刻只有一个线程可以执行该代码块或方法;而 volatile 不能保证原子性,它只能保证共享变量的可见性,不能解决多线程并发修改共享变量的原子性问题。
  4. 内存语义:synchronized 在释放锁时会将对共享变量的修改刷新到主内存中,使得其他线程可以看到最新的值;而 volatile 在写操作时会立即将对共享变量的修改刷新到主内存中,并且在读操作时会从主内存中获取最新的值,保证了共享变量的可见性。
  5. 适用场景:synchronized 适用于多个线程之间需要互斥访问共享资源的场景,可以保证线程安全;而 volatile 适用于一个线程修改共享变量,其他线程需要立即看到最新值的场景,可以保证可见性。

需要注意的是,synchronized 和 volatile 并不是完全互相替代的,它们有不同的应用场景和使用方式。在实际开发中,需要根据具体的需求和场景选择合适的关键字来实现线程同步和共享变量的可见性。

  • 问题 40. 简述 synchronized 与 ReentrantLock 的区别

解答:

synchronized 和 ReentrantLock 都是 Java 中用于实现线程同步的机制,它们有以下几个主要区别:

  1. 可重入性:ReentrantLock 是可重入锁,即同一个线程可以多次获取同一个锁,而不会发生死锁;而 synchronized 也是可重入的,同一个线程可以多次获取同一个锁,且在释放锁之前必须释放相同次数的锁。
  2. 锁的获取方式:ReentrantLock 提供了公平锁和非公平锁两种获取锁的方式,可以通过构造函数指定;而 synchronized 是非公平锁,无法指定获取锁的方式。
  3. 锁的灵活性:ReentrantLock 提供了更多的灵活性和扩展性。它支持可中断的获取锁、超时获取锁、公平锁和非公平锁、多个条件变量等特性,可以更精细地控制锁的行为;而 synchronized 的功能相对简单,无法提供这些额外的特性。
  4. 性能:在低竞争的情况下,synchronized 的性能通常比 ReentrantLock 好,因为 synchronized 是 JVM 内置的关键字,底层实现经过了优化;而在高竞争的情况下,ReentrantLock 的性能可能更好,因为它提供了更细粒度的控制和更灵活的特性。
  5. 锁的释放方式:synchronized 在代码块或方法执行完毕时会自动释放锁;而 ReentrantLock 需要手动调用 unlock() 方法来释放锁,需要注意避免忘记释放锁导致死锁的问题。

需要注意的是,synchronized 是 JVM 内置的关键字,使用起来更简单,适用于大多数的线程同步场景;而 ReentrantLock 是一个类,提供了更多的灵活性和扩展性,适用于一些特殊的线程同步需求。在实际开发中,可以根据具体的需求和场景选择合适的机制来实现线程同步。

0 人点赞