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