沈阳求做商城 网站,可以做锚文本链接的网站,常州做网站包括哪些,ppt模板下载网址大家好#xff0c;我是栗筝i#xff0c;从 2022 年 10 月份开始#xff0c;我便开始致力于对 Java 技术栈进行全面而细致的梳理。这一过程#xff0c;不仅是对我个人学习历程的回顾和总结#xff0c;更是希望能够为各位提供一份参考。因此得到了很多读者的正面反馈。 而在… 大家好我是栗筝i从 2022 年 10 月份开始我便开始致力于对 Java 技术栈进行全面而细致的梳理。这一过程不仅是对我个人学习历程的回顾和总结更是希望能够为各位提供一份参考。因此得到了很多读者的正面反馈。 而在 2023 年 10 月份开始我将推出 Java 面试题/知识点系列内容期望对大家有所助益让我们一起提升。 今天与您分享的是 Java 并发知识面试题系列的总结篇中篇我诚挚地希望它能为您带来启发并在您的职业生涯中起到助益作用。衷心感谢每一位朋友的关注与支持。 文章目录 1、Java并发面试题问题1.1、Java 线程池1.2、ThreadLocal1.3、CAS1.4、Synchronized 2、Java并发面试题解答2.1、Java 线程池2.2、ThreadLocal2.3、CAS2.4、Synchronized 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 线程池的主要优点包括 提高性能线程池可以控制并发线程的数量避免了线程过多导致的资源竞争和上下文切换开销从而提高了系统的性能。 提高资源利用率线程池可以复用线程避免了频繁创建和销毁线程的开销提高了系统的资源利用率。 提供任务队列线程池可以接收并存储任务当线程池中的线程空闲时可以从任务队列中取出任务执行从而实现任务的异步执行。 提供线程管理和监控线程池可以管理线程的生命周期包括线程的创建、销毁和状态监控等。
Java 线程池的实现类是 java.util.concurrent.ThreadPoolExecutor它提供了一系列的构造方法和配置参数可以根据需求来创建不同类型的线程池。常用的线程池类型包括固定大小线程池、缓存线程池和定时任务线程池等。
问题 22. 简述 Java 线程池的核心参数
解答
Java 线程池的核心参数包括以下几个 核心线程数corePoolSize线程池中保持的最小线程数。即使线程处于空闲状态核心线程也不会被销毁。默认情况下核心线程数为 0。 最大线程数maximumPoolSize线程池中允许的最大线程数。当任务数量超过核心线程数并且任务队列已满时线程池会创建新的线程来执行任务直到达到最大线程数。超过最大线程数的任务将被拒绝执行。 任务队列workQueue用于存储待执行任务的队列。当线程池中的线程都在执行任务时新的任务会被放入任务队列中等待执行。常见的任务队列类型包括有界队列如 ArrayBlockingQueue和无界队列如 LinkedBlockingQueue。 线程存活时间keepAliveTime当线程池中的线程数量超过核心线程数时空闲线程的存活时间。超过存活时间的空闲线程将被销毁以控制线程池的大小。 拒绝策略rejectedExecutionHandler当任务无法被线程池执行时的处理策略。常见的拒绝策略包括抛出异常、丢弃任务、丢弃队列中最旧的任务或在调用者线程中执行任务。
这些核心参数可以通过线程池的构造方法或 setter 方法进行配置。根据具体的需求和场景可以调整这些参数来优化线程池的性能和行为。
问题 23. 简述 Java 线程池执行流程
解答
Java 线程池的执行流程如下 当有任务提交给线程池时线程池会首先检查核心线程数是否已满。如果还有空闲的核心线程则会立即创建一个核心线程来执行任务。 如果核心线程数已满线程池会将任务放入任务队列中等待执行。任务队列可以是有界队列或无界队列根据具体的线程池配置而定。 如果任务队列已满且线程池中的线程数还未达到最大线程数线程池会创建新的线程来执行任务。 如果线程池中的线程数已达到最大线程数并且任务队列也已满根据设定的拒绝策略来处理无法执行的任务。常见的拒绝策略包括抛出异常、丢弃任务、丢弃队列中最旧的任务或在调用者线程中执行任务。 当线程池中的线程执行完任务后会继续从任务队列中获取新的任务来执行。如果任务队列为空空闲的线程会等待新的任务到来。 如果线程池中的线程空闲时间超过设定的存活时间keepAliveTime则这些空闲线程会被销毁以控制线程池的大小。
通过合理配置线程池的核心参数可以实现任务的异步执行、线程的复用和资源的合理利用从而提高系统的性能和响应能力。
问题 24. 简述 Java 线程池拒绝策略
解答
Java 线程池的拒绝策略用于处理无法执行的任务。当线程池中的线程数已达到最大线程数并且任务队列也已满时线程池会根据设定的拒绝策略来处理无法执行的任务。以下是常见的拒绝策略 AbortPolicy默认抛出 RejectedExecutionException 异常表示拒绝执行该任务。 CallerRunsPolicy将任务返回给提交任务的线程执行。也就是说如果线程池无法执行任务它会将任务退回给调用者线程来执行。 DiscardPolicy直接丢弃无法执行的任务不做任何处理。 DiscardOldestPolicy丢弃队列中最旧的任务然后尝试执行新的任务。
可以根据具体的业务需求选择合适的拒绝策略。例如如果对任务的执行顺序没有特殊要求可以选择 DiscardPolicy 或 DiscardOldestPolicy 来忽略无法执行的任务。如果希望调用者线程来执行任务可以选择 CallerRunsPolicy。如果希望在任务无法执行时抛出异常并通知调用者可以选择 AbortPolicy。
拒绝策略可以通过线程池的构造方法或setter方法进行配置。在创建线程池时可以根据具体的业务场景和需求来选择合适的拒绝策略。
问题 25. 简述 Java 线程池状态
解答
Java线程池有几种不同的状态用于表示线程池的当前状态。以下是Java线程池的状态 RUNNING运行状态线程池处于正常运行状态可以接收新的任务并且可以处理任务队列中的任务。 SHUTDOWN关闭状态线程池不再接收新的任务但仍然会处理任务队列中的任务。已提交但尚未执行的任务可能会被丢弃。 STOP停止状态线程池不再接收新的任务并且会中断正在执行的任务。已提交但尚未执行的任务可能会被丢弃。 TIDYING整理状态所有的任务都已经终止工作线程数量为0线程池正在进行最终的清理工作。 TERMINATED终止状态线程池已经完全终止不再接收新的任务也不再处理任务队列中的任务。
线程池的状态会随着不同的操作而发生变化。例如当调用线程池的shutdown()方法时线程池的状态会从RUNNING变为SHUTDOWN当所有任务执行完毕后线程池的状态会从SHUTDOWN变为TIDYING最终变为TERMINATED。
了解线程池的状态可以帮助我们更好地管理和监控线程池的运行情况以及正确地使用线程池的各种方法和操作。
问题 26. 简述 Java 线程池创建方法
解答
Java 线程池的创建方法主要有两种使用 ThreadPoolExecutor 类的构造方法和使用 Executors 工厂类的静态方法。
使用 ThreadPoolExecutor 类的构造方法ThreadPoolExecutor 是 Java 线程池的实现类可以通过其构造方法来创建线程池。构造方法的参数包括核心线程数、最大线程数、线程存活时间、任务队列等。例如
ThreadPoolExecutor executor new ThreadPoolExecutor(corePoolSize, // 核心线程数maximumPoolSize, // 最大线程数keepAliveTime, // 线程存活时间TimeUnit.MILLISECONDS, // 存活时间单位new LinkedBlockingQueueRunnable() // 任务队列
);使用 Executors 工厂类的静态方法Executors 类提供了一些静态方法来创建不同类型的线程池。这些方法隐藏了 ThreadPoolExecutor 的复杂性提供了一些常用的线程池配置。例如
创建固定大小的线程池
ExecutorService executor Executors.newFixedThreadPool(nThreads);创建单线程的线程池
ExecutorService executor Executors.newSingleThreadExecutor();创建可缓存的线程池
ExecutorService executor Executors.newCachedThreadPool();创建定时任务的线程池
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 对象用于获取任务的执行结果。它还提供了管理线程池的方法如动态调整线程池大小、关闭线程池等。
ThreadPoolExecutor 是 ExecutorService 接口的一个实现类它是一个线程池的具体实现。通过 ThreadPoolExecutor我们可以创建一个线程池并指定线程池的核心线程数、最大线程数、线程空闲时间等参数。
除了以上核心接口和类Executor 框架还提供了一些辅助类如 ScheduledExecutorService 用于执行定时任务CompletionService 用于获取多个任务的执行结果等。
Executor 框架的优点是简化了线程的管理和调度提供了高效的线程池实现可以更好地控制线程的创建和销毁避免了频繁创建和销毁线程的开销。它还提供了丰富的任务管理功能可以方便地提交任务、获取任务的执行结果并支持任务的定时执行。
总之Executor 框架是 Java 中用于管理和调度线程的重要工具可以提高多线程编程的效率和可靠性。
问题 28. 简述 Executor 框架的继承关系
解答
Executor 框架的继承关系如下 Executor 接口是 Executor 框架的核心接口定义了一个用于执行任务的方法 execute(Runnable command)。 ExecutorService 接口继承自 Executor 接口提供了更丰富的任务管理功能。它定义了一系列提交任务、管理线程池的方法如 submit(CallableT task)、shutdown() 等。 AbstractExecutorService 抽象类实现了 ExecutorService 接口的一部分方法提供了一些默认的实现。它是 ExecutorService 接口的一个方便的基类可以用来自定义线程池的实现。 ThreadPoolExecutor 类是 ExecutorService 接口的一个具体实现类用于创建和管理线程池。它继承自 AbstractExecutorService 抽象类并实现了 ExecutorService 接口的所有方法。 ScheduledExecutorService 接口继承自 ExecutorService 接口提供了执行定时任务的功能。它定义了一系列提交定时任务的方法如 schedule(Runnable command, long delay, TimeUnit unit)。 ScheduledThreadPoolExecutor 类是 ScheduledExecutorService 接口的一个具体实现类用于创建和管理定时任务的线程池。它继承自 ThreadPoolExecutor 类并实现了 ScheduledExecutorService 接口的所有方法。
继承关系可以总结为Executor 接口是顶层接口ExecutorService 接口继承自 Executor 接口AbstractExecutorService 抽象类实现了 ExecutorService 接口的一部分方法ThreadPoolExecutor 类继承自 AbstractExecutorService 抽象类ScheduledExecutorService 接口继承自 ExecutorService 接口ScheduledThreadPoolExecutor 类继承自 ThreadPoolExecutor 类。
2.2、ThreadLocal
问题 29. 什么是 ThreadLocal它是如何工作的
解答
ThreadLocal 是 Java 中的一个线程局部变量它提供了一种线程封闭的机制使得每个线程都可以独立地访问自己的变量副本互不干扰。
ThreadLocal 的工作原理如下 每个 ThreadLocal 对象都维护了一个线程私有的变量副本这个副本存储在 Thread 对象中的 ThreadLocalMap 中。 当通过 ThreadLocal 对象的 get() 方法获取变量时它会首先获取当前线程的 Thread 对象然后从 Thread 对象的 ThreadLocalMap 中根据 ThreadLocal 对象获取对应的变量副本。 如果当前线程没有对应的变量副本ThreadLocal 会调用 initialValue() 方法创建一个初始值并将其存储在 ThreadLocalMap 中。 当通过 ThreadLocal 对象的 set() 方法设置变量时它会首先获取当前线程的 Thread 对象然后将变量存储在 Thread 对象的 ThreadLocalMap 中。 当线程结束时ThreadLocalMap 中的变量副本会被自动回收避免了内存泄漏。
ThreadLocal 的使用场景包括但不限于以下情况 在多线程环境下每个线程需要独立地维护自己的变量副本例如线程池中的线程需要处理不同的任务。 在 Web 应用中每个请求需要独立地维护自己的变量副本例如保存用户的登录信息。
需要注意的是由于 ThreadLocal 的特性它可能导致内存泄漏问题。如果 ThreadLocal 对象长时间不被使用但是变量副本却一直存在于 ThreadLocalMap 中这会导致变量无法被垃圾回收。因此在使用 ThreadLocal 时需要及时清理不再使用的 ThreadLocal 对象可以通过调用 remove() 方法来清理 ThreadLocalMap 中的变量副本。
总之ThreadLocal 提供了一种线程封闭的机制使得每个线程都可以独立地访问自己的变量副本避免了线程间的数据共享和竞争条件但需要注意内存泄漏问题。
问题 30. 介绍一下 InheritableThreadLocal
解答
InheritableThreadLocal 是 ThreadLocal 的一个子类它提供了一种特殊的 ThreadLocal 变量可以在子线程中继承父线程的变量副本。
InheritableThreadLocal 的工作原理与 ThreadLocal 类似但它在创建子线程时会将父线程的变量副本复制到子线程中。这样子线程就可以访问父线程的变量副本实现了变量的继承。
使用 InheritableThreadLocal 时需要注意以下几点 在父线程中设置 InheritableThreadLocal 变量时子线程会继承父线程的变量副本。 子线程可以通过 InheritableThreadLocal 的 get() 方法获取父线程的变量副本。 子线程可以通过 InheritableThreadLocal 的 set() 方法设置自己的变量副本而不会影响父线程的变量副本。 子线程可以通过 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。例如 ThreadLocalString threadLocal new ThreadLocal();
try {// 使用 threadLocal
} finally {threadLocal.remove();
}通过以上措施可以有效预防 ThreadLocal 内存泄漏问题。但需要注意使用 ThreadLocal 时仍然需要谨慎确保正确地使用和清理 ThreadLocal 对象以避免潜在的内存泄漏风险。
2.3、CAS
问题 32. 简述 Java 乐观锁悲观锁的概念
解答
Java 中的乐观锁和悲观锁是并发编程中常用的两种锁机制用于解决多线程环境下的数据竞争和并发访问的问题。
悲观锁Pessimistic Locking是一种保守的锁策略它假设在并发环境下会发生冲突因此在访问共享资源之前会先获取锁确保其他线程无法同时访问该资源。悲观锁的典型应用是使用 synchronized 关键字或 ReentrantLock 类来实现它们都是独占锁一次只允许一个线程访问被锁定的资源。悲观锁的特点是保证数据的一致性和安全性但可能会导致线程的阻塞和等待。乐观锁Optimistic Locking是一种乐观的锁策略它假设在并发环境下不会发生冲突因此在访问共享资源之前不会获取锁而是在更新资源时检查是否发生了冲突。乐观锁的典型应用是使用版本号或时间戳来实现每个线程在读取数据时会记录一个版本号或时间戳当要更新数据时会检查当前的版本号或时间戳是否与之前读取的一致如果一致则更新成功否则表示发生了冲突需要进行相应的处理。乐观锁的特点是避免了线程的阻塞和等待提高了并发性能但可能会导致数据的不一致。
乐观锁和悲观锁各有优缺点选择使用哪种锁策略取决于具体的应用场景和需求。悲观锁适用于对数据一致性要求较高的场景而乐观锁适用于对数据一致性要求较低但并发性能要求较高的场景。
需要注意的是乐观锁和悲观锁并不是绝对的对立关系可以根据具体情况结合使用。例如在读多写少的场景中可以使用乐观锁来提高并发性能而在写多读少的场景中可以使用悲观锁来保证数据的一致性和安全性。
问题 33. 什么是 CAS 操作什么是 ABA 问题
解答
CASCompare and Swap操作是一种并发编程中常用的原子操作用于实现乐观锁。CAS 操作包含三个操作数内存位置或称为变量、期望值和新值。它的执行过程如下 首先读取内存位置的当前值记为当前值 A。 比较当前值 A 是否等于期望值如果相等则将内存位置的值更新为新值。 如果当前值 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 的概念如下 互斥性Synchronized 保证了同一时刻只有一个线程可以执行被 Synchronized 修饰的代码块或方法。当一个线程获取到锁时其他线程将被阻塞直到锁被释放。 可见性Synchronized 保证了共享变量的可见性。当一个线程释放锁时会将对共享变量的修改刷新到主内存中使得其他线程可以看到最新的值。
Synchronized 可以用于以下几种方式 Synchronized 代码块使用 synchronized 关键字修饰的代码块通过指定一个对象作为锁只有获取到该对象的线程才能执行该代码块。例如 synchronized (lock) {// 需要同步的代码块
}Synchronized 方法使用 synchronized 关键字修饰的方法整个方法都被视为一个同步代码块锁对象是当前对象即 this。例如 public synchronized void method() {// 需要同步的方法体
}静态 Synchronized 方法使用 synchronized 关键字修饰的静态方法整个静态方法都被视为一个同步代码块锁对象是当前类的 Class 对象。例如 public static synchronized void staticMethod() {// 需要同步的静态方法体
}Synchronized 的使用可以确保线程安全保护共享资源的一致性。但需要注意以下几点 Synchronized 是独占锁可能会导致线程的阻塞和等待影响程序的性能。 Synchronized 仅保护代码块或方法内的共享资源对于类的其他非同步方法或非同步代码块无法提供保护。 Synchronized 不能解决所有的并发问题有些复杂的并发场景可能需要使用其他的同步机制如 Lock、Condition、Atomic 类等。
总之Synchronized 是 Java 中用于实现线程同步的关键字通过互斥性和可见性保证了共享资源的安全访问。它是一种简单而有效的线程同步机制但需要注意性能和使用的范围。
问题 35. 简述 Synchronized 的使用场景
解答
Synchronized 的使用场景包括但不限于以下情况 保护共享资源当多个线程需要同时访问和修改共享资源时可以使用 Synchronized 来保护共享资源的一致性和安全性。通过在访问共享资源的代码块或方法上添加 Synchronized 关键字确保同一时刻只有一个线程可以访问共享资源。 实现线程安全的类当设计一个多线程环境下的类时可以使用 Synchronized 来实现线程安全。通过在类的方法上添加 Synchronized 关键字确保多个线程对该类的实例进行操作时的线程安全。 实现线程间的协调与通信Synchronized 可以用于实现线程间的协调与通信。通过使用 Synchronized 关键字配合 wait()、notify()、notifyAll() 方法可以实现线程的等待和唤醒实现线程间的协调与通信。 实现线程的顺序执行有时候需要确保多个线程按照特定的顺序执行可以使用 Synchronized 来实现线程的顺序执行。通过在不同线程的代码块或方法上添加 Synchronized 关键字并使用共享的对象作为锁可以实现线程的有序执行。
需要注意的是Synchronized 的使用应该遵循以下原则 尽量减小同步范围只在必要的代码块或方法上使用 Synchronized避免过多的同步操作以提高并发性能。 避免死锁当使用多个锁时需要注意锁的获取顺序避免出现死锁的情况。 考虑性能影响Synchronized 是独占锁可能会导致线程的阻塞和等待影响程序的性能。在高并发场景下可以考虑使用其他的同步机制如 Lock、Condition、Atomic 类等。
总之Synchronized 的使用场景包括保护共享资源、实现线程安全的类、线程间的协调与通信以及线程的顺序执行。在使用 Synchronized 时需要根据具体的需求和场景进行合理的选择和使用。
问题 36. 简述 Synchronized 的锁升级过程
解答
在 Java 中Synchronized 的锁升级过程涉及到三种不同的锁状态即无锁状态、偏向锁状态和轻量级锁状态。 无锁状态Unlocked当一个线程访问一个同步代码块时如果没有其他线程竞争该锁那么该线程会直接进入临界区执行代码此时处于无锁状态。 偏向锁状态Biased Locking当一个线程获取到锁并进入临界区执行代码后JVM 会将该锁标记为偏向锁。此时如果其他线程也想要获取该锁会发现该锁已经被偏向线程占有但是由于只有一个线程获取到锁其他线程不会竞争锁而是直接进入临界区执行代码。这样可以减少锁的竞争提高性能。 轻量级锁状态Lightweight Locking当多个线程竞争同一个锁时偏向锁会升级为轻量级锁。在轻量级锁状态下JVM 会通过 CAS 操作尝试获取锁如果成功获取到锁则进入临界区执行代码。如果获取锁失败表示有其他线程竞争锁此时会进一步升级为重量级锁。 重量级锁状态Heavyweight Locking当多个线程竞争同一个锁且轻量级锁获取失败时锁会升级为重量级锁。在重量级锁状态下竞争锁的线程会进入阻塞状态等待锁的释放。当持有锁的线程执行完临界区代码并释放锁时其他线程会竞争锁获取到锁的线程进入临界区执行代码。
锁的升级过程是由 JVM 自动完成的根据竞争情况和线程的执行情况来决定是否升级锁的状态。锁的升级过程是为了在不同的竞争情况下提供更好的性能和资源利用。
需要注意的是锁的升级过程是逐级升级的即从无锁状态到偏向锁状态再到轻量级锁状态最后到重量级锁状态。而且锁的升级过程是不可逆的一旦锁升级为重量级锁就无法再降级为轻量级锁或偏向锁。
锁的升级过程是 JVM 内部的实现细节对于开发者来说只需要关注正确使用 Synchronized 关键字来保证线程安全即可无需过多关注锁的升级过程。
问题 37. 简述 自旋锁与自适应自旋锁
解答
自旋锁和自适应自旋锁都是在并发编程中用于解决线程竞争的锁机制它们都属于乐观锁的一种实现方式。 自旋锁Spin Lock自旋锁是一种忙等待的锁机制当一个线程尝试获取锁时如果发现锁已经被其他线程占用它会一直循环自旋等待直到锁被释放。自旋锁适用于锁的持有时间很短且线程竞争不激烈的情况。自旋锁的优点是避免了线程的阻塞和切换减少了线程上下文切换的开销但如果锁的持有时间较长或线程竞争激烈会导致自旋等待时间过长浪费 CPU 资源。 自适应自旋锁Adaptive Spin Lock自适应自旋锁是一种根据线程竞争情况动态调整自旋等待时间的锁机制。在自适应自旋锁中系统会根据之前获取锁的线程的自旋等待时间和锁的释放情况来决定当前线程的自旋等待时间。如果之前获取锁的线程自旋等待时间较长且成功获取到锁那么当前线程会增加自旋等待时间如果之前获取锁的线程自旋等待时间较短或未成功获取到锁那么当前线程会减少自旋等待时间。通过动态调整自旋等待时间自适应自旋锁可以更好地适应不同的线程竞争情况提高并发性能。
自适应自旋锁是在 JDK 6 中引入的一种优化机制通过减少自旋等待时间来提高并发性能。在 JDK 9 中默认情况下自适应自旋锁是开启的但也可以通过 JVM 参数来控制自适应自旋锁的行为。
需要注意的是自旋锁和自适应自旋锁适用于不同的场景。自旋锁适用于锁的持有时间短、线程竞争不激烈的情况而自适应自旋锁适用于锁的持有时间不确定、线程竞争较激烈的情况。在实际应用中可以根据具体的场景和需求选择合适的锁机制。
问题 38. 简述 锁膨胀锁粗化锁消除
解答
锁膨胀Lock Escalation、锁粗化Lock Coarsening和锁消除Lock Elimination是在编译器和运行时优化中常见的锁优化技术。 锁膨胀Lock Escalation锁膨胀指的是当一个线程多次获取同一个锁时锁的粒度逐渐扩大从细粒度锁升级为粗粒度锁。例如当一个线程多次获取某个对象的锁时如果发现该对象的锁已经被其他线程竞争那么锁膨胀机制会将该对象的锁升级为更高层次的锁如类锁或全局锁。锁膨胀的目的是减少锁的竞争和细粒度锁带来的开销提高并发性能。 锁粗化Lock Coarsening锁粗化指的是将多个连续的细粒度锁合并为一个粗粒度锁。当编译器发现一系列的连续的加锁和解锁操作且没有其他线程竞争这些锁时它会将这些细粒度锁合并为一个粗粒度锁减少加锁和解锁的次数从而提高性能。 锁消除Lock Elimination锁消除指的是在编译器优化阶段通过静态分析和逃逸分析等技术判断某些锁是多余的可以被消除。当编译器发现某个锁对象只被一个线程访问并且不会被其他线程访问时它会将该锁消除从而避免了不必要的加锁和解锁操作提高性能。
锁膨胀、锁粗化和锁消除都是为了优化锁的使用减少锁带来的开销提高并发性能。它们的具体应用和效果取决于具体的编译器和运行时环境。在实际开发中可以通过合理的代码设计和编写以及运行时环境的配置来充分利用这些锁优化技术提高程序的性能和并发能力。
问题 39. 简述 synchronized 和 volatile 的区别
解答
synchronized 和 volatile 是 Java 中用于实现线程同步和共享变量可见性的关键字它们有以下几个主要区别 作用范围synchronized 可以用于修饰代码块、方法和静态方法可以实现对代码块或方法的同步控制而 volatile 只能修饰成员变量用于实现对共享变量的可见性。 实现机制synchronized 是一种独占锁的机制它通过获取锁来保证同一时刻只有一个线程可以执行被 synchronized 修饰的代码块或方法而 volatile 是一种轻量级的同步机制它通过内存屏障和禁止指令重排序来保证共享变量的可见性。 原子性synchronized 可以保证代码块或方法的原子性即同一时刻只有一个线程可以执行该代码块或方法而 volatile 不能保证原子性它只能保证共享变量的可见性不能解决多线程并发修改共享变量的原子性问题。 内存语义synchronized 在释放锁时会将对共享变量的修改刷新到主内存中使得其他线程可以看到最新的值而 volatile 在写操作时会立即将对共享变量的修改刷新到主内存中并且在读操作时会从主内存中获取最新的值保证了共享变量的可见性。 适用场景synchronized 适用于多个线程之间需要互斥访问共享资源的场景可以保证线程安全而 volatile 适用于一个线程修改共享变量其他线程需要立即看到最新值的场景可以保证可见性。
需要注意的是synchronized 和 volatile 并不是完全互相替代的它们有不同的应用场景和使用方式。在实际开发中需要根据具体的需求和场景选择合适的关键字来实现线程同步和共享变量的可见性。
问题 40. 简述 synchronized 与 ReentrantLock 的区别
解答
synchronized 和 ReentrantLock 都是 Java 中用于实现线程同步的机制它们有以下几个主要区别 可重入性ReentrantLock 是可重入锁即同一个线程可以多次获取同一个锁而不会发生死锁而 synchronized 也是可重入的同一个线程可以多次获取同一个锁且在释放锁之前必须释放相同次数的锁。 锁的获取方式ReentrantLock 提供了公平锁和非公平锁两种获取锁的方式可以通过构造函数指定而 synchronized 是非公平锁无法指定获取锁的方式。 锁的灵活性ReentrantLock 提供了更多的灵活性和扩展性。它支持可中断的获取锁、超时获取锁、公平锁和非公平锁、多个条件变量等特性可以更精细地控制锁的行为而 synchronized 的功能相对简单无法提供这些额外的特性。 性能在低竞争的情况下synchronized 的性能通常比 ReentrantLock 好因为 synchronized 是 JVM 内置的关键字底层实现经过了优化而在高竞争的情况下ReentrantLock 的性能可能更好因为它提供了更细粒度的控制和更灵活的特性。 锁的释放方式synchronized 在代码块或方法执行完毕时会自动释放锁而 ReentrantLock 需要手动调用 unlock() 方法来释放锁需要注意避免忘记释放锁导致死锁的问题。
需要注意的是synchronized 是 JVM 内置的关键字使用起来更简单适用于大多数的线程同步场景而 ReentrantLock 是一个类提供了更多的灵活性和扩展性适用于一些特殊的线程同步需求。在实际开发中可以根据具体的需求和场景选择合适的机制来实现线程同步。