大家好,我是栗筝i,从 2022 年 10 月份开始,我便开始致力于对 Java 技术栈进行全面而细致的梳理。这一过程,不仅是对我个人学习历程的回顾和总结,更是希望能够为各位提供一份参考。因此得到了很多读者的正面反馈。 而在 2023 年 10 月份开始,我将推出 Java 面试题/知识点系列内容,期望对大家有所助益,让我们一起提升。 今天与您分享的,是 Java 并发知识面试题系列的总结篇(下篇),我诚挚地希望它能为您带来启发,并在您的职业生涯中起到助益作用。衷心感谢每一位朋友的关注与支持。
解答:
volatile 是 Java 中用于实现共享变量可见性的关键字。它具有以下特点:
volatile 的使用场景包括但不限于以下情况:
需要注意的是,虽然 volatile 可以保证共享变量的可见性,但它并不能解决所有的并发问题。在一些复杂的并发场景中,可能需要使用其他的同步机制,如锁(synchronized、ReentrantLock)、原子类(Atomic 类)等来保证线程安全。
总之,volatile 是 Java 中用于实现共享变量可见性的关键字,它通过禁止指令重排序和及时刷新主内存来保证共享变量的可见性。在适当的场景下,使用 volatile 可以提供简单而有效的线程同步机制。
解答:
volatile 关键字可以保证共享变量的可见性,即当一个线程对 volatile 变量进行写操作时,其他线程可以立即看到最新的值。
volatile 保证可见性的原理如下:
需要注意的是,volatile 关键字只能保证可见性,不能保证原子性。如果一个变量的操作需要保证原子性,需要使用其他的同步机制,如 synchronized 或 Atomic 类。
在实际应用中,可以使用 volatile 关键字来修饰标识状态的变量,确保多个线程可以及时看到最新的状态。例如,在双重检查锁定(Double-Checked Locking)中,使用 volatile 可以保证单例对象的可见性和正确初始化。
总之,volatile 关键字通过内存屏障和缓存一致性协议来保证共享变量的可见性。它是一种简单而有效的线程同步机制,适用于一些特定的场景,但不能解决所有的并发问题。在使用 volatile 时,需要根据具体的需求和场景进行合理的选择和使用。
解答:
volatile 关键字可以保证共享变量的有序性,即禁止指令重排序,确保 volatile 变量的读写操作按照程序的顺序执行。
volatile 保证有序性的原理如下:
需要注意的是,volatile 关键字只能保证有序性,不能保证原子性。如果一个变量的操作需要保证原子性,需要使用其他的同步机制,如 synchronized 或 Atomic 类。
在实际应用中,可以使用 volatile 关键字来修饰需要保证有序性的共享变量。例如,在双重检查锁定(Double-Checked Locking)中,使用 volatile 可以保证单例对象的可见性和正确初始化。
总之,volatile 关键字通过内存屏障和禁止指令重排序来保证共享变量的有序性。它是一种简单而有效的线程同步机制,适用于一些特定的场景,但不能解决所有的并发问题。在使用 volatile 时,需要根据具体的需求和场景进行合理的选择和使用。
解答:
volatile 关键字在读写操作前后会插入内存屏障(Memory Barrier),也称为内存栅栏。内存屏障具有以下两个作用:
内存屏障的作用是保证 volatile 变量的可见性和有序性。它通过防止指令重排序和强制刷新缓存来确保对 volatile 变量的读写操作按照程序的顺序执行,并且保证了对其他线程的可见性。
需要注意的是,内存屏障的具体实现是由编译器和处理器来完成的,不同的编译器和处理器可能有不同的实现方式。在实际应用中,可以依赖于 volatile 关键字来使用内存屏障,而无需过多关注内存屏障的具体实现细节。
总之,volatile 关键字通过插入内存屏障来保证对 volatile 变量的读写操作的有序性和可见性。内存屏障阻止指令重排序和强制刷新缓存,确保了对其他线程的可见性和最新值的获取。
解答:
happens-before 是 Java 内存模型(Java Memory Model,JMM)中的一个概念,用于描述多线程程序中操作的顺序性和可见性。
happens-before 原则规定了在多线程环境下,对共享变量的写操作对于其他线程的读操作具有可见性和顺序性。具体来说,如果一个操作 happens-before 另一个操作,那么第一个操作的结果对于第二个操作是可见的,并且第一个操作在时间上发生在第二个操作之前。
happens-before 原则的几个规则如下:
happens-before 原则提供了一种在多线程环境下推断操作顺序和可见性的规则,帮助开发者编写正确的多线程程序。通过遵循 happens-before 原则,可以确保多线程程序的正确性和可靠性。
需要注意的是,happens-before 原则只是描述了操作之间的顺序关系和可见性,而不是保证原子性。如果需要保证操作的原子性,需要使用其他的同步机制,如锁(synchronized、ReentrantLock)、原子类(Atomic 类)等。
总之,happens-before 原则是 Java 内存模型中描述多线程程序操作顺序性和可见性的规则。遵循 happens-before 原则可以确保多线程程序的正确性和可靠性。
解答:
happens-before 是 Java 并发编程中的一个概念,用于描述多线程之间操作的顺序关系。happens-before 规则定义了一组规则,用于确定在多线程环境下,一个操作是否可以看到另一个操作的结果。下面是 happens-before 的八条规则:
这些规则提供了一种可靠的方式来推断多线程程序中操作的顺序关系,帮助开发者编写正确的并发代码。通过遵守 happens-before 规则,可以避免一些常见的并发问题,如数据竞争和内存可见性问题。
解答:
ReentrantLock 是 Java 并发编程中的一种锁机制,它实现了 Lock 接口,提供了与 synchronized 关键字类似的功能,但更加灵活和可扩展。
ReentrantLock 是可重入锁,也就是说同一个线程可以多次获取同一个锁,而不会造成死锁。它使用了一种叫做 “互斥性” 的机制,确保同一时刻只有一个线程可以执行被锁住的代码块。
ReentrantLock 提供了以下特性:
相比于 synchronized 关键字,ReentrantLock 提供了更多的灵活性和功能,但使用起来也更加复杂。在使用 ReentrantLock 时,需要手动调用 lock() 方法获取锁,并在合适的时机调用 unlock() 方法释放锁,以确保线程安全和避免死锁的发生。同时,需要注意避免忘记释放锁,导致资源泄露的问题。
解答:
ReentrantLock 是基于 AbstractQueuedSynchronizer(AQS)的实现的。AQS 是一个用于构建锁和同步器的框架,ReentrantLock 利用了 AQS 提供的底层机制来实现锁的功能。
ReentrantLock 内部维护了一个 Sync 对象,Sync 是 ReentrantLock 的内部类,它继承了 AQS 并重写了其中的方法。Sync 类实现了独占锁的语义,通过维护一个 state 变量来表示锁的状态。
当一个线程调用 ReentrantLock 的 lock() 方法时,它会尝试获取锁。如果锁当前没有被其他线程占用,那么该线程就会成功获取锁,并将 state 设置为 1。如果锁已经被其他线程占用,那么当前线程就会进入等待队列,并被阻塞。
当一个线程释放锁时,它会调用 unlock() 方法,该方法会将 state 减 1。如果 state 变为 0,表示锁已经完全释放,此时会唤醒等待队列中的一个线程,使其获取锁。
ReentrantLock 还支持可重入性,即同一个线程可以多次获取锁。在 ReentrantLock 中,每个线程都维护了一个 holdCount 变量,用于记录当前线程获取锁的次数。当一个线程再次获取锁时,只需要将 holdCount 加 1,当释放锁时,将 holdCount 减 1。只有当 holdCount 变为 0 时,才会真正释放锁。
ReentrantLock 还提供了公平锁和非公平锁的选择。公平锁会按照线程请求的顺序来获取锁,而非公平锁允许插队,可能会导致某些线程长时间等待。
总结来说,ReentrantLock 的实现原理是基于 AQS 的,通过维护一个 state 变量和等待队列来实现锁的获取和释放,同时支持可重入性和公平性。这种基于 AQS 的实现方式使得 ReentrantLock 具有更高的灵活性和可扩展性。
解答:
ReentrantLock 实现可重入性的关键在于两个方面:线程标识和计数器。
通过线程标识和计数器的组合,ReentrantLock 实现了可重入性。当一个线程再次获取锁时,会检查 owner 是否为当前线程,如果是,则允许再次获取锁,并将 holdCount 加 1。这样就可以实现同一个线程多次获取锁的效果。
可重入性的实现使得同一个线程可以在持有锁的情况下,多次进入被锁住的代码块,而不会造成死锁。同时,ReentrantLock 还提供了相应的 unlock() 方法来释放锁,并将 holdCount 减 1。只有当 holdCount 变为 0 时,才会真正释放锁,其他线程才有机会获取锁。
总结来说,ReentrantLock 实现可重入性的方式是通过线程标识和计数器的组合来实现的。线程标识用于判断当前线程是否已经持有锁,计数器用于记录当前线程获取锁的次数。这种机制使得同一个线程可以多次获取锁,避免了死锁的发生。
解答:
AQS(AbstractQueuedSynchronizer)是 Java 并发编程中的一个抽象类,它提供了一种用于构建锁和同步器的框架。AQS 是许多并发工具的基础,如 ReentrantLock、CountDownLatch、Semaphore 等。
AQS 的核心思想是使用一个 FIFO(先进先出)的等待队列来管理线程的竞争和等待状态。它通过内部的状态变量来表示锁的状态,并提供了一组方法来操作和管理这个状态。
AQS 的主要特点和功能包括:
通过继承 AQS 并重写其中的方法,可以实现自定义的同步器。AQS 提供了一些模板方法,如 acquire()、release() 等,用于实现具体的获取和释放锁的逻辑。通过这些方法的组合和调用,可以构建出各种不同类型的锁和同步器。
总结来说,AQS 是一个用于构建锁和同步器的框架,通过状态管理、等待队列、线程阻塞和唤醒等机制,提供了一种灵活可扩展的方式来实现并发控制。它是许多并发工具的基础,为 Java 并发编程提供了强大的支持。
解答:
AQS(AbstractQueuedSynchronizer)通过内部的同步状态(sync state)来表示锁的状态。同步状态是一个整数变量,用于表示锁的状态信息。
AQS 的同步状态处理主要涉及以下几个方面:
通过对同步状态的处理,AQS 实现了锁的获取和释放的机制,并提供了灵活的条件变量来实现线程间的等待和通知。通过继承 AQS 并重写其中的方法,可以实现自定义的同步器,根据具体的需求来处理同步状态的更新和条件变量的使用。
总结来说,AQS 通过同步状态的处理来实现锁的获取和释放的机制,并提供了条件变量来实现线程间的等待和通知。同步状态的处理是 AQS 实现并发控制的核心机制之一,为构建各种类型的锁和同步器提供了基础。
解答:
AQS(AbstractQueuedSynchronizer)使用 FIFO(先进先出)队列来管理等待线程的竞争和等待状态。这个队列被称为等待队列(wait queue)或者阻塞队列(blocking queue)。
AQS FIFO 队列的设计主要涉及以下几个方面:
通过使用 FIFO 队列,AQS 实现了公平性的机制。当一个线程无法获取锁时,它会被加入到等待队列的尾部,按照先来先服务的原则等待获取锁。当锁的状态发生变化时,AQS 会从等待队列的头部唤醒一个或多个线程,使其有机会再次竞争锁。
总结来说,AQS 使用 FIFO 队列来管理等待线程的竞争和等待状态。通过节点的入队和出队操作,以及等待状态的管理,实现了线程的有序等待和唤醒。这种设计使得 AQS 能够提供公平性的机制,确保线程按照先来先服务的顺序获取锁。
解答:
AQS(AbstractQueuedSynchronizer)提供了一种机制来实现共享资源的竞争和释放。通过 AQS,可以实现多个线程对共享资源的并发访问控制。
AQS 共享资源的竞争和释放主要涉及以下几个方面:
通过对共享资源的竞争和释放的处理,AQS 实现了对共享资源的并发访问控制。通过继承 AQS 并重写其中的方法,可以实现自定义的同步器,根据具体的需求来处理共享资源的竞争和释放。
总结来说,AQS 提供了一种机制来实现共享资源的竞争和释放。通过同步状态的处理和条件变量的使用,可以实现对共享资源的并发访问控制。这种机制为构建各种类型的锁和同步器提供了基础,为 Java 并发编程提供了强大的支持。
解答:
Java 中的 Atomic 类主要包括以下几种:
AtomicInteger
:提供了一个可以原子性更新的 int
类型。
AtomicLong
:提供了一个可以原子性更新的 long
类型。
AtomicBoolean
:提供了一个可以原子性更新的 boolean
类型。
AtomicReference
:提供了一个可以原子性更新的引用类型。
AtomicIntegerArray
、AtomicLongArray
和 AtomicReferenceArray
:分别提供了可以原子性更新的 int
、long
和引用类型数组。
AtomicMarkableReference
:提供了一个可以原子性更新的带有标记位的引用类型。
AtomicStampedReference
:提供了一个可以原子性更新的带有版本号的引用类型。
这些类主要用于实现无锁的线程安全操作,其内部主要通过 CAS
(Compare And Swap)操作来保证线程安全。
解答:
Java 中的 Atomic 类主要依赖于 CAS(Compare And Swap)操作来实现线程安全的更新操作。
CAS 是一种无锁算法,其基本思想是:系统给每个读取出来的变量都配对上一个版本号,每次更新时检查当前版本号和最初读取出来的版本号是否一致,如果一致则更新,否则不进行任何操作。
Java 的 Atomic 类主要通过调用 Unsafe 类的 CAS 相关的本地方法来实现。例如,在 AtomicInteger 类中,使用了 Unsafe 类的 compareAndSwapInt
方法来实现 compareAndSet
方法,该方法会尝试将变量的值从预期值更新为新的值,并返回操作是否成功。
这种方式可以有效地减少线程同步的开销,提高并发性能。但是,CAS 操作也存在一些问题,例如 ABA 问题、循环时间长和 CPU 开销大等,需要在实际使用时注意。
解答:
CountDownLatch
是 Java 并发编程中的一个同步工具类,它允许一个或多个线程等待直到在其他线程中执行的一组操作完成。
CountDownLatch
提供了一个构造函数,接收一个 int
类型的参数作为计数器。如果你想让在一个线程中等待 N 个线程完成某个任务,可以传递 N 个计数器。
当我们调用 CountDownLatch
的 countDown
方法时,N 就会减 1。
CountDownLatch
的 await
方法会阻塞当前线程,直到 N 变成零。由于 countDown
方法可以用在任何地方,所以这里说的 N 个线程可以是一个线程的 N 个执行步骤。也可以是 N 个线程。当 N 变为 0 时,表示锁打开,所有调用 await
方法的线程都会继续执行。
注意,CountDownLatch
无法重置计数器,这意味着一旦计数器的值变为 0,所有 await
的线程都会继续进行,后续的 countDown
调用不会再有任何效果。如果需要重置计数器,可以考虑使用 CyclicBarrier
或 Semaphore
。
解答:
CyclicBarrier
是 Java 并发编程中的一个同步工具类,它允许一组线程互相等待,直到所有线程都达到一个公共的屏障点(Barrier Point)。
CyclicBarrier
提供了一个构造函数,接收一个 int
类型的参数作为屏障点,表示需要相互等待的线程数量。
当一个线程调用 CyclicBarrier
的 await
方法时,该线程会被阻塞,直到所有线程都调用了 await
方法,即达到了屏障点,所有线程才会继续执行。
此外,CyclicBarrier
还提供了一个带有 Runnable
参数的构造函数,当所有线程都达到屏障点后,Runnable
任务会被执行。这个 Runnable
任务可以用于更新共享状态,或者进行一些集体工作。
与 CountDownLatch
不同的是,CyclicBarrier
可以重用。当所有等待线程都被释放后,CyclicBarrier
的计数会重置,可以再次用来等待一组线程达到屏障点。
解答:
Semaphore
是 Java 并发编程中的一个同步工具类,它主要用于限制可以访问某些资源(物理或逻辑的)的线程数量。
Semaphore
提供了一个构造函数,接收一个 int
类型的参数作为许可证数量。这个数量就是同时访问特定资源的最大线程数量。
当一个线程尝试获取一个许可证以访问某个资源时,可以调用 Semaphore
的 acquire
方法。如果 Semaphore
内部的当前许可证数量大于 0,那么 Semaphore
就会减少一个许可证并允许这个线程访问资源。如果当前许可证数量为 0,那么 acquire
方法会阻塞,直到有其他线程释放一个许可证。
当一个线程完成对资源的访问后,可以调用 Semaphore
的 release
方法来释放一个许可证。这会增加 Semaphore
内部的当前许可证数量。如果有其他线程因为调用 acquire
方法而被阻塞,那么它们中的一个会被选择并被允许访问资源。
Semaphore
可以用于实现资源池,如数据库连接池等。
解答:
Exchanger
是 Java 并发编程中的一个同步工具类,它提供了一个同步点,在这个同步点,两个线程可以交换各自的数据。
Exchanger
的主要方法是 exchange
,它有两个版本:一个是可以中断的版本,另一个是不可中断的版本。当一个线程到达交换点,它会调用 exchange
方法,将自己的数据传入方法,然后阻塞等待另一个线程到达。当另一个线程也到达交换点,它也会调用 exchange
方法,将自己的数据传入方法。这时,两个线程的数据就会被交换,然后两个线程都会从 exchange
方法返回,返回的结果就是另一个线程传入的数据。
Exchanger
可以用于遗传算法、流水线设计等场景。例如,在遗传算法中,可以用 Exchanger
来交换种群;在流水线设计中,可以用 Exchanger
来交换生产线上的产品。
解答:
Unsafe
类是 sun.misc 包下的一个类,它提供了一些可以直接操作内存、线程、类等的方法,这些方法主要被系统和 JVM 使用,一般不建议在业务代码中使用。
以下是 Unsafe
类的一些主要功能:
Unsafe
类可以分配、释放、修改直接内存,这在一些高性能的场景下会被使用,比如 NIO、Netty 等。
Unsafe
类可以挂起和恢复线程,这在实现一些底层的并发操作时会被使用。
Unsafe
类提供了硬件级别的 CAS 操作,这是实现高效并发算法的基础。
Unsafe
类可以操作类、对象和变量,比如获取对象的大小、修改对象变量的值、获取变量的地址等。
Unsafe
类提供了创建内存屏障的方法。
需要注意的是,由于 Unsafe
类的部分方法非常底层,使用不当可能会导致 JVM 崩溃,因此在一般的业务开发中,我们应该尽量避免使用 Unsafe
类。