java后端面试题大全
- 1.JUC
-
- [1.1 ReentrantLock(可重入独占式锁)(难度:★★★ 频率:★★)](#1.1 ReentrantLock(可重入独占式锁)(难度:★★★ 频率:★★))
- [1.2 ReentrantLock比synchronized的优点(难度:★★★ 频率:★★)](#1.2 ReentrantLock比synchronized的优点(难度:★★★ 频率:★★))
- [1.3 CAS的原理和优缺点](#1.3 CAS的原理和优缺点)
- [1.4 synchronized与Lock的区别(难度:★★ 频率:★★★★)](#1.4 synchronized与Lock的区别(难度:★★ 频率:★★★★))
- [1.5 常用的原子类有哪些, 它们是如何保证原子性的?](#1.5 常用的原子类有哪些, 它们是如何保证原子性的?)
1.JUC
JUC指的是Java Util Concurrent(java工具中的并发工具), 是 Java SE 5.0 版本之后引入的一个用于支持并发编程的扩展包。
JUC 提供了一组强大的工具和框架,用于更方便、更安全地处理多线程编程的问题。
- Executor 框架
提供了一套用于管理线程池的接口和类,例如 ExecutorService、ThreadPoolExecutor 等,用于简化多线程任务的管理和执行。 - 并发集合类
提供了一些线程安全的集合类,例如 ConcurrentHashMap、ConcurrentLinkedQueue 等,用于在多线程环境下安全地操作集合 - Lock 框架
引入了显式的锁机制,例如 ReentrantLock,允许更灵活地进行同步操作。 - Atomic 变量
提供了一系列原子变量类,如 AtomicInteger、AtomicLong,用于在没有锁的情况下进行原子性操作。 - 并发工具类
包括 CountDownLatch、CyclicBarrier、Semaphore 等,用于协调多个线程的执行。 - Fork/Join 框架
提供了一种用于并行计算的框架,用于处理任务的分解和合并。 - 并发设计模式
提供了一些支持并发编程的设计模式,例如生产者-消费者模式、读写锁等。
1.1 ReentrantLock(可重入独占式锁)(难度:★★★ 频率:★★)
它实现了 Lock 接口。与传统的 synchronized 关键字相比,ReentrantLock 提供了更多的灵活性和功能。
- 可重入性(Reentrant)
同一个线程在持有锁的情况下,可以再次获取该锁,而不会发生死锁 - 公平性
多个线程竞争同一个锁时,这些线程能否按照它们发出请求的顺序获得锁 - 条件变量
提供了与锁关联的 Condition 对象,可以用于实现更复杂的线程间协调。
1.可重入性
ReentrantLock 的可重入性体现在同一个线程可以多次获得同一个锁,而不会发生死锁。这是通过内部维护一个持有锁的线程计数器来实现的。
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockReentrancyExample {
private final Lock lock = new ReentrantLock();
public void outerMethod() {
lock.lock(); // 第一次获得锁
try {
innerMethod(); // 在同一线程中调用另一个方法,再次获得锁
} finally {
lock.unlock(); // 第一次释放锁
}
}
public void innerMethod() {
lock.lock(); // 第二次获得锁
try {
// 执行需要同步的代码块
System.out.println("线程 " + Thread.currentThread().getName() + " 获得锁");
} finally {
lock.unlock(); // 第二次释放锁
System.out.println("线程 " + Thread.currentThread().getName() + " 释放锁");
}
}
public static void main(String[] args) {
ReentrantLockReentrancyExample example = new ReentrantLockReentrancyExample();
// 启动一个线程调用 outerMethod 方法
new Thread(() -> {
example.outerMethod();
}).start();
}
}
outerMethod 方法中调用了 innerMethod 方法,而这两个方法都在同一个线程中执行。当线程首次进入 outerMethod 时,它成功获得了锁,并在 innerMethod 中再次成功获得了同一个锁。在 innerMethod 中释放锁时,锁的计数器减为零,才真正释放了锁。这种方式确保了同一线程可以在持有锁的情况下多次进入同一个锁保护的代码块。
我们看下执行流程:
- 线程进入outerMethod,获取锁。
- 调用innerMethod,在同一线程中再次获取锁。
- innerMethod执行完毕后释放锁,但锁仍然保持在outerMethod中。
- outerMethod执行完毕后释放锁,此时其他线程有机会获取锁。
什么场景下,会使用到锁的可重入性
- 递归调用
当一个方法递归调用自身,并且这个方法是在持有锁的情况下调用的,可重入性允许同一个线程在递归调用中多次获取相同的锁,而不会发生死锁。
java
public class RecursiveExample {
private final Object lock = new Object();
public void recursiveMethod(int count) {
synchronized (lock) {
if (count > 0) {
System.out.println("Count: " + count);
recursiveMethod(count - 1); // 递归调用,再次获取相同锁
}
}
}
public static void main(String[] args) {
RecursiveExample example = new RecursiveExample();
example.recursiveMethod(3);
}
}
- 嵌套调用
当一个方法调用了另一个方法,而这两个方法都需要获取相同的锁时,可重入性使得同一个线程可以在嵌套调用中多次获取相同的锁。
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class NestedCallExample {
private final Lock lock = new ReentrantLock();
public void outerMethod() {
lock.lock(); // 第一次获得锁
try {
System.out.println("执行 outerMethod");
innerMethod(); // 在同一线程中再次获取相同锁
} finally {
lock.unlock(); // 第一次释放锁
}
}
public void innerMethod() {
lock.lock(); // 第二次获得锁
try {
System.out.println("执行 innerMethod");
} finally {
lock.unlock(); // 第二次释放锁
}
}
public static void main(String[] args) {
NestedCallExample example = new NestedCallExample();
example.outerMethod();
}
}
二、公平性
- 公平锁: 一个公平的锁会按照请求锁的顺序逐个地授予等待的线程,确保所有线程都有机会获得锁,避免饥饿(某个线程一直无法获取锁的情况)
java
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
- 非公平锁: 在一个非公平的锁中,当锁可用时,系统会任意选择一个等待的线程赋予锁,而不考虑等待的时间或请求的顺序。这可能导致某些线程一直无法获得锁,从而引发饥饿问题
java
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁
需要注意的是,虽然公平锁确保了锁的公平性,但在高并发环境下,公平锁的性能可能相对较低,因为每次都要考虑等待队列中线程的顺序。非公平锁可能会在性能上有一些优势,但可能导致某些线程长时间无法获得锁。
在实际应用中,一般情况下会使用非公平锁,因为在高并发的情况下,公平锁可能会导致线程频繁切换,影响性能。非公平锁虽然在一些情况下可能会引入不公平的竞争,但能够更好地提高并发性能。选择使用公平锁还是非公平锁需要根据具体的业务场景和性能需求来权衡。
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class FairnessExample {
private static final int THREAD_COUNT = 5;
private static final Lock fairLock = new ReentrantLock(true); // 公平锁
private static final Lock unfairLock = new ReentrantLock(false); // 非公平锁
private static void performTask(Lock lock, String lockType) {
for (int i = 0; i < 3; i++) {
lock.lock();
try {
System.out.println("Thread " + Thread.currentThread().getName() +
" acquired " + lockType + " lock, counter: " + i);
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
// 使用公平锁
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> performTask(fairLock, "fair")).start();
}
// 使用非公平锁
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> performTask(unfairLock, "unfair")).start();
}
}
}
在上述示例中,创建了两个 ReentrantLock,一个是公平锁,一个是非公平锁。在 performTask 方法中,线程通过 lock() 方法获取锁,执行一些操作,然后通过 unlock() 方法释放锁。运行这个程序,你会观察到使用公平锁时,线程按照请求锁的顺序获取锁,而使用非公平锁时,线程可能会插队成功,不按照请求的顺序获取锁。这是通过 ReentrantLock 的公平性来体现的。
1.2 ReentrantLock比synchronized的优点(难度:★★★ 频率:★★)
1.可中断性
可中断性是指在一个线程等待获取锁的过程中,如果其他线程对该等待线程进行中断(调用 interrupt() 方法),那么等待线程能够感知到中断,并有机会响应中断而不是一直等待下去。在这种情况下,等待线程会收到 InterruptedException 异常。
在使用 ReentrantLock 的 lockInterruptibly() 方法时,线程可以响应中断,即在等待锁的过程中,如果线程被其他线程中断,它会立即抛出 InterruptedException 异常,而不是一直等待。
下面是一个简单的示例,演示了 ReentrantLock 的可中断性:
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockInterruptExample {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
lock.lockInterruptibly(); // 可中断获取锁
try {
System.out.println("Thread 1 acquired the lock");
Thread.sleep(2000); // 模拟持有锁的一些操作
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("Thread 1 interrupted while waiting for the lock");
}
});
Thread thread2 = new Thread(() -> {
try {
lock.lockInterruptibly(); // 可中断获取锁
try {
System.out.println("Thread 2 acquired the lock");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("Thread 2 interrupted while waiting for the lock");
}
});
// 启动第一个线程,并让它持有锁
thread1.start();
// 等待一段时间,确保第一个线程先获取到锁
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 启动第二个线程,但在获取锁之前中断它
thread2.start();
thread2.interrupt();
}
}
结果输出:
Thread 1 acquired the lock
Thread 2 interrupted while waiting for the lock
第二个线程在获取锁之前被中断, 当中断时抛出InterruptedException, 从而可以在中断的处理代码块中执行相应的逻辑。
2.超时获取
ReentrantLock提供了一种超时获取锁的方式,即通过 tryLock(long time, TimeUnit unit) 方法,线程在一定的时间范围内尝试获取锁,如果在指定的时间内获取到锁,则返回 true,否则返回 false。这样可以避免线程一直阻塞等待锁,而是在一定时间内尝试获取,如果获取不到则可以执行其他逻辑或放弃锁的获取。
方法的参数 time 表示等待时间的数量,unit 表示等待时间的单位。如果在指定的时间内获取到锁,方法返回 true,否则返回 false。
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class ReentrantLockTimeoutExample {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) { // 尝试在2秒内获取锁
try {
System.out.println("Thread acquired the lock");
} finally {
lock.unlock();
}
} else {
System.out.println("Thread failed to acquire the lock within 2 seconds");
}
} catch (InterruptedException e) {
System.out.println("Thread interrupted while waiting for the lock");
}
});
// 启动线程
thread.start();
// 等待一段时间后中断线程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt(); // 中断线程
}
}
在上述示例中,一个线程尝试在2秒内获取锁,如果在2秒内获取到锁,则执行相应的逻辑,否则放弃锁的获取。在等待的过程中,另一个线程对该线程进行中断。在中断的情况下,tryLock 方法会抛出 InterruptedException 异常,并在异常处理代码块中执行相应的逻辑。这就是 ReentrantLock 的超时获取。
3.条件变量
ReentrantLock 提供了与锁关联的条件变量(Condition),条件变量允许线程以灵活的方式进行等待和通知,以实现更复杂的线程协作。条件变量提供了一种在某些条件不满足时线程等待的机制,并在条件满足时通知其他线程的方式。
要使用 ReentrantLock 的条件变量,首先需要通过 ReentrantLock 的 newCondition() 方法创建一个条件变量。然后,可以使用条件变量的 await() 方法让线程等待条件满足,以及使用 signal() 或 signalAll() 方法通知其他等待的线程条件已经满足。
下面是一个简单的示例,演示了 ReentrantLock 的条件变量的基本用法:
java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionExample {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean isConditionMet = false;
public void awaitCondition() throws InterruptedException {
lock.lock();
try {
while (!isConditionMet) {
// 等待条件满足
condition.await();
}
// 执行条件满足后的操作
System.out.println("Condition is met!");
} finally {
lock.unlock();
}
}
public void signalCondition() {
lock.lock();
try {
// 改变条件
isConditionMet = true;
// 通知等待的线程条件已满足
condition.signal();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ConditionExample example = new ConditionExample();
// 启动一个线程等待条件
new Thread(() -> {
try {
example.awaitCondition();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 主线程改变条件并通知等待的线程
example.signalCondition();
}
}
在这个例子中,awaitCondition 方法用于等待条件满足,而 signalCondition 方法用于改变条件并通知等待的线程。在使用条件变量时,通常需要在获取锁后使用 await 方法等待条件,而在满足条件时使用 signal 或 signalAll 方法通知等待的线程。条件变量的使用有助于实现更灵活的线程协作。
condition.await()会阻塞线程运行吗?
是的,condition.await()方法会阻塞当前线程的执行。在 ReentrantLock 中,await() 方法用于使当前线程等待,并释放当前线程持有的锁,直到其他线程调用相应的 signal() 或 signalAll() 方法,或者被中断,或者超时,才会继续执行。
具体来说,在 await() 被调用时,它会释放当前线程持有的锁,使其他线程有机会获取这个锁。当条件满足时,通过调用 signal() 或 signalAll() 来唤醒一个或所有等待的线程。被唤醒的线程会尝试重新获得锁,然后继续执行。
awaitCondition 方法中的 condition.await() 会使调用该方法的线程等待,直到其他线程调用 signalCondition 方法,将条件设置为满足,并唤醒等待的线程。
需要注意的是,在使用 await() 之前,必须先获得与 Condition 相关联的锁,否则会抛出 IllegalMonitorStateException。在 await() 期间,线程会释放锁,以便其他线程可以进入相应的临界区。
isConditionMet并没有使用voliate, 在线程中是怎么感知它的值变成true
虽然确实没有使用 volatile 关键字来声明 isConditionMet 变量,但是它是在同一个锁的保护下被读取和修改的。
在Java中,当一个线程在获取锁的时候,会从主内存中读取共享变量的最新值,并在执行过程中将其缓存在线程的本地内存中。其他线程在获取同一把锁时,会从主内存中重新读取共享变量的值。因为 ReentrantLock 是一个可重入锁,同一个线程在获取锁的时候,不会真正释放锁,因此它的本地内存中的共享变量值是可见的。
在你的例子中,awaitCondition 方法中的 while (!isConditionMet) 中的读取操作和 signalCondition 方法中的写入操作都在同一个 ReentrantLock 的保护下,这确保了线程在获取锁的时候能够看到最新的共享变量值。
虽然没有使用 volatile,但由于所有对 isConditionMet 的读取和写入都在同一把锁的保护下,因此在这个特定的上下文中,是可以保证可见性的。要注意的是,如果 isConditionMet 不是在同一个锁的保护下进行读写,或者在其他地方可能会涉及到多线程并发访问,那么使用 volatile 或其他同步手段可能是更安全的选择。
1.3 CAS的原理和优缺点
CAS,Compare And Swap(比较并交换),是一种多线程同步的原子操作, 用于实现多线程环境下的数据共享.
CAS操作包含三个参数
- 内存地址: 需要进行原子操作的内存地址
- 期望值: 预期内存位置的当前值, 可以理解为更新前的值, 用于比较
- 新值
1.原理:
- 读取内存值: CAS首先读取内存地址的当前值, 这个值通常称为
期望值
- 执行操作: CAS执行相应的操作, 例如给
期望值
赋值, 或者其他复杂的操作, 得到新值
- 比较值: CAS比较
期望值
与当前内存地址的实际值
是否相等, 如果相等, 说明在读取和执行操作的过程中, 没有其他线程修改了这个值 - 更新值: 比较成功后, CAS将
新值
写回到内存位置, 如果在这个过程中,其他线程修改了内存位置的值,CAS会失败。 - 检查CAS的结果: CAS操作通常返回一个布尔值,指示操作是否成功。如果操作成功,表示CAS过程中没有其他线程干扰,如果失败,则可能需要
重新尝试
整个CAS过程, 直到CAS操作成功, 或达到某个预定的重试次数
2.缺点:
- 循环时间长, 开销大: CAS 操作需要通过循环来不断尝试更新变量的值,直到成功。在高并发情况下,可能需要多次尝试才能成功,导致循环时间较长,增加了开销。
- ABA 问题: CAS 操作本质上是比较并交换,但它不会关心变量的值在比较前后是否发生了其他变化。这可能导致ABA问题,即一个值在经过一系列操作后又变回原始值,但 CAS 操作无法察觉到这种变化
1.4 synchronized与Lock的区别(难度:★★ 频率:★★★★)
synchronized | Lock | |
---|---|---|
实现方式 | Java 内置的关键字 | 是一个接口 |
锁的获取方式 | 使用关键字修饰的同步代码块或方法在进入时会自动获取锁,在退出时自动释放锁。如果发生异常,锁也会被释放 | 需要手动调用 Lock 接口的 lock() 方法获取锁,使用 unlock() 方法释放锁。通常使用 try-finally 语句确保在获取锁后最终会释放锁,以处理异常情况。 |
可中断性 | 不支持中断。如果一个线程在获取锁的过程中被中断,它会一直等待锁的释放。 | 支持中断。线程可以在等待锁的过程中响应中断,从而提高了对中断的响应性 |
公平性 | 不保证公平性。线程争夺锁的顺序是不确定的,可能导致某些线程一直无法获取锁。 | 可以选择是否公平。ReentrantLock 的构造方法可以传入一个参数,用于指定锁是否为公平锁,默认为非公平锁。 |
条件变量 | 内置的 wait() 和 notify() 方法用于支持条件等待和通知。 | Lock 接口提供了显式的条件变量,可以使用 newCondition() 方法创建条件对象,并使用 await()、signal() 和 signalAll() 方法进行等待和通知 |
1.5 常用的原子类有哪些, 它们是如何保证原子性的?
- 基本类型
- AtomicBoolean
- AtomicInteger
- AtomicLong
- 引用类型
- AtomicReference
- AtomicStampedReference
- AtomicReferenceFieldUpdater
- AtomicMarkableReference
- 数组类型
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
- 字段类型
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
- AtomicStampedFieldUpdater
- JDK8新增原子类简介
- DoubleAccumulator
- LongAccumulator
- DoubleAdder
- LongAdder
原子类的实现通常使用CAS操作