一、Java并发基础
1. 什么是Java中的线程?线程与进程有什么区别?
回答:
- 线程(Thread):
- 是操作系统能够进行运算调度的最小单位。
- 线程是进程中的一个执行路径,共享进程的资源(如内存)。
- Java中的
Thread类和Runnable接口用于创建和管理线程。
- 进程(Process):
- 是具有一定独立功能的程序关于某数据集合上的一次运行活动。
- 进程之间是相互独立的,每个进程有自己的内存空间。
- 一个进程可以包含多个线程。
区别:
- 资源占用:进程拥有独立的内存空间,线程共享进程的内存。
- 创建和销毁:线程的创建和销毁比进程更高效。
- 通信方式:线程间通信(共享内存)比进程间通信(如管道、信号)更简单和高效。
2. Java中如何创建和启动一个线程?
回答:
Java中创建和启动线程有两种主要方式:
-
继承
Thread类:public class MyThread extends Thread { @Override public void run() { System.out.println("Thread is running."); } public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); // 启动线程 } } -
实现
Runnable接口:public class MyRunnable implements Runnable { @Override public void run() { System.out.println("Runnable is running."); } public static void main(String[] args) { Thread thread = new Thread(new MyRunnable()); thread.start(); // 启动线程 } }
推荐:实现Runnable接口更灵活,因为Java不支持多继承,可以同时继承其他类。
3. synchronized关键字的作用是什么?它是如何工作的?
回答:
-
作用:
synchronized关键字用于实现线程同步,防止多个线程同时访问共享资源导致数据不一致。- 它可以修饰方法或代码块,确保在同一时间只有一个线程执行被修饰的部分。
-
工作原理:
- 每个对象都有一个内置的锁(monitor)。
- 当一个线程进入
synchronized方法或代码块时,会获取相应对象的锁。 - 其他线程如果尝试进入同一锁的
synchronized方法或代码块,会被阻塞,直到锁被释放。
-
示例:
public class Counter { private int count = 0; // 同步方法 public synchronized void increment() { count++; } // 同步代码块 public void decrement() { synchronized(this) { count--; } } public int getCount() { return count; } }
注意:
- 使用
synchronized会带来性能开销,应合理使用。 - 避免锁的嵌套和死锁。
4. 什么是可重入锁(Reentrant Lock)?它与synchronized有何不同?
回答:
-
可重入锁(Reentrant Lock):
- 是指同一个线程可以多次获取同一把锁而不会被阻塞。
- Java中通过
java.util.concurrent.locks.ReentrantLock类实现。
-
与
synchronized的区别:-
功能丰富:
ReentrantLock提供了比synchronized更丰富的功能,如公平锁、可中断的锁获取、尝试锁定等。
-
灵活性:
ReentrantLock可以在不同的代码块之间共享锁。
-
性能:
- 在某些场景下,
ReentrantLock的性能可能优于synchronized,但现代JVM对synchronized进行了优化,两者性能差异已较小。
- 在某些场景下,
-
解锁机制:
ReentrantLock需要显式调用unlock()方法释放锁,通常在finally块中进行,以确保锁被释放。synchronized自动释放锁。
-
-
示例:
import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockExample { private final ReentrantLock lock = new ReentrantLock(); private int count = 0; public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } public int getCount() { return count; } }
注意:使用ReentrantLock时必须确保在所有可能的执行路径上都释放锁,否则会导致死锁。
5. 线程的生命周期有哪些阶段?
回答:
线程的生命周期主要包括以下几个阶段:
- 新建(New):
- 线程对象被创建,但尚未启动。
- 通过
new Thread()创建线程实例。
- 就绪(Runnable):
- 线程已启动并等待CPU时间片执行。
- 调用
start()方法后,线程进入就绪状态。
- 运行(Running):
- 线程获得CPU资源,开始执行
run()方法中的代码。
- 线程获得CPU资源,开始执行
- 阻塞(Blocked):
- 线程因为等待锁或资源而暂时停止执行。
- 例如,等待
synchronized锁释放,或调用wait()、sleep()等方法。
- 等待(Waiting):
- 线程在等待另一个线程执行某个特定操作。
- 通过
Object.wait()、Thread.join()、Lock的await()等方法进入等待状态。
- 计时等待(Timed Waiting):
- 线程在等待指定时间后会自动恢复就绪状态。
- 通过
Thread.sleep(long millis)、Object.wait(long timeout)、Lock的await(long time, TimeUnit unit)等方法进入计时等待状态。
- 终止(Terminated):
- 线程执行完
run()方法或被异常终止,进入终止状态。
- 线程执行完
状态转换图:
New
|
start()
|
Runnable <----> Blocked
| |
run() acquire lock
| |
Running |
| |
finish() |
| |
Terminated
二、线程池(ThreadPool)
6. 线程池的作用是什么?为什么要使用线程池?
回答:
-
作用:
- 管理和复用多个线程,避免频繁创建和销毁线程带来的性能开销。
- 提供线程的生命周期管理,包括线程的创建、调度和销毁。
- 控制并发执行的线程数量,避免系统过载。
-
为什么要使用线程池:
-
性能优化:
- 减少线程创建和销毁的开销,提升性能。
-
资源管理:
- 控制并发执行的线程数量,避免系统资源耗尽。
-
任务管理:
- 提供任务排队和调度机制,合理分配任务到线程执行。
-
稳定性:
- 通过线程池的参数配置(如核心线程数、最大线程数、队列容量等),提升系统的稳定性和可预测性。
-
便捷性:
- 提供丰富的接口和工具类,简化多线程编程。
-
7. Java中如何创建一个线程池?请解释Executors类提供的不同类型的线程池。
回答:
Java中可以通过java.util.concurrent.Executors工厂类创建不同类型的线程池。以下是常见的线程池类型及其特点:
-
Fixed Thread Pool(固定大小线程池):
- 使用固定数量的线程执行任务。
- 如果所有线程都在忙碌,任务会被放入等待队列。
- 适用于负载稳定且任务数量已知的场景。
创建方式:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10); -
Cached Thread Pool(缓存线程池):
- 根据需要创建新线程执行任务,空闲线程在一定时间后会被回收。
- 适用于执行大量短期异步任务。
- 可能创建无限数量的线程,需谨慎使用以避免资源耗尽。
创建方式:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); -
Single Thread Executor(单线程池):
- 只有一个线程执行任务,任务按提交顺序依次执行。
- 适用于需要保证任务按顺序执行的场景。
创建方式:
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); -
Scheduled Thread Pool(定时线程池):
- 支持定时和周期性任务执行。
- 适用于需要延迟执行或定期执行的任务。
创建方式:
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5); -
Work Stealing Pool(工作窃取线程池,Java 8引入):
- 基于ForkJoinPool实现,适用于处理大量的短期任务。
- 线程可以窃取其他线程的任务,提高资源利用率。
创建方式:
ExecutorService workStealingPool = Executors.newWorkStealingPool();
示例:
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
final int index = i;
executor.submit(() -> {
System.out.println("Task " + index + " is running on " + Thread.currentThread().getName());
});
}
executor.shutdown();
注意:
- 使用
Executors工厂方法创建的线程池有一些缺陷(如无限线程池可能导致资源耗尽),在生产环境中更推荐使用ThreadPoolExecutor构造器进行自定义配置。
8. 什么是ThreadPoolExecutor?解释其主要参数。
回答:
ThreadPoolExecutor是Java中最灵活和强大的线程池实现类,位于java.util.concurrent包下。它提供了丰富的参数和方法,允许开发者根据具体需求自定义线程池行为。
主要参数:
- corePoolSize(核心线程数):
- 线程池中始终保持的最小线程数量。
- 线程池启动时,会创建核心线程数的线程。
- 即使线程处于空闲状态,也不会被回收,除非设置了
allowCoreThreadTimeOut。
- maximumPoolSize(最大线程数):
- 线程池中允许的最大线程数量。
- 当任务队列已满且核心线程都在忙碌时,线程池会创建新的非核心线程,直到达到最大线程数。
- keepAliveTime(线程空闲保持时间):
- 非核心线程在空闲时等待新任务的最长时间。
- 超过此时间,空闲的非核心线程会被回收。
- unit(时间单位):
keepAliveTime的时间单位,如TimeUnit.SECONDS、TimeUnit.MILLISECONDS等。
- workQueue(任务队列):
- 用于存储待执行任务的队列。
- 常见实现类包括:
LinkedBlockingQueue:有界或无界的阻塞队列,适用于较多读操作。SynchronousQueue:不存储元素的阻塞队列,适用于需要直接提交任务给线程执行的场景。ArrayBlockingQueue:有界的阻塞队列,适用于固定容量的任务队列。
- threadFactory(线程工厂):
- 用于创建新线程的工厂,允许自定义线程的创建方式,如设置线程名、守护线程等。
- handler(拒绝策略):
- 当任务无法被执行时的处理策略。
- 常见策略包括:
AbortPolicy(默认):抛出RejectedExecutionException。CallerRunsPolicy:由调用线程执行任务。DiscardPolicy:静默丢弃任务。DiscardOldestPolicy:丢弃队列中最旧的任务,尝试执行新任务。
构造示例:
import java.util.concurrent.*;
public class CustomThreadPool {
public static void main(String[] args) {
int corePoolSize = 5;
int maximumPoolSize = 10;
long keepAliveTime = 60;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
// 提交任务
for (int i = 0; i < 50; i++) {
final int index = i;
executor.execute(() -> {
System.out.println("Task " + index + " is running on " + Thread.currentThread().getName());
});
}
executor.shutdown();
}
}
注意:
- 合理配置线程池参数(核心线程数、最大线程数、任务队列大小、拒绝策略等)对系统性能和稳定性至关重要。
- 避免使用
Executors工厂方法创建线程池,因为它们隐藏了线程池的具体参数配置,可能导致资源问题。
9. 什么是拒绝策略(Rejection Handler)?Java中有哪些常见的拒绝策略?
回答:
-
拒绝策略(Rejection Handler):
- 当线程池无法执行提交的任务时,
ThreadPoolExecutor会调用拒绝策略处理该任务。 - 任务无法执行的原因通常是线程池已达到最大线程数,并且任务队列已满。
- 当线程池无法执行提交的任务时,
-
常见的拒绝策略:
-
AbortPolicy(默认):
- 抛出
RejectedExecutionException,拒绝任务的提交。
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); - 抛出
-
CallerRunsPolicy:
- 由调用线程(提交任务的线程)执行该任务。
- 不会抛出异常,适用于缓解任务提交速率过高的情况。
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); -
DiscardPolicy:
- 静默丢弃被拒绝的任务,不抛出任何异常。
- 适用于某些不关心丢失任务的场景。
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy(); -
DiscardOldestPolicy:
- 丢弃任务队列中最旧的未处理任务,然后尝试提交当前任务。
- 适用于希望保留最新任务的场景。
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardOldestPolicy();
-
-
自定义拒绝策略:
- 通过实现
RejectedExecutionHandler接口,可以定义自定义的拒绝策略。
RejectedExecutionHandler customHandler = new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { // 自定义处理逻辑,如记录日志、重新提交任务等 System.out.println("Task rejected: " + r.toString()); } }; - 通过实现
选择策略:
- AbortPolicy:适用于不允许任务丢失的场景。
- CallerRunsPolicy:适用于希望减缓任务提交速度,并保持任务执行的场景。
- DiscardPolicy:适用于任务丢失不会影响系统的场景。
- DiscardOldestPolicy:适用于希望保留最新任务,丢弃最旧任务的场景。
10. 什么是ScheduledExecutorService?它有哪些常用的方法?
回答:
-
ScheduledExecutorService:- 是
ExecutorService的子接口,提供了定时和周期性任务执行的能力。 - 支持在指定延迟后执行任务,或按照固定频率执行任务。
- 是
-
常用方法:
-
schedule:- 延迟指定时间后执行一个任务。
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); scheduler.schedule(() -> System.out.println("Task executed after delay"), 5, TimeUnit.SECONDS); -
scheduleAtFixedRate:- 按固定频率执行任务,任务之间的间隔时间是固定的,不考虑任务执行时间。
scheduler.scheduleAtFixedRate(() -> System.out.println("Periodic task"), 0, 10, TimeUnit.SECONDS); -
scheduleWithFixedDelay:- 按固定延迟执行任务,任务之间的间隔时间是固定的,任务执行完成后开始计时。
scheduler.scheduleWithFixedDelay(() -> System.out.println("Periodic task with delay"), 0, 10, TimeUnit.SECONDS); -
shutdown:- 关闭线程池,不再接受新任务,但会执行已提交的任务。
scheduler.shutdown(); -
shutdownNow:- 立即关闭线程池,尝试停止所有正在执行的任务。
scheduler.shutdownNow();
-
-
示例:
import java.util.concurrent.*; public class ScheduledExecutorExample { public static void main(String[] args) { ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); // 延迟3秒执行任务 scheduler.schedule(() -> System.out.println("Task executed after 3 seconds"), 3, TimeUnit.SECONDS); // 每5秒执行一次任务,固定频率 scheduler.scheduleAtFixedRate(() -> System.out.println("Fixed Rate Task"), 0, 5, TimeUnit.SECONDS); // 每5秒执行一次任务,固定延迟 scheduler.scheduleWithFixedDelay(() -> System.out.println("Fixed Delay Task"), 0, 5, TimeUnit.SECONDS); // 让线程池运行一段时间后关闭 scheduler.schedule(() -> { scheduler.shutdown(); System.out.println("Scheduler shutdown."); }, 20, TimeUnit.SECONDS); } }
注意:
- 定时任务应合理配置延迟和频率,避免任务执行时间过长影响后续任务。
- 使用
shutdown()或shutdownNow()方法正确关闭线程池,释放资源。
三、锁机制
11. 什么是锁(Lock)?Java中有哪些锁的实现?
回答:
-
锁(Lock):
- 是一种用于控制多个线程对共享资源访问的同步机制。
- 通过锁机制,确保在同一时间只有一个线程可以访问临界区,避免数据竞争和不一致。
-
Java中锁的实现:
-
内置锁(Intrinsic Lock):
- 通过
synchronized关键字实现,隐式获取和释放锁。 - 每个对象都有一个内置锁(monitor)。
- 通过
-
显示锁(Explicit Lock):
- 通过
java.util.concurrent.locks.Lock接口及其实现类实现。 - 提供更灵活的锁控制,如可中断的锁获取、非阻塞尝试锁定等。
常见实现类:
-
ReentrantLock:- 可重入锁,允许同一线程多次获取同一把锁。
- 提供公平锁和非公平锁两种模式。
-
ReentrantReadWriteLock:- 读写锁,允许多个读线程同时访问,写线程独占访问。
- 适用于读多写少的场景。
-
StampedLock(Java 8引入):
- 提供乐观读锁和悲观读锁,支持更高效的读写操作。
- 适用于高并发读写场景。
-
Semaphore:- 信号量,用于控制同时访问特定资源的线程数量。
-
CountDownLatch:- 允许一个或多个线程等待,直到一组操作完成。
-
CyclicBarrier:- 允许一组线程互相等待,直到达到一个共同的屏障点。
-
LockSupport:- 提供底层的锁和同步机制,支持构建高级同步工具。
- 通过
-
12. 什么是ReentrantLock?它有哪些特点和优势?
回答:
-
ReentrantLock:- 是
java.util.concurrent.locks包下的一个可重入锁实现,提供了比synchronized更高级的锁控制。
- 是
-
特点:
-
可重入性:
- 同一线程可以多次获取同一把锁而不会被阻塞,锁的获取计数会增加,释放时计数减少,直到计数为零锁被释放。
-
公平性:
-
提供公平锁和非公平锁两种模式。公平锁按照线程请求锁的顺序进行获取,非公平锁则允许线程抢占锁。
-
公平锁通过构造器参数设置:
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁 ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁
-
-
可中断的锁获取:
- 线程在等待锁时可以响应中断,通过
lockInterruptibly()方法实现。
try { lock.lockInterruptibly(); // 执行任务 } catch (InterruptedException e) { // 处理中断 } finally { lock.unlock(); } - 线程在等待锁时可以响应中断,通过
-
尝试锁定:
- 线程可以尝试获取锁,如果锁不可用可以选择不等待,通过
tryLock()方法实现。
if (lock.tryLock()) { try { // 执行任务 } finally { lock.unlock(); } } else { // 处理未能获取锁的情况 } - 线程可以尝试获取锁,如果锁不可用可以选择不等待,通过
-
条件变量:
- 提供多个条件变量(
Condition)支持更灵活的线程间通信,类似于Object.wait()和Object.notify()。
Condition condition = lock.newCondition(); - 提供多个条件变量(
-
-
优势:
-
更高的灵活性:
- 提供可中断的锁获取、尝试锁定等功能,满足更复杂的同步需求。
-
公平性选择:
- 允许选择公平锁或非公平锁,适应不同的应用场景。
-
条件变量支持:
- 允许创建多个条件变量,支持更加精细的线程等待和通知机制。
-
性能优化:
- 在高并发场景下,
ReentrantLock可能比synchronized表现出更好的性能和可伸缩性。
- 在高并发场景下,
-
示例:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 确保锁被释放
}
}
public int getCount() {
return count;
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + example.getCount()); // 输出: Final count: 2000
}
}
13. 什么是ReentrantReadWriteLock?它的工作原理是什么?
回答:
-
ReentrantReadWriteLock:- 是
java.util.concurrent.locks包下的一个读写锁实现,允许多个读线程并发访问,或一个写线程独占访问。 - 实现了
ReadWriteLock接口。
- 是
-
工作原理:
-
读锁(Read Lock):
- 多个线程可以同时持有读锁,只要没有线程持有写锁。
- 适用于读多写少的场景,提升并发性能。
-
写锁(Write Lock):
- 只有一个线程可以持有写锁,同时阻塞所有读锁和其他写锁的获取。
- 适用于需要独占访问的场景,确保数据一致性。
-
可重入性:
- 同一线程可以多次获取读锁或写锁,且读锁和写锁之间也是可重入的。
-
锁降级和升级:
- 锁降级:先获取写锁,再获取读锁,最后释放写锁,可以安全地降级为读锁。
- 锁升级:先获取读锁,再尝试获取写锁,可能会导致死锁,因此不推荐。
-
-
组成:
- ReadLock:表示读锁,通过
readLock()方法获取。 - WriteLock:表示写锁,通过
writeLock()方法获取。
- ReadLock:表示读锁,通过
-
示例:
import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteLockExample { private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock(); private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock(); private int count = 0; public void increment() { writeLock.lock(); try { count++; } finally { writeLock.unlock(); } } public int getCount() { readLock.lock(); try { return count; } finally { readLock.unlock(); } } public static void main(String[] args) { ReadWriteLockExample example = new ReadWriteLockExample(); Runnable writer = () -> { for (int i = 0; i < 1000; i++) { example.increment(); } }; Runnable reader = () -> { for (int i = 0; i < 1000; i++) { System.out.println("Count: " + example.getCount()); } }; Thread t1 = new Thread(writer); Thread t2 = new Thread(reader); t1.start(); t2.start(); } }
注意:
- 锁升级可能导致死锁:从读锁尝试获取写锁,如果其他线程持有写锁,会导致死锁。因此,应避免锁升级。
- 锁降级是安全的:从写锁获取读锁后,再释放写锁,可以安全地降级为读锁。
14. 什么是StampedLock?它与ReentrantReadWriteLock有何不同?
回答:
-
StampedLock:- 是Java 8引入的一个锁实现,提供了一种基于乐观锁的读写锁机制。
- 通过标记(stamp)机制管理锁状态,支持更高效的并发读写操作。
- 提供了三种锁模式:写锁、悲观读锁和乐观读锁。
-
与
ReentrantReadWriteLock的区别:-
乐观锁支持:
StampedLock提供了乐观读锁,允许线程在没有实际加锁的情况下读取数据,并通过验证确保数据的一致性。ReentrantReadWriteLock仅提供悲观读锁,读锁期间会阻塞写锁。
-
标记机制:
StampedLock使用标记(stamp)来表示锁的状态,允许更细粒度的锁控制。
-
不可重入:
StampedLock是不支持重入的,线程不能多次获取同一类型的锁。ReentrantReadWriteLock支持重入。
-
性能:
- 在读多写少的场景下,
StampedLock由于乐观读锁的存在,可能比ReentrantReadWriteLock表现更好。
- 在读多写少的场景下,
-
方法复杂度:
StampedLock提供了更多的控制方法,如tryOptimisticRead()、validate()等,使用相对复杂。
-
-
使用示例:
import java.util.concurrent.locks.StampedLock; public class StampedLockExample { private final StampedLock sl = new StampedLock(); private int count = 0; public void increment() { long stamp = sl.writeLock(); try { count++; } finally { sl.unlockWrite(stamp); } } public int getCount() { long stamp = sl.tryOptimisticRead(); int currentCount = count; if (!sl.validate(stamp)) { stamp = sl.readLock(); try { currentCount = count; } finally { sl.unlockRead(stamp); } } return currentCount; } public static void main(String[] args) { StampedLockExample example = new StampedLockExample(); Runnable writer = () -> { for (int i = 0; i < 1000; i++) { example.increment(); } }; Runnable reader = () -> { for (int i = 0; i < 1000; i++) { System.out.println("Count: " + example.getCount()); } }; Thread t1 = new Thread(writer); Thread t2 = new Thread(reader); t1.start(); t2.start(); } }
注意:
- 不可重入:
StampedLock不支持重入,如果需要重入功能,应使用ReentrantReadWriteLock。 - 复杂性:
StampedLock的使用相对复杂,需要谨慎处理标记(stamp)的获取和释放。
15. 什么是Condition?如何在ReentrantLock中使用它?
回答:
-
Condition:- 是
java.util.concurrent.locks包下的一个接口,提供了类似于Object的wait()和notify()方法的功能,但更灵活。 - 允许线程在某些条件下等待,并在条件满足时被唤醒。
- 每个
Lock可以创建多个Condition,提供了更细粒度的线程控制。
- 是
-
在
ReentrantLock中使用Condition:步骤:
-
创建
ReentrantLock和Condition:ReentrantLock lock = new ReentrantLock(); Condition condition = lock.newCondition(); -
线程等待条件:
lock.lock(); try { while (!conditionMet) { condition.await(); // 释放锁并等待 } // 条件满足,继续执行 } catch (InterruptedException e) { // 处理中断 } finally { lock.unlock(); } -
线程通知条件:
lock.lock(); try { // 修改条件状态 conditionMet = true; condition.signal(); // 唤醒一个等待的线程 // 或者 condition.signalAll(); // 唤醒所有等待的线程 } finally { lock.unlock(); }
-
-
示例:生产者-消费者模型
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import java.util.LinkedList; import java.util.Queue; public class ProducerConsumer { private final Queue<Integer> queue = new LinkedList<>(); private final int MAX_SIZE = 5; private final ReentrantLock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); public void produce(int value) throws InterruptedException { lock.lock(); try { while (queue.size() == MAX_SIZE) { notFull.await(); } queue.add(value); notEmpty.signal(); // 通知消费者 } finally { lock.unlock(); } } public int consume() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { notEmpty.await(); } int value = queue.poll(); notFull.signal(); // 通知生产者 return value; } finally { lock.unlock(); } } public static void main(String[] args) { ProducerConsumer pc = new ProducerConsumer(); // 生产者 Runnable producer = () -> { for (int i = 0; i < 10; i++) { try { pc.produce(i); System.out.println("Produced: " + i); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }; // 消费者 Runnable consumer = () -> { for (int i = 0; i < 10; i++) { try { int value = pc.consume(); System.out.println("Consumed: " + value); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }; new Thread(producer).start(); new Thread(consumer).start(); } }
注意:
- 使用
Condition时,必须在获取锁的情况下调用await()和signal()。 await()方法会释放锁,并在条件满足时重新获取锁后继续执行。
四、并发集合
16. Java中有哪些并发集合类?简要介绍它们的用途。
回答:
Java提供了多种线程安全的并发集合类,位于java.util.concurrent包下。这些集合类设计用于在高并发环境下安全、高效地处理数据。
- ConcurrentHashMap:
- 线程安全的哈希表实现,支持高并发的读写操作。
- 不允许
null键和null值。 - 适用于需要频繁读写的键值对存储,如缓存、共享数据存储等。
- CopyOnWriteArrayList:
- 线程安全的
List实现,基于写时复制(Copy-On-Write)策略。 - 适用于读多写少的场景,如事件监听器列表、配置项集合等。
- 写操作会创建数组的副本,读操作无需同步。
- 线程安全的
- CopyOnWriteArraySet:
- 线程安全的
Set实现,基于CopyOnWriteArrayList。 - 适用于读多写少的唯一元素集合。
- 线程安全的
- ConcurrentLinkedQueue:
- 基于链表的非阻塞线程安全队列。
- 适用于需要高并发读写的队列,如任务队列、消息队列等。
- ConcurrentLinkedDeque:
- 基于链表的非阻塞线程安全双端队列。
- 适用于需要在两端插入和删除元素的高并发场景。
- BlockingQueue接口及其实现:
- 提供了阻塞的队列操作,适用于生产者-消费者模型。
- 常见实现类:
- ArrayBlockingQueue:有界阻塞队列,基于数组实现。
- LinkedBlockingQueue:有界或无界阻塞队列,基于链表实现。
- PriorityBlockingQueue:无界优先级阻塞队列,基于优先级堆实现。
- DelayQueue:无界延迟阻塞队列,元素必须实现
Delayed接口。
- ConcurrentSkipListMap 和 ConcurrentSkipListSet:
- 基于跳表实现的线程安全有序映射和集合。
- 适用于需要有序访问和高并发的场景。
- BlockingDeque接口及其实现:
- 提供了阻塞的双端队列操作。
- 常见实现类:
- LinkedBlockingDeque:基于链表的双端阻塞队列。
- ArrayDeque(非阻塞,需外部同步)。
- ConcurrentSkipListSet:
- 基于
ConcurrentSkipListMap实现的线程安全有序集合。 - 适用于需要高并发且有序的唯一元素集合。
- 基于
选择合适的并发集合:
- 根据具体的应用场景和性能需求,选择最适合的并发集合类,以实现高效、线程安全的数据处理。
17. 什么是ConcurrentHashMap?它的内部实现原理是什么?
回答:
-
ConcurrentHashMap:- 是
java.util.concurrent包下的一个线程安全的哈希表实现。 - 提供高并发的读写操作,支持多线程同时访问和修改。
- 不允许
null键和null值。
- 是
-
内部实现原理:
Java 7之前:
- 基于分段锁(Segment Locks)机制。
- 将整个哈希表分为多个段(Segment),每个段是一个独立的哈希表,有自己的锁。
- 读操作无需锁定,写操作只锁定对应的段,提高了并发性能。
Java 8及之后:
-
取消了分段锁,采用更细粒度的锁机制,基于Node的锁定和CAS操作。
-
采用链表和红黑树相结合的数据结构:
- 链表:存储哈希冲突的键值对。
- 红黑树:当链表长度超过一定阈值(默认为8),链表会转换为红黑树,提升查找性能。
-
同步机制:
- 使用
CAS(Compare-And-Swap)操作实现无锁读操作。 - 在写操作时,采用节点级别的锁定,保证高效的并发写入。
- 使用
-
关键特性:
- 高并发性能:读操作基本不需要锁定,写操作只锁定必要的部分。
- 动态扩容:支持动态扩容,随着元素数量的增加自动调整容量。
- 线程安全性:通过内置的同步机制,确保在多线程环境下的数据一致性。
-
示例:
import java.util.concurrent.ConcurrentHashMap; public class ConcurrentHashMapExample { public static void main(String[] args) { ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); map.put("A", 1); map.put("B", 2); map.put("C", 3); // 线程安全的操作 map.forEach(1, (key, value) -> System.out.println(key + ": " + value)); map.computeIfAbsent("D", key -> 4); map.computeIfPresent("A", (key, value) -> value + 10); System.out.println(map); // 输出: {A=11, B=2, C=3, D=4} } }
注意:
- 不允许
null键和值:尝试插入null键或值会抛出NullPointerException。 - 并发级别:Java 8的
ConcurrentHashMap通过无锁读操作和更细粒度的锁定机制,提供了更高的并发性能。
18. 什么是CopyOnWriteArrayList?它适用于哪些场景?
回答:
-
CopyOnWriteArrayList:- 是
java.util.concurrent包下的一个线程安全的List实现,基于写时复制(Copy-On-Write)策略。 - 所有的可变操作(如
add、set、remove等)都会在内部复制一个新的数组,进行修改,然后替换原有的数组。 - 迭代器是基于数组的快照,独立于后续的修改,不会抛出
ConcurrentModificationException。
- 是
-
适用场景:
- 读多写少的场景:适用于读操作频繁,而写操作较少的环境,如事件监听器列表、配置项集合等。
- 线程安全的迭代:在多线程环境下,允许线程安全地遍历集合,而不需要显式同步。
- 不可变视图:提供了一种不可变的视图,适合需要安全共享数据的场景。
-
优点:
- 高效的读操作:由于读操作无需锁定,且迭代器基于快照,读性能高。
- 线程安全:通过写时复制机制,保证线程安全。
- 避免锁竞争:不需要显式的同步机制,避免了锁竞争问题。
-
缺点:
- 高开销的写操作:每次写操作都会复制整个数组,开销较大。
- 内存消耗高:频繁的写操作会导致大量的数组复制,增加内存使用。
- 不适用于写多读少的场景。
-
示例:
import java.util.concurrent.CopyOnWriteArrayList; public class CopyOnWriteArrayListExample { public static void main(String[] args) { CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); list.add("A"); list.add("B"); list.add("C"); // 迭代时可以安全地修改集合 for (String s : list) { System.out.println(s); list.add("D"); // 不会影响当前迭代 } System.out.println(list); // 输出: [A, B, C, D, D, D] } }
注意:
- 适用场景:应根据应用需求选择合适的并发集合,避免在写操作频繁的场景下使用
CopyOnWriteArrayList。 - 内存与性能:在高并发写操作时,
CopyOnWriteArrayList可能导致内存和性能问题。
19. 什么是BlockingQueue?Java中有哪些常见的BlockingQueue实现?
回答:
-
BlockingQueue:- 是
java.util.concurrent包下的一个接口,扩展了Queue接口,支持阻塞的插入和移除操作。 - 适用于生产者-消费者模型,提供线程安全的任务队列。
- 是
-
常见的
BlockingQueue实现:-
ArrayBlockingQueue:- 基于数组实现的有界阻塞队列。
- 需要在创建时指定容量。
- FIFO(先进先出)顺序。
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100); -
LinkedBlockingQueue:- 基于链表实现的可选有界或无界阻塞队列。
- 默认情况下是无界的,但可以通过构造器指定容量。
- FIFO顺序。
BlockingQueue<String> queue = new LinkedBlockingQueue<>(); BlockingQueue<String> boundedQueue = new LinkedBlockingQueue<>(100); -
PriorityBlockingQueue:- 基于优先级堆实现的无界阻塞队列。
- 元素按照自然顺序或指定的比较器排序。
- 不保证FIFO顺序。
BlockingQueue<Integer> priorityQueue = new PriorityBlockingQueue<>(); -
DelayQueue:- 无界阻塞队列,只有到期的元素才能被取出。
- 元素必须实现
Delayed接口。 - 适用于需要延迟处理的任务,如定时任务。
BlockingQueue<DelayedTask> delayQueue = new DelayQueue<>(); -
SynchronousQueue:- 无缓冲的阻塞队列,每个插入操作必须等待对应的移除操作,反之亦然。
- 适用于需要直接传递任务的场景,如任务提交到线程池。
BlockingQueue<Runnable> syncQueue = new SynchronousQueue<>(); -
LinkedTransferQueue:- 基于链表实现的无界阻塞队列,支持直接传输元素给消费者。
- 提供了
transfer方法,允许生产者等待消费者接收元素。
BlockingQueue<String> transferQueue = new LinkedTransferQueue<>();
-
选择合适的BlockingQueue:
- 有界 vs 无界:根据应用场景选择有界队列以限制资源消耗,或无界队列以避免任务丢失。
- FIFO vs 优先级:选择FIFO队列进行任务的顺序执行,或优先级队列根据任务的优先级执行。
- 特殊需求:如需要延迟执行的任务,选择
DelayQueue;需要直接传递任务,选择SynchronousQueue。
20. 什么是ConcurrentLinkedQueue?它的适用场景是什么?
回答:
-
ConcurrentLinkedQueue:- 是
java.util.concurrent包下的一个基于链表的非阻塞线程安全队列。 - 实现了
Queue接口,支持高并发的无锁操作。 - 采用了乐观锁策略,使用
CAS(Compare-And-Swap)操作来保证线程安全。 - 无界队列,理论上可以容纳任意数量的元素(受限于系统内存)。
- 是
-
适用场景:
- 高并发环境:适用于多个线程同时进行入队和出队操作的场景,如任务调度、消息传递等。
- 非阻塞需求:不需要阻塞等待队列状态变化,适用于快速处理任务的场景。
- 轻量级应用:由于没有阻塞机制,适用于对性能要求较高、任务处理快速的应用。
-
关键特性:
- 线程安全:通过无锁算法实现高并发读写操作。
- 高性能:在高并发场景下表现优异,避免了锁竞争带来的性能瓶颈。
- 无阻塞:入队和出队操作不会阻塞线程,适合快速处理任务。
-
示例:
import java.util.concurrent.ConcurrentLinkedQueue; public class ConcurrentLinkedQueueExample { public static void main(String[] args) { ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>(); queue.add("A"); queue.add("B"); queue.add("C"); // 多线程访问 Runnable producer = () -> { for (int i = 0; i < 5; i++) { queue.add("P" + i); System.out.println("Produced: P" + i); } }; Runnable consumer = () -> { for (int i = 0; i < 5; i++) { String value = queue.poll(); if (value != null) { System.out.println("Consumed: " + value); } } }; Thread t1 = new Thread(producer); Thread t2 = new Thread(consumer); t1.start(); t2.start(); } }
注意:
- 元素顺序:
ConcurrentLinkedQueue遵循FIFO顺序。 - 不支持阻塞操作:如果需要阻塞等待元素,使用
BlockingQueue的实现类,如ConcurrentLinkedDeque、LinkedBlockingQueue等。
五、原子变量
21. 什么是原子变量(Atomic Variables)?它们的作用是什么?
回答:
-
原子变量(Atomic Variables):
- 是
java.util.concurrent.atomic包下的一组类,提供了对单个变量的原子操作,支持无锁的线程安全编程。 - 通过硬件级别的原子指令(如CAS操作)实现高效的并发操作。
- 是
-
作用:
- 无锁编程:避免使用传统的锁机制,减少锁竞争和上下文切换开销,提升性能。
- 简化同步:提供了一种更简单、直观的方式来实现线程安全的变量操作。
- 原子性保证:确保对变量的操作是不可分割的,防止数据竞争和不一致。
-
常见的原子变量类:
AtomicInteger、AtomicLong、AtomicBoolean:支持基本数据类型的原子操作。AtomicReference<V>、AtomicStampedReference<V>:支持引用类型的原子操作。AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray<V>:支持数组类型的原子操作。
-
示例:
import java.util.concurrent.atomic.AtomicInteger; public class AtomicIntegerExample { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); // 原子递增 } public int getCount() { return count.get(); } public static void main(String[] args) { AtomicIntegerExample example = new AtomicIntegerExample(); Runnable task = () -> { for (int i = 0; i < 1000; i++) { example.increment(); } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Final count: " + example.getCount()); // 输出: Final count: 2000 } }
注意:
- CAS操作:原子变量类主要依赖于CAS(Compare-And-Swap)操作实现原子性。
- ABA问题:在某些情况下,CAS操作可能会遇到ABA问题,
AtomicStampedReference等类提供了解决方案。 - 与锁的选择:原子变量适用于简单的原子操作,复杂的同步需求仍需使用锁机制。
22. 解释AtomicInteger的常用方法,如incrementAndGet、compareAndSet。
回答:
-
AtomicInteger:- 是
java.util.concurrent.atomic包下的一个类,提供对int类型的原子操作。
- 是
-
常用方法:
-
incrementAndGet():- 原子地将当前值加1,并返回更新后的值。
- 等价于
addAndGet(1)。
AtomicInteger atomicInt = new AtomicInteger(0); int newValue = atomicInt.incrementAndGet(); // newValue = 1 -
decrementAndGet():- 原子地将当前值减1,并返回更新后的值。
int newValue = atomicInt.decrementAndGet(); // newValue = 0 -
getAndIncrement():- 原子地将当前值加1,但返回的是加1前的值。
int oldValue = atomicInt.getAndIncrement(); // oldValue = 0, atomicInt = 1 -
getAndDecrement():- 原子地将当前值减1,但返回的是减1前的值。
int oldValue = atomicInt.getAndDecrement(); // oldValue = 1, atomicInt = 0 -
compareAndSet(int expect, int update):- 如果当前值等于
expect,则原子地将其设置为update,并返回true。 - 否则,不做任何操作,返回
false。
boolean success = atomicInt.compareAndSet(0, 100); // success = true, atomicInt = 100 - 如果当前值等于
-
get():- 返回当前的值。
int currentValue = atomicInt.get(); // currentValue = 100 -
set(int newValue):- 设置为新值。
atomicInt.set(200); // atomicInt = 200 -
addAndGet(int delta):- 原子地将当前值加上
delta,并返回更新后的值。
int newValue = atomicInt.addAndGet(50); // newValue = 250 - 原子地将当前值加上
-
getAndAdd(int delta):- 原子地将当前值加上
delta,但返回的是加上delta前的值。
int oldValue = atomicInt.getAndAdd(50); // oldValue = 250, atomicInt = 300 - 原子地将当前值加上
-
-
使用场景:
- 实现高效的计数器。
- 管理并发环境下的共享状态。
- 无需使用锁的简单同步场景。
示例:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerDemo {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
int newCount = count.incrementAndGet();
System.out.println("Incremented count: " + newCount);
}
public void conditionalUpdate(int expected, int newValue) {
boolean updated = count.compareAndSet(expected, newValue);
System.out.println("Conditional update from " + expected + " to " + newValue + ": " + updated);
}
public static void main(String[] args) {
AtomicIntegerDemo demo = new AtomicIntegerDemo();
demo.increment(); // Incremented count: 1
demo.conditionalUpdate(1, 10); // Conditional update from 1 to 10: true
demo.conditionalUpdate(5, 20); // Conditional update from 5 to 20: false
}
}
输出:
Incremented count: 1
Conditional update from 1 to 10: true
Conditional update from 5 to 20: false
23. 什么是AtomicReference?它的应用场景是什么?
回答:
-
AtomicReference<V>:- 是
java.util.concurrent.atomic包下的一个类,提供了对引用类型的原子操作。 - 支持原子地获取、设置和更新对象引用。
- 是
-
应用场景:
-
非原子类型的原子操作:
- 当需要对对象引用进行原子更新时,使用
AtomicReference可以避免使用显式的同步。
- 当需要对对象引用进行原子更新时,使用
-
实现无锁算法:
- 在构建复杂的并发数据结构或算法时,使用
AtomicReference可以实现无锁的线程安全操作。
- 在构建复杂的并发数据结构或算法时,使用
-
状态管理:
- 管理共享对象的状态变化,如状态机、配置对象的动态更新等。
-
引用更新:
- 在多线程环境下,安全地更新共享的对象引用。
-
-
常用方法:
-
get():- 返回当前的引用。
AtomicReference<String> atomicRef = new AtomicReference<>("Initial"); String value = atomicRef.get(); // "Initial" -
set(V newValue):- 设置为新引用。
atomicRef.set("Updated"); -
compareAndSet(V expect, V update):- 如果当前引用等于
expect,则原子地将其设置为update,并返回true。
boolean success = atomicRef.compareAndSet("Updated", "Final"); - 如果当前引用等于
-
getAndSet(V newValue):- 原子地设置为新引用,并返回旧引用。
String oldValue = atomicRef.getAndSet("New Value"); -
updateAndGet和getAndUpdate:- 通过函数更新引用。
atomicRef.updateAndGet(s -> s + "!");
-
-
示例:
import java.util.concurrent.atomic.AtomicReference; public class AtomicReferenceExample { private AtomicReference<String> atomicString = new AtomicReference<>("Hello"); public void updateString(String newValue) { boolean updated = atomicString.compareAndSet("Hello", newValue); if (updated) { System.out.println("String updated to: " + newValue); } else { System.out.println("Update failed."); } } public String getString() { return atomicString.get(); } public static void main(String[] args) { AtomicReferenceExample example = new AtomicReferenceExample(); example.updateString("World"); // String updated to: World example.updateString("Java"); // Update failed. System.out.println(example.getString()); // World } }
注意:
- CAS操作的ABA问题:在某些场景下,
AtomicReference的CAS操作可能会遇到ABA问题,即对象从A变为B再变回A,导致CAS成功但实际状态已变化。可以使用AtomicStampedReference来解决。
六、Fork/Join框架
24. 什么是Fork/Join框架?它的主要组件是什么?
回答:
-
Fork/Join框架:
- 是Java 7引入的一个框架,旨在利用多核处理器的优势,通过任务分解(Fork)和结果合并(Join)来并行处理大任务。
- 适用于可以递归分解为更小任务的计算密集型任务,如排序、矩阵运算等。
-
主要组件:
-
ForkJoinPool:- 是
java.util.concurrent包下的一个特殊线程池,专门用于执行ForkJoinTask。 - 使用工作窃取(Work Stealing)算法,允许空闲线程从繁忙线程的任务队列中窃取任务,提高资源利用率。
- 是
-
ForkJoinTask<V>:- 是所有Fork/Join任务的基类,定义了任务的基本操作。
- 两个主要的子类:
RecursiveAction:不返回结果的任务。RecursiveTask<V>:返回结果的任务。
-
工作窃取算法(Work Stealing):
- 当一个线程的任务队列为空时,它可以从其他线程的队列中窃取任务,保持线程池的活跃性和高效性。
-
-
工作流程:
- 将大任务分解为小任务(Fork)。
- 小任务被提交到
ForkJoinPool的工作线程执行。 - 等待所有小任务完成(Join)。
- 合并小任务的结果,得到最终结果。
-
示例:
import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveTask; public class ForkJoinExample { // 计算数组元素的和 static class SumTask extends RecursiveTask<Long> { private static final int THRESHOLD = 1000; private final int[] array; private final int start; private final int end; public SumTask(int[] array, int start, int end) { this.array = array; this.start = start; this.end = end; } @Override protected Long compute() { if (end - start <= THRESHOLD) { long sum = 0; for (int i = start; i < end; i++) { sum += array[i]; } return sum; } else { int mid = (start + end) / 2; SumTask leftTask = new SumTask(array, start, mid); SumTask rightTask = new SumTask(array, mid, end); leftTask.fork(); // 异步执行左半部分 long rightResult = rightTask.compute(); // 同步执行右半部分 long leftResult = leftTask.join(); // 等待左半部分结果 return leftResult + rightResult; } } } public static void main(String[] args) { int[] array = new int[10000]; for (int i = 0; i < array.length; i++) { array[i] = i + 1; } ForkJoinPool pool = new ForkJoinPool(); SumTask task = new SumTask(array, 0, array.length); long result = pool.invoke(task); System.out.println("Sum: " + result); // 输出: Sum: 50005000 } }
注意:
- Fork/Join框架适用于计算密集型任务,不适用于IO密集型任务。
- 合理划分任务的粒度,避免任务过小导致过多的任务调度开销,或任务过大导致并行度不足。
25. Fork/Join框架的工作窃取(Work Stealing)算法是如何实现的?
回答:
-
工作窃取(Work Stealing)算法:
- 是Fork/Join框架中提高并发性能和资源利用率的关键机制。
- 主要思想是让空闲的线程主动从其他繁忙线程的任务队列中“窃取”任务,以保持线程池中所有线程的活跃性。
-
实现原理:
-
双端队列(Deque):
- 每个工作线程拥有一个双端队列,用于存储其任务。
- 线程以LIFO(后进先出)的方式从队列的尾部提交和执行任务。
-
任务提交和执行:
- 当一个线程创建子任务时,会将子任务“fork”到其自己的任务队列的尾部。
- 线程会优先从自己的队列的尾部获取任务执行。
-
任务窃取:
- 当一个线程的任务队列为空时,它会尝试从其他线程的队列的头部窃取任务。
- 窃取的任务是其他线程最早提交的任务,有助于保持任务的分布均匀。
-
避免竞争:
- 由于一个线程只从自己队列的尾部提交和获取任务,而窃取线程从其他线程的头部窃取任务,减少了并发访问同一队列的冲突。
- 使用无锁的算法和
CAS操作实现高效的队列操作。
-
-
优势:
- 高效的任务分配:通过工作窃取,确保所有线程都能持续工作,避免空闲。
- 负载均衡:自动分配任务到空闲线程,平衡各线程的负载。
- 提高资源利用率:最大化利用多核处理器的并行能力。
-
示例:
- 在前述的
ForkJoinExample中,ForkJoinPool内部采用了工作窃取算法,自动管理任务的分配和执行。
- 在前述的
注意:
- 任务粒度:合理划分任务的粒度,避免任务过大或过小影响工作窃取效率。
- 避免死锁:确保任务的分解和合并不会导致线程间的死锁。
七、Java 8新特性与并发
26. 什么是CompletableFuture?它如何改进了异步编程?
回答:
-
CompletableFuture:- 是Java 8引入的
java.util.concurrent包下的一个类,代表了一个可以在未来完成的异步计算。 - 提供了丰富的API来处理异步操作的结果,如回调、组合、异常处理等。
- 实现了
CompletionStage接口,支持链式调用和函数式编程。
- 是Java 8引入的
-
改进点:
-
灵活的回调机制:
- 提供了多种回调方法,如
thenApply、thenAccept、thenRun等,简化了异步操作的处理。
- 提供了多种回调方法,如
-
任务组合:
- 支持任务的组合执行,如
thenCombine、thenCompose、allOf、anyOf等,方便构建复杂的异步流程。
- 支持任务的组合执行,如
-
异常处理:
- 提供了
exceptionally、handle等方法,简化了异步任务中的异常处理。
- 提供了
-
非阻塞:
- 通过异步执行,不阻塞主线程,提高资源利用率。
-
与Stream API集成:
- 可以与Java 8的Stream API结合,构建高效的并行和异步处理流程。
-
-
示例:
import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; public class CompletableFutureExample { public static void main(String[] args) throws InterruptedException, ExecutionException { // 创建一个异步任务 CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return "Hello"; }); // 处理异步任务的结果 CompletableFuture<String> greetingFuture = future.thenApply(name -> name + ", World!"); // 打印结果 System.out.println(greetingFuture.get()); // 输出: Hello, World! } }
注意:
- 线程池选择:
CompletableFuture默认使用ForkJoinPool.commonPool(),可通过CompletableFuture.supplyAsync的第二个参数指定自定义线程池。 - 异常处理:需要正确处理异步任务中的异常,避免未捕获的异常影响程序。
27. Stream API如何与并发编程结合使用?请举例说明。
回答:
-
Stream API与并发编程的结合:- Java 8的
Stream API提供了对集合数据的高效处理方式,包括串行流和并行流。 - 并行流(Parallel Streams):通过利用多核处理器,实现数据的并行处理,提升性能。
- 内部通过
ForkJoinPool执行任务,自动分解和合并任务。
- Java 8的
-
使用并行流的示例:
import java.util.Arrays; import java.util.List; public class ParallelStreamExample { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 串行流 long startTime = System.currentTimeMillis(); long sumSerial = numbers.stream() .mapToLong(Integer::longValue) .sum(); long endTime = System.currentTimeMillis(); System.out.println("Serial Sum: " + sumSerial + ", Time: " + (endTime - startTime) + "ms"); // 并行流 startTime = System.currentTimeMillis(); long sumParallel = numbers.parallelStream() .mapToLong(Integer::longValue) .sum(); endTime = System.currentTimeMillis(); System.out.println("Parallel Sum: " + sumParallel + ", Time: " + (endTime - startTime) + "ms"); } } -
示例说明:
- 串行流:按顺序处理元素,适用于小规模数据或不需要并行的场景。
- 并行流:自动将数据分解为多个子任务,利用多线程并行处理,适用于大规模数据和计算密集型任务。
-
注意事项:
-
线程安全:
- 确保在并行流中操作的数据结构是线程安全的,或避免共享可变状态。
-
性能评估:
- 并行流并不总是比串行流快,取决于数据规模、任务复杂度和硬件资源。
-
可分性:
- 数据处理任务应具备良好的可分性,才能充分利用并行流的优势。
-
线程池限制:
- 并行流默认使用
ForkJoinPool.commonPool(),在某些场景下可能需要自定义线程池。
- 并行流默认使用
-
-
高级示例:并行流结合自定义线程池
import java.util.Arrays; import java.util.List; import java.util.concurrent.ForkJoinPool; public class CustomThreadPoolParallelStream { public static void main(String[] args) throws InterruptedException { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); ForkJoinPool customThreadPool = new ForkJoinPool(4); // 创建自定义线程池 try { customThreadPool.submit(() -> { long sum = numbers.parallelStream() .mapToLong(Integer::longValue) .sum(); System.out.println("Parallel Sum with Custom ThreadPool: " + sum); }).get(); } catch (Exception e) { e.printStackTrace(); } finally { customThreadPool.shutdown(); } } }
总结:Stream API通过并行流提供了简洁、高效的并行数据处理方式,但需要注意线程安全和性能评估,合理选择使用场景。
28. 什么是Phaser?它与CountDownLatch和CyclicBarrier有什么区别?
回答:
-
Phaser:- 是
java.util.concurrent包下的一个同步工具类,支持可变数量的参与者。 - 提供比
CountDownLatch和CyclicBarrier更灵活的同步机制,适用于多阶段的同步控制。 - 允许动态注册和注销参与者,适应参与者数量的变化。
- 是
-
与
CountDownLatch和CyclicBarrier的区别:-
CountDownLatch:- 用于等待一组线程完成操作。
- 计数器只允许减到零一次,不能重用。
- 参与者数量固定,不能动态变化。
CountDownLatch latch = new CountDownLatch(3); -
CyclicBarrier:- 用于让一组线程互相等待,直到所有线程都达到某个共同点。
- 计数器可以重用,适用于多个周期性的同步点。
- 参与者数量固定。
CyclicBarrier barrier = new CyclicBarrier(3); -
Phaser:- 综合了
CountDownLatch和CyclicBarrier的特性,支持多个阶段的同步。 - 允许动态注册和注销参与者,参与者数量可以在运行时变化。
- 支持灵活的阶段控制,如分阶段的任务执行。
Phaser phaser = new Phaser(1); // 初始注册1个参与者 - 综合了
-
-
使用场景:
CountDownLatch:等待固定数量的线程完成初始化或某个任务。CyclicBarrier:在多个线程之间同步执行多个阶段的任务。Phaser:在复杂的多阶段、多动态参与者的同步场景下使用。
-
示例:
Phaser的基本使用:import java.util.concurrent.Phaser; public class PhaserExample { public static void main(String[] args) { Phaser phaser = new Phaser(1); // 主线程注册 for (int i = 0; i < 3; i++) { final int taskId = i; phaser.register(); // 动态注册参与者 new Thread(() -> { System.out.println("Task " + taskId + " is running."); phaser.arriveAndAwaitAdvance(); // 到达并等待其他参与者 System.out.println("Task " + taskId + " is completed."); phaser.arriveAndDeregister(); // 到达并注销 }).start(); } phaser.arriveAndAwaitAdvance(); // 主线程等待所有参与者到达 System.out.println("All tasks are completed."); phaser.arriveAndDeregister(); // 主线程注销 } }
输出:
Task 0 is running.
Task 1 is running.
Task 2 is running.
All tasks are completed.
Task 0 is completed.
Task 1 is completed.
Task 2 is completed.
总结:Phaser提供了灵活的多阶段同步机制,适用于复杂的并发任务控制,相比于CountDownLatch和CyclicBarrier更具扩展性和灵活性。
八、阻塞队列与生产者-消费者
29. 解释生产者-消费者模型,并说明如何使用BlockingQueue实现它。
回答:
-
生产者-消费者模型:
- 是一种经典的多线程设计模式,涉及两个角色:生产者和消费者。
- 生产者负责生成数据或任务,并将其放入共享的缓冲区(队列)。
- 消费者从缓冲区中取出数据或任务,并进行处理。
- 目标:解耦生产者和消费者,使其能够独立运行,且在速度不匹配时保持系统稳定。
-
使用
BlockingQueue实现生产者-消费者模型:步骤:
- 选择合适的
BlockingQueue实现:如ArrayBlockingQueue、LinkedBlockingQueue等,根据需求选择有界或无界队列。 - 创建生产者和消费者线程:
- 生产者:生成任务,调用
put()方法将任务放入队列。 - 消费者:调用
take()方法从队列中取出任务并处理。
- 生产者:生成任务,调用
- 启动线程:
- 启动多个生产者和消费者线程,实现并发生产和消费。
- 选择合适的
-
示例:
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class ProducerConsumerDemo { public static void main(String[] args) { BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5); // 有界队列 // 生产者 Runnable producer = () -> { for (int i = 0; i < 10; i++) { try { queue.put(i); System.out.println("Produced: " + i); Thread.sleep(100); // 模拟生产时间 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }; // 消费者 Runnable consumer = () -> { for (int i = 0; i < 10; i++) { try { int value = queue.take(); System.out.println("Consumed: " + value); Thread.sleep(150); // 模拟消费时间 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }; new Thread(producer).start(); new Thread(consumer).start(); } }
输出:
Produced: 0
Consumed: 0
Produced: 1
Produced: 2
Consumed: 1
Produced: 3
Produced: 4
Consumed: 2
Produced: 5
Consumed: 3
Produced: 6
Produced: 7
Consumed: 4
Produced: 8
Consumed: 5
Produced: 9
Consumed: 6
Consumed: 7
Consumed: 8
Consumed: 9
注意:
- 队列容量:有界队列可以防止生产者过快生成任务导致内存溢出;无界队列可以防止生产者阻塞,但可能导致内存占用过高。
- 线程同步:
BlockingQueue内部已实现线程同步,生产者和消费者无需显式同步。
30. 什么是LinkedBlockingQueue和ArrayBlockingQueue?它们的区别是什么?
回答:
-
LinkedBlockingQueue:- 基于链表实现的阻塞队列。
- 可以是有界队列或无界队列。
- 默认容量为
Integer.MAX_VALUE(无界)。 - 适用于生产者和消费者速度不匹配且需要高吞吐量的场景。
特点:
- 支持公平性,可以设置为FIFO顺序。
- 内部通过两个
ReentrantLock锁控制put和take操作,提高并发性能。
创建方式:
// 无界 BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(); // 有界 BlockingQueue<Integer> boundedQueue = new LinkedBlockingQueue<>(100); -
ArrayBlockingQueue:- 基于数组实现的有界阻塞队列。
- 需要在创建时指定固定的容量。
- 适用于需要严格控制队列大小的场景,如限流、资源控制等。
特点:
- FIFO顺序。
- 内部使用一个
ReentrantLock锁控制所有操作,简单但锁竞争可能更高。
创建方式:
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(100); -
主要区别:
特性 LinkedBlockingQueueArrayBlockingQueue内部结构 链表 数组 队列容量 可有界或无界(默认无界) 有界,必须在创建时指定容量 并发性能 双锁机制,读写锁分离,较高的并发性能 单锁机制,锁竞争可能更高 是否允许 null元素不允许 不允许 FIFO保证 是 是 公平性 可配置(默认非公平) 可配置(默认非公平) 适用场景 生产者和消费者速度不匹配,高吞吐量场景 需要严格控制队列大小,限流、资源控制等场景
选择建议:
LinkedBlockingQueue:适用于生产者和消费者速度不匹配、需要高吞吐量的场景。ArrayBlockingQueue:适用于需要严格控制队列容量、限流和资源控制的场景。
九、同步工具类
31. 解释CountDownLatch及其使用场景。
回答:
-
CountDownLatch:- 是
java.util.concurrent包下的一个同步工具类,允许一个或多个线程等待,直到其他线程完成一组操作。 - 通过一个计数器控制,计数器的初始值由创建时指定,每调用一次
countDown()方法,计数器减1。 - 当计数器达到零时,等待的线程被唤醒。
- 是
-
使用场景:
-
等待多个线程完成初始化:
- 主线程等待多个子线程完成初始化工作后继续执行。
-
并行测试:
- 在测试中,等待多个测试步骤完成后进行断言或结果验证。
-
任务同步:
- 多个任务完成后,触发后续的操作。
-
-
特点:
- 一次性使用:
CountDownLatch不能重用,计数器只能从初始值减到零一次。 - 不可中断:等待线程可以被中断,抛出
InterruptedException。
- 一次性使用:
-
示例:等待多个线程完成任务后继续执行
import java.util.concurrent.CountDownLatch; public class CountDownLatchExample { public static void main(String[] args) throws InterruptedException { int numberOfWorkers = 3; CountDownLatch latch = new CountDownLatch(numberOfWorkers); // 创建并启动工作线程 for (int i = 0; i < numberOfWorkers; i++) { final int workerId = i; new Thread(() -> { System.out.println("Worker " + workerId + " started."); try { Thread.sleep(1000 + workerId * 500); // 模拟工作 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Worker " + workerId + " finished."); latch.countDown(); // 完成任务,计数器减1 }).start(); } // 主线程等待所有工作线程完成 System.out.println("Main thread waiting for workers to finish."); latch.await(); System.out.println("All workers finished. Main thread proceeding."); } }
输出:
Worker 0 started.
Worker 1 started.
Worker 2 started.
Main thread waiting for workers to finish.
Worker 0 finished.
Worker 1 finished.
Worker 2 finished.
All workers finished. Main thread proceeding.
注意:
- 一次性使用:如果需要重复使用同步工具,考虑使用
CyclicBarrier或Phaser。 - 防止线程泄漏:确保所有工作线程最终都会调用
countDown(),否则主线程会一直等待。
32. 什么是CyclicBarrier?它如何与CountDownLatch不同?
回答:
-
CyclicBarrier:- 是
java.util.concurrent包下的一个同步工具类,允许一组线程互相等待,直到所有线程都达到某个共同的屏障点。 - 通过一个计数器控制,计数器的初始值由创建时指定,每个线程调用
await()方法后,计数器减1。 - 当计数器达到零时,所有等待的线程被同时唤醒,并可以继续执行。
- 计数器可以重用,因此
CyclicBarrier是可循环使用的。
- 是
-
与
CountDownLatch的区别:特性 CountDownLatchCyclicBarrier重用性 不能重用,一旦计数器到零后不可再次使用 可重用,多次使用同一个屏障点 参与者注册 需要在创建时指定固定数量,不能动态变化 可以动态注册和注销参与者(Java 9及以上) 额外动作 不支持,当计数器到零后无额外动作 支持,当所有线程到达屏障点后可以执行一个回调 使用场景 等待固定数量的线程完成某个操作 同步多个线程在某个屏障点 -
使用场景:
-
多阶段任务同步:
- 一组线程需要在多个阶段同步执行,如分阶段计算、分阶段汇总等。
-
集体行动:
- 需要一组线程在共同的屏障点等待,协调集体行动的场景。
-
测试同步:
- 测试中,模拟多个线程同时到达某个点。
-
-
示例:
import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; public class CyclicBarrierExample { public static void main(String[] args) { int numberOfParties = 3; CyclicBarrier barrier = new CyclicBarrier(numberOfParties, () -> { System.out.println("All parties have arrived at the barrier. Proceeding..."); }); Runnable task = () -> { try { String threadName = Thread.currentThread().getName(); System.out.println(threadName + " is performing some work."); Thread.sleep((long) (Math.random() * 1000)); System.out.println(threadName + " has arrived at the barrier."); barrier.await(); // 等待其他线程 System.out.println(threadName + " is proceeding after the barrier."); } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } }; // 启动线程 for (int i = 0; i < numberOfParties; i++) { new Thread(task, "Thread-" + i).start(); } // 重用屏障 new Thread(task, "Thread-3").start(); new Thread(task, "Thread-4").start(); new Thread(task, "Thread-5").start(); } }
输出:
Thread-0 is performing some work.
Thread-1 is performing some work.
Thread-2 is performing some work.
Thread-1 has arrived at the barrier.
Thread-0 has arrived at the barrier.
Thread-2 has arrived at the barrier.
All parties have arrived at the barrier. Proceeding...
Thread-1 is proceeding after the barrier.
Thread-0 is proceeding after the barrier.
Thread-2 is proceeding after the barrier.
Thread-3 is performing some work.
Thread-4 is performing some work.
Thread-5 is performing some work.
Thread-3 has arrived at the barrier.
Thread-4 has arrived at the barrier.
Thread-5 has arrived at the barrier.
All parties have arrived at the barrier. Proceeding...
Thread-3 is proceeding after the barrier.
Thread-5 is proceeding after the barrier.
Thread-4 is proceeding after the barrier.
注意:
- 异常处理:在使用
CyclicBarrier时,需处理BrokenBarrierException和InterruptedException。 - 可重用性:在屏障被释放后,可以再次使用同一个
CyclicBarrier进行同步。
33. 什么是Semaphore?它的常见应用场景是什么?
回答:
-
Semaphore:- 是
java.util.concurrent包下的一个同步工具类,基于信号量的概念。 - 控制同时访问特定资源的线程数量。
- 信号量维护一个计数器,表示可用的许可证数量。
- 是
-
工作原理:
-
许可获取:
- 线程调用
acquire()方法获取一个许可证,如果许可证数量为零,线程会被阻塞,直到有许可证可用。
- 线程调用
-
许可释放:
- 线程调用
release()方法释放一个许可证,唤醒被阻塞的线程。
- 线程调用
-
计数器:
- 初始许可数量由构造方法指定。
Semaphore可以是公平的(按照线程请求许可证的顺序)或非公平的(允许线程抢占许可证)。
-
-
应用场景:
-
资源限制:
- 控制对有限资源(如数据库连接、文件句柄)的并发访问数量。
-
流量控制:
- 限制系统的并发请求量,防止过载。
-
信号控制:
- 实现线程间的信号传递,如事件触发、同步操作等。
-
实现生产者-消费者模式:
- 控制生产者和消费者的并发关系,确保数据一致性。
-
-
示例:
import java.util.concurrent.Semaphore; public class SemaphoreExample { public static void main(String[] args) { // 创建一个Semaphore,最多允许3个线程同时访问 Semaphore semaphore = new Semaphore(3); Runnable task = () -> { String threadName = Thread.currentThread().getName(); try { System.out.println(threadName + " is attempting to acquire a permit."); semaphore.acquire(); // 获取许可证 System.out.println(threadName + " has acquired a permit."); Thread.sleep(2000); // 模拟工作 System.out.println(threadName + " is releasing a permit."); semaphore.release(); // 释放许可证 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }; // 启动5个线程 for (int i = 0; i < 5; i++) { new Thread(task, "Thread-" + i).start(); } } }
输出:
Thread-0 is attempting to acquire a permit.
Thread-0 has acquired a permit.
Thread-1 is attempting to acquire a permit.
Thread-1 has acquired a permit.
Thread-2 is attempting to acquire a permit.
Thread-2 has acquired a permit.
Thread-3 is attempting to acquire a permit.
Thread-4 is attempting to acquire a permit.
Thread-0 is releasing a permit.
Thread-3 has acquired a permit.
Thread-1 is releasing a permit.
Thread-4 has acquired a permit.
Thread-2 is releasing a permit.
Thread-3 is releasing a permit.
Thread-4 is releasing a permit.
注意:
-
公平性:
Semaphore可以通过构造方法设置为公平模式(按照线程请求顺序获取许可证)。Semaphore fairSemaphore = new Semaphore(3, true); -
许可证管理:确保每次
acquire()对应一次release(),避免许可证泄漏。
十、线程安全与同步
34. 什么是volatile关键字?它如何确保变量的可见性?
回答:
-
volatile关键字:- 是Java中的一个修饰符,用于声明变量的可见性和禁止指令重排序。
- 适用于变量在多线程环境下被多个线程读取和写入,但不涉及复合操作(如自增)。
-
确保可见性:
- 内存可见性:
volatile变量的写操作会立即刷新到主内存,读操作会从主内存读取最新的值。 - 禁止指令重排序:编译器和处理器不会对
volatile变量的读写操作进行重排序,保证程序执行的顺序性。
- 内存可见性:
-
适用场景:
- 标记位(flag),用于控制线程的停止或启动。
- 单一读写操作的共享变量,不涉及复合操作。
-
示例:
public class VolatileExample { private volatile boolean flag = false; public void setFlag() { flag = true; } public void checkFlag() { while (!flag) { // 等待flag被设置为true } System.out.println("Flag is true, proceeding..."); } public static void main(String[] args) { VolatileExample example = new VolatileExample(); // 线程A:检查flag new Thread(() -> example.checkFlag(), "Thread-A").start(); // 线程B:设置flag new Thread(() -> { try { Thread.sleep(1000); // 模拟工作 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } example.setFlag(); System.out.println("Flag has been set to true."); }, "Thread-B").start(); } }
输出:
Flag has been set to true.
Flag is true, proceeding...
注意:
- 不适用于复合操作:
volatile不能保证复合操作(如count++)的原子性,需结合原子变量或锁机制。 - 性能影响:
volatile会引入一定的内存屏障,可能影响性能,应合理使用。
35. 解释乐观锁和悲观锁的区别。Java中如何实现乐观锁?
回答:
-
乐观锁(Optimistic Lock):
- 假设多个线程不会同时修改同一资源,允许并发访问。
- 在提交修改时,检查是否有其他线程已修改资源,如果有,则回滚或重试。
- 不阻塞线程,适用于读多写少的场景。
- 常用实现方式:基于版本号的CAS(Compare-And-Swap)操作。
-
悲观锁(Pessimistic Lock):
- 假设多个线程可能会同时修改同一资源,阻塞其他线程的访问。
- 通过加锁机制,确保同一时间只有一个线程能修改资源。
- 适用于写操作频繁或竞争激烈的场景。
- 常用实现方式:
synchronized、ReentrantLock等显式锁。
-
区别:
特性 乐观锁 悲观锁 假设 并发冲突较少,允许并发访问 并发冲突较多,阻塞其他线程访问 性能 在读多写少场景下性能较高 在高竞争场景下性能较低,存在锁竞争 实现方式 CAS操作,基于版本号检查 显式加锁( synchronized、ReentrantLock)复杂性 需要版本管理,处理回滚或重试逻辑 相对简单,通过锁机制控制访问 -
Java中实现乐观锁:
使用
AtomicReference实现简单的乐观锁:import java.util.concurrent.atomic.AtomicReference; public class OptimisticLockExample { static class SharedResource { private final AtomicReference<Integer> value = new AtomicReference<>(0); public void increment() { while (true) { Integer current = value.get(); Integer newValue = current + 1; if (value.compareAndSet(current, newValue)) { break; } // CAS失败,重试 } } public int getValue() { return value.get(); } } public static void main(String[] args) throws InterruptedException { SharedResource resource = new SharedResource(); Runnable task = () -> { for (int i = 0; i < 1000; i++) { resource.increment(); } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("Final value: " + resource.getValue()); // 输出: Final value: 2000 } }
注意:
- 适用场景:乐观锁适用于读多写少、冲突概率低的场景。
- 复杂性:实现乐观锁需要管理版本号和处理冲突回滚,逻辑相对复杂。
36. 如何避免死锁?请举例说明。
回答:
-
死锁:
- 是指两个或多个线程在执行过程中,因为争夺资源而造成互相等待,导致程序无法继续执行。
-
避免死锁的方法:
-
避免嵌套锁:
- 尽量减少持有多个锁的情况,避免线程在持有一个锁的同时申请另一个锁。
-
锁的顺序:
- 所有线程获取多个锁时,遵循相同的锁获取顺序,避免循环等待。
-
使用定时锁:
- 使用带有超时机制的锁获取方法,如
tryLock(long time, TimeUnit unit),避免无限等待。
- 使用带有超时机制的锁获取方法,如
-
使用
Lock的tryLock方法:- 通过尝试获取锁,如果无法获取,则放弃并采取其他措施,避免进入等待状态。
-
减少锁的粒度:
- 尽量细化锁的范围,减少锁的持有时间,降低锁竞争和死锁风险。
-
使用锁的层次结构:
- 设定锁的层次结构,确保线程按照层次顺序获取锁,避免循环依赖。
-
-
示例:死锁的场景与避免方法
死锁示例:
public class DeadlockDemo { private final Object lock1 = new Object(); private final Object lock2 = new Object(); public void methodA() { synchronized (lock1) { System.out.println("Thread A acquired lock1"); try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } synchronized (lock2) { System.out.println("Thread A acquired lock2"); } } } public void methodB() { synchronized (lock2) { System.out.println("Thread B acquired lock2"); try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } synchronized (lock1) { System.out.println("Thread B acquired lock1"); } } } public static void main(String[] args) { DeadlockDemo demo = new DeadlockDemo(); new Thread(demo::methodA).start(); new Thread(demo::methodB).start(); } }输出:
Thread A acquired lock1 Thread B acquired lock2 // 线程A等待获取lock2,线程B等待获取lock1,形成死锁避免死锁:
通过锁顺序避免:
public class DeadlockAvoidance { private final Object lock1 = new Object(); private final Object lock2 = new Object(); public void methodA() { synchronized (lock1) { System.out.println("Thread A acquired lock1"); try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } synchronized (lock2) { System.out.println("Thread A acquired lock2"); } } } public void methodB() { synchronized (lock1) { // 按相同顺序获取锁 System.out.println("Thread B acquired lock1"); try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } synchronized (lock2) { System.out.println("Thread B acquired lock2"); } } } public static void main(String[] args) { DeadlockAvoidance demo = new DeadlockAvoidance(); new Thread(demo::methodA).start(); new Thread(demo::methodB).start(); } }输出:
Thread A acquired lock1 Thread B acquired lock1 // 线程B等待锁1释放 Thread A acquired lock2 Thread B acquired lock2
总结:通过合理的锁设计和同步策略,可以有效避免死锁的发生,确保多线程程序的稳定性和可靠性。
37. 解释ReadWriteLock的工作机制,并说明它的优势。
回答:
-
ReadWriteLock:- 是
java.util.concurrent.locks包下的一个接口,定义了读写锁的行为。 - 允许多个线程同时读取共享资源,但在写入时独占访问,保证数据一致性。
- 是
-
工作机制:
-
读锁(Read Lock):
- 多个线程可以同时持有读锁,只要没有线程持有写锁。
- 适用于读多写少的场景,提高并发性能。
-
写锁(Write Lock):
- 只有一个线程可以持有写锁,并且在持有写锁时,所有其他线程的读锁和写锁请求都会被阻塞。
- 适用于需要独占访问的写操作,确保数据一致性。
-
锁获取顺序:
- 当有线程持有写锁时,后续的读锁和写锁请求会被阻塞。
- 当有线程持有读锁时,写锁请求会被阻塞,但新读锁请求可能被允许(取决于具体实现和配置)。
-
-
优势:
-
提高并发性能:
- 在读多写少的场景下,允许多个线程并发读操作,减少锁竞争,提高性能。
-
数据一致性:
- 写锁确保写操作的独占性,防止数据被同时修改,保证数据一致性。
-
灵活的锁控制:
- 通过读锁和写锁的分离,提供更细粒度的锁控制,适应不同的访问需求。
-
-
实现类:
-
ReentrantReadWriteLock:- 提供了可重入的读写锁实现,支持公平锁和非公平锁。
-
StampedLock(Java 8引入):
- 提供了读写锁的另一种实现,支持乐观读锁,适用于高并发读写场景。
-
-
示例:
import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteLockExample { private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); private int count = 0; public void write() { rwLock.writeLock().lock(); try { count++; System.out.println(Thread.currentThread().getName() + " incremented count to " + count); } finally { rwLock.writeLock().unlock(); } } public void read() { rwLock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + " read count as " + count); } finally { rwLock.readLock().unlock(); } } public static void main(String[] args) { ReadWriteLockExample example = new ReadWriteLockExample(); // 创建读线程 Runnable readTask = () -> { for (int i = 0; i < 5; i++) { example.read(); try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }; // 创建写线程 Runnable writeTask = () -> { for (int i = 0; i < 5; i++) { example.write(); try { Thread.sleep(150); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }; // 启动线程 new Thread(readTask, "Reader-1").start(); new Thread(readTask, "Reader-2").start(); new Thread(writeTask, "Writer-1").start(); } }
输出:
Reader-1 read count as 0
Reader-2 read count as 0
Writer-1 incremented count to 1
Reader-1 read count as 1
Reader-2 read count as 1
Writer-1 incremented count to 2
Reader-1 read count as 2
Reader-2 read count as 2
Writer-1 incremented count to 3
Reader-1 read count as 3
Reader-2 read count as 3
Writer-1 incremented count to 4
Reader-1 read count as 4
Reader-2 read count as 4
Writer-1 incremented count to 5
注意:
-
公平性:
ReentrantReadWriteLock支持公平锁和非公平锁,通过构造器参数设置:ReadWriteLock rwLock = new ReentrantReadWriteLock(true); // 公平锁 -
读锁和写锁的互斥:当写锁被持有时,所有读锁和写锁的获取请求都会被阻塞,直到写锁被释放。
十一、线程池的高级配置与优化
38. 如何自定义一个线程池?解释ThreadPoolExecutor构造函数的各个参数。
回答:
-
自定义线程池:
- 通过直接创建
ThreadPoolExecutor实例,传入自定义的参数,实现灵活的线程池配置。 - 适用于需要精细控制线程池行为的场景,避免使用
Executors工厂方法带来的潜在问题(如无限线程池、无界队列等)。
- 通过直接创建
-
ThreadPoolExecutor构造函数:public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) -
参数详解:
corePoolSize(核心线程数):- 线程池中始终保持的最小线程数量。
- 即使线程处于空闲状态,也不会被回收,除非调用
allowCoreThreadTimeOut(true)。
maximumPoolSize(最大线程数):- 线程池中允许的最大线程数量。
- 当任务队列已满且核心线程数已被占用时,线程池会创建新的线程,直到达到最大线程数。
keepAliveTime(线程空闲保持时间):- 超过核心线程数的线程在空闲时保持的最长时间,超过后会被回收。
- 单位由
unit参数指定。
unit(时间单位):- 指定
keepAliveTime的时间单位,如TimeUnit.SECONDS、TimeUnit.MILLISECONDS等。
- 指定
workQueue(任务队列):- 用于存储待执行任务的阻塞队列。
- 常见实现类包括:
LinkedBlockingQueue:有界或无界的阻塞队列,适用于任务数不固定的场景。ArrayBlockingQueue:有界的阻塞队列,适用于固定容量的任务队列。SynchronousQueue:无缓冲的阻塞队列,每个插入操作必须等待对应的移除操作。PriorityBlockingQueue:基于优先级堆的无界阻塞队列。
threadFactory(线程工厂):- 用于创建新线程的工厂,允许自定义线程的创建方式,如设置线程名、守护线程等。
- 通过
Executors.defaultThreadFactory()或自定义实现。
handler(拒绝策略):- 当线程池无法接受新任务时的处理策略。
- 常见策略包括:
AbortPolicy(默认):抛出RejectedExecutionException。CallerRunsPolicy:由调用线程执行任务。DiscardPolicy:静默丢弃任务。DiscardOldestPolicy:丢弃队列中最旧的任务,尝试执行新任务。
-
示例:自定义
ThreadPoolExecutorimport java.util.concurrent.*; public class CustomThreadPoolExecutor { public static void main(String[] args) { int corePoolSize = 5; int maximumPoolSize = 10; long keepAliveTime = 60; TimeUnit unit = TimeUnit.SECONDS; BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100); ThreadFactory threadFactory = Executors.defaultThreadFactory(); RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler ); // 提交任务 for (int i = 0; i < 50; i++) { final int taskId = i; executor.execute(() -> { System.out.println("Task " + taskId + " is running on " + Thread.currentThread().getName()); try { Thread.sleep(1000); // 模拟任务执行 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } // 关闭线程池 executor.shutdown(); try { if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { executor.shutdownNow(); } } catch (InterruptedException e) { executor.shutdownNow(); } } }
注意:
- 合理配置参数:根据应用需求,合理配置核心线程数、最大线程数、任务队列大小等参数,避免资源浪费或系统过载。
- 拒绝策略选择:根据任务的重要性和系统要求,选择合适的拒绝策略,避免任务丢失或系统异常。
39. 解释ForkJoinPool和ThreadPoolExecutor的区别。
回答:
-
ForkJoinPool:- 专为执行
ForkJoinTask(如RecursiveTask和RecursiveAction)设计的线程池,实现了工作窃取(Work Stealing)算法。 - 适用于分治算法和递归任务,利用多核处理器的优势,实现高效的并行计算。
- 线程池中的线程通常为工作线程,专注于执行分解后的子任务。
- 通过
ForkJoinPool.commonPool()可以获取一个共享的全局线程池。
- 专为执行
-
ThreadPoolExecutor:- 是Java中最通用的线程池实现,支持多种任务类型和调度策略。
- 适用于处理各种类型的任务,如IO密集型、计算密集型等。
- 不具备工作窃取机制,适用于独立的任务执行。
- 提供了丰富的构造器参数,允许自定义线程池行为。
-
主要区别:
特性 ForkJoinPoolThreadPoolExecutor任务类型 专为 ForkJoinTask设计,适用于分治和递归任务适用于各种类型的 Runnable或Callable任务工作窃取机制 是,支持工作窃取(Work Stealing) 否,基于提交队列的调度策略 线程复用与创建 使用工作线程,动态创建和复用线程 通过核心线程数和最大线程数控制线程的创建与复用 适用场景 计算密集型、可分解的并行任务 IO密集型、短期任务、固定任务量的场景 提供的API 特定于 ForkJoinTask的API,如fork()、join()通用的 ExecutorServiceAPI线程池的特性 内部使用工作窃取算法,优化多核处理器利用率 提供灵活的任务队列和拒绝策略 -
示例:
使用
ForkJoinPool:import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveTask; public class ForkJoinVsThreadPool { // ForkJoinTask实现 static class SumTask extends RecursiveTask<Long> { private static final int THRESHOLD = 1000; private final int[] array; private final int start; private final int end; public SumTask(int[] array, int start, int end) { this.array = array; this.start = start; this.end = end; } @Override protected Long compute() { if (end - start <= THRESHOLD) { long sum = 0; for (int i = start; i < end; i++) { sum += array[i]; } return sum; } else { int mid = (start + end) / 2; SumTask left = new SumTask(array, start, mid); SumTask right = new SumTask(array, mid, end); left.fork(); // 异步执行左半部分 long rightResult = right.compute(); // 同步执行右半部分 long leftResult = left.join(); // 等待左半部分结果 return leftResult + rightResult; } } } public static void main(String[] args) throws Exception { int[] array = new int[10000]; for (int i = 0; i < array.length; i++) { array[i] = i + 1; } ForkJoinPool forkJoinPool = new ForkJoinPool(); SumTask task = new SumTask(array, 0, array.length); long result = forkJoinPool.invoke(task); System.out.println("ForkJoinPool Sum: " + result); // 输出: ForkJoinPool Sum: 50005000 // 使用ThreadPoolExecutor实现相同功能 ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(4); Future<Long> future = executor.submit(() -> { long sum = 0; for (int num : array) { sum += num; } return sum; }); System.out.println("ThreadPoolExecutor Sum: " + future.get()); // 输出: ThreadPoolExecutor Sum: 50005000 executor.shutdown(); } }
总结:ForkJoinPool适用于可分解的并行计算任务,利用工作窃取机制提升多核处理器的利用率;而ThreadPoolExecutor则更通用,适用于各种类型的并发任务。
40. 解释Future和CompletableFuture的区别。
回答:
-
Future<V>:-
是
java.util.concurrent包下的一个接口,表示异步计算的结果。 -
提供了获取结果、取消任务、检查任务是否完成等方法。
-
主要方法:
get():等待任务完成并获取结果。cancel(boolean mayInterruptIfRunning):尝试取消任务。isDone():检查任务是否完成。isCancelled():检查任务是否被取消。
-
限制:
- 不支持链式调用和回调机制。
- 一旦创建,不能手动完成结果。
- 只适用于单一的异步结果处理。
-
-
CompletableFuture<V>:- 是
java.util.concurrent包下的一个类,实现了Future和CompletionStage接口,提供了更强大的异步编程能力。 - 支持链式调用、回调、任务组合、异常处理等功能。
- 提供了多种方法来构建和管理复杂的异步任务流程。
- 主要方法:
supplyAsync(Supplier<U> supplier):异步执行提供者的任务,并返回结果。thenApply(Function<? super T, ? extends U> fn):在任务完成后,应用函数处理结果。thenAccept(Consumer<? super T> action):在任务完成后,接受结果执行操作。thenRun(Runnable action):在任务完成后,执行操作,不使用结果。exceptionally(Function<Throwable, ? extends T> fn):处理任务中的异常。handle(BiFunction<? super T, Throwable, ? extends U> fn):同时处理结果和异常。allOf(CompletableFuture<?>... cfs):等待所有给定的CompletableFuture完成。anyOf(CompletableFuture<?>... cfs):等待任意一个CompletableFuture完成。
- 是
-
区别:
特性 FutureCompletableFuture继承/实现 接口 类,实现了 Future和CompletionStage接口回调机制 不支持 支持多种回调和链式调用方法 任务组合 不支持 支持组合多个异步任务,如 thenCombine、thenCompose等手动完成 不能手动完成(由线程池完成) 支持手动完成,通过 complete()、completeExceptionally()方法异常处理 通过 get()抛出异常提供专门的异常处理方法,如 exceptionally()、handle()取消任务 支持 cancel()方法支持 cancel()方法,但更多地用于任务的结果处理 -
示例:
使用
Future:import java.util.concurrent.*; public class FutureExample { public static void main(String[] args) throws InterruptedException, ExecutionException { ExecutorService executor = Executors.newFixedThreadPool(2); Future<Integer> future = executor.submit(() -> { Thread.sleep(1000); return 42; }); System.out.println("Result: " + future.get()); // 阻塞等待结果 executor.shutdown(); } }使用
CompletableFuture:import java.util.concurrent.CompletableFuture; public class CompletableFutureExample { public static void main(String[] args) throws InterruptedException { CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return 42; }); future.thenApply(result -> "Result is " + result) .thenAccept(System.out::println); // 主线程等待异步任务完成 Thread.sleep(2000); } }
总结:CompletableFuture通过提供丰富的API和功能,极大地简化了异步编程,支持复杂的任务组合和结果处理;而Future则适用于简单的异步结果获取,但缺乏灵活性和功能。
十二、同步原语与信号量
41. 解释Semaphore的工作机制,并说明其常见用途。
回答:
-
Semaphore:- 是
java.util.concurrent包下的一个同步工具类,基于信号量(Semaphore)的概念。 - 用于控制同时访问特定资源的线程数量。
- 是
-
工作机制:
-
许可(Permit):
Semaphore维护一个许可计数器,表示可以同时访问的线程数量。- 初始许可数量在创建时指定。
-
获取许可:
- 线程调用
acquire()方法获取一个许可,如果许可数量大于零,许可数量减1,线程继续执行。 - 如果许可数量为零,线程会被阻塞,直到有许可可用。
- 线程调用
-
释放许可:
- 线程调用
release()方法释放一个许可,许可数量加1,唤醒等待的线程。
- 线程调用
-
公平性:
Semaphore可以设置为公平模式或非公平模式。- 公平模式下,线程按照请求许可的顺序获取许可。
- 非公平模式下,线程可能会抢占许可,增加吞吐量。
-
-
常见用途:
-
限流:
- 控制系统中并发访问某个资源(如数据库连接、API调用)的线程数量,防止过载。
-
资源池管理:
- 管理有限数量的资源,如连接池、线程池中的任务槽。
-
实现信号:
- 通过控制许可数量,实现线程间的信号传递,如同步多个线程的执行。
-
读写控制:
- 在某些场景下,使用信号量控制读写线程的并发访问。
-
-
示例:限流控制,最多允许3个线程同时访问资源
import java.util.concurrent.Semaphore; public class SemaphoreExample { private static final int MAX_CONCURRENT_ACCESS = 3; private final Semaphore semaphore = new Semaphore(MAX_CONCURRENT_ACCESS); public void accessResource(String threadName) { try { System.out.println(threadName + " is attempting to acquire a permit."); semaphore.acquire(); // 获取许可 System.out.println(threadName + " has acquired a permit."); // 模拟资源访问 Thread.sleep(2000); System.out.println(threadName + " is releasing the permit."); semaphore.release(); // 释放许可 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } public static void main(String[] args) { SemaphoreExample example = new SemaphoreExample(); Runnable task = () -> example.accessResource(Thread.currentThread().getName()); // 启动5个线程,超过许可数量的线程会被阻塞 for (int i = 0; i < 5; i++) { new Thread(task, "Thread-" + i).start(); } } }
输出:
Thread-0 is attempting to acquire a permit.
Thread-0 has acquired a permit.
Thread-1 is attempting to acquire a permit.
Thread-1 has acquired a permit.
Thread-2 is attempting to acquire a permit.
Thread-2 has acquired a permit.
Thread-3 is attempting to acquire a permit.
Thread-4 is attempting to acquire a permit.
Thread-0 is releasing the permit.
Thread-3 has acquired a permit.
Thread-1 is releasing the permit.
Thread-4 has acquired a permit.
Thread-2 is releasing the permit.
Thread-3 is releasing the permit.
Thread-4 is releasing the permit.
注意:
- 许可数量控制:
Semaphore通过许可数量限制并发访问,确保资源不会被过度占用。 - 公平性选择:根据需求选择公平模式或非公平模式,平衡吞吐量和公平性。
42. 什么是CountDownLatch?它的使用场景是什么?
回答:
-
CountDownLatch:- 是
java.util.concurrent包下的一个同步工具类,允许一个或多个线程等待,直到其他线程完成一组操作。 - 通过一个计数器控制,计数器的初始值由创建时指定,每调用一次
countDown()方法,计数器减1。 - 当计数器达到零时,等待的线程被唤醒。
- 是
-
使用场景:
-
等待多个线程完成初始化:
- 主线程等待多个子线程完成初始化工作后继续执行。
-
并行测试:
- 在测试中,等待多个测试步骤完成后进行断言或结果验证。
-
任务同步:
- 多个任务完成后,触发后续的操作。
-
启动信号:
- 控制多个线程在收到启动信号后同时开始执行。
-
-
示例:等待多个线程完成任务后继续执行
import java.util.concurrent.CountDownLatch; public class CountDownLatchExample { public static void main(String[] args) throws InterruptedException { int numberOfWorkers = 3; CountDownLatch latch = new CountDownLatch(numberOfWorkers); // 创建并启动工作线程 for (int i = 0; i < numberOfWorkers; i++) { final int workerId = i; new Thread(() -> { System.out.println("Worker " + workerId + " started."); try { Thread.sleep(1000 + workerId * 500); // 模拟工作 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Worker " + workerId + " finished."); latch.countDown(); // 完成任务,计数器减1 }).start(); } // 主线程等待所有工作线程完成 System.out.println("Main thread waiting for workers to finish."); latch.await(); System.out.println("All workers finished. Main thread proceeding."); } }
输出:
Worker 0 started.
Worker 1 started.
Worker 2 started.
Main thread waiting for workers to finish.
Worker 0 finished.
Worker 1 finished.
Worker 2 finished.
All workers finished. Main thread proceeding.
注意:
- 一次性使用:
CountDownLatch不能重用,计数器只能从初始值减到零一次。 - 无法添加新的参与者:在初始化后,无法动态增加计数器。
十三、线程安全的设计模式
43. 什么是双重检查锁定(Double-Check Locking)?它如何在Java中实现?
回答:
-
双重检查锁定(Double-Check Locking):
- 是一种优化的同步机制,旨在减少在多线程环境下获取锁的开销。
- 常用于单例模式的懒加载,实现线程安全的单例实例创建。
-
工作原理:
-
第一次检查:
- 在同步块外检查实例是否已创建,如果已创建,则直接返回,避免获取锁的开销。
-
加锁和第二次检查:
- 如果实例未创建,进入同步块,再次检查实例是否已创建。
- 如果仍未创建,则创建实例。
-
-
实现方式:
示例:
public class Singleton { private static volatile Singleton instance; private Singleton() { // 私有构造函数,防止外部实例化 } public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { // 加锁 if (instance == null) { // 第二次检查 instance = new Singleton(); } } } return instance; } } -
关键点:
-
volatile修饰:instance变量必须声明为volatile,确保可见性和禁止指令重排序,防止创建部分初始化的实例。
-
双重检查:
- 同步块外和内部都进行实例检查,确保线程安全且提高性能。
-
-
优缺点:
-
优点:
- 高效,避免不必要的锁定操作。
- 线程安全,确保单例实例的唯一性。
-
缺点:
- 实现复杂,易出错。
- 需要
volatile支持,Java 5及以上版本。
-
注意:
-
确保
volatile:在Java 5及以上版本,通过volatile关键字修饰实例变量,确保双重检查锁定的正确性。 -
替代方案:使用静态内部类或枚举实现单例,简化实现且天然线程安全。
静态内部类示例:
public class Singleton { private Singleton() { // 私有构造函数 } private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }枚举实现:
public enum Singleton { INSTANCE; public void someMethod() { // 方法实现 } }
44. 解释ReadWriteLock的优势,并举例说明如何使用它。
回答:
-
ReadWriteLock的优势:-
提高并发性:
- 允许多个线程同时读取共享资源,提升读操作的并发性能。
-
优化资源利用:
- 当读操作远多于写操作时,使用
ReadWriteLock可以减少读操作之间的阻塞,优化资源利用率。
- 当读操作远多于写操作时,使用
-
数据一致性:
- 写锁确保独占访问,防止数据被多个线程同时修改,保证数据一致性。
-
灵活的锁控制:
- 通过读锁和写锁的分离,提供更细粒度的锁控制,适应不同的访问需求。
-
-
使用示例:使用
ReentrantReadWriteLock实现线程安全的共享资源访问import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class SharedData { private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); private int data = 0; // 写操作 public void write(int value) { rwLock.writeLock().lock(); try { data = value; System.out.println(Thread.currentThread().getName() + " wrote data: " + data); } finally { rwLock.writeLock().unlock(); } } // 读操作 public int read() { rwLock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + " read data: " + data); return data; } finally { rwLock.readLock().unlock(); } } public static void main(String[] args) { SharedData sharedData = new SharedData(); // 创建多个读线程 Runnable readTask = () -> { for (int i = 0; i < 5; i++) { sharedData.read(); try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }; // 创建写线程 Runnable writeTask = () -> { for (int i = 0; i < 5; i++) { sharedData.write(i); try { Thread.sleep(150); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }; // 启动线程 new Thread(readTask, "Reader-1").start(); new Thread(readTask, "Reader-2").start(); new Thread(writeTask, "Writer-1").start(); } }
输出:
Reader-1 read data: 0
Reader-2 read data: 0
Writer-1 wrote data: 0
Reader-1 read data: 0
Reader-2 read data: 0
Writer-1 wrote data: 1
Reader-1 read data: 1
Reader-2 read data: 1
Writer-1 wrote data: 2
Reader-1 read data: 2
Reader-2 read data: 2
Writer-1 wrote data: 3
Reader-1 read data: 3
Reader-2 read data: 3
Writer-1 wrote data: 4
Reader-1 read data: 4
Reader-2 read data: 4
注意:
- 锁降级:在读写锁的使用中,可以先获取写锁,然后获取读锁,最后释放写锁,实现锁的降级。
- 避免锁升级:不要在持有读锁的情况下尝试获取写锁,以防死锁。
十四、总结与最佳实践
45. 什么是并发编程中的“锁粗化”(Lock Coarsening)?它的优势是什么?
回答:
-
锁粗化(Lock Coarsening):
- 是一种优化技术,旨在通过减少锁的获取和释放次数,降低锁操作的开销。
- 将多个连续的锁操作合并为一个更大的锁操作,减少锁的粒度。
-
工作原理:
- 当多个相邻的代码块需要获取同一把锁时,编译器或JIT优化器会将这些锁操作合并为一个更大的锁区域。
- 这样可以减少频繁的锁获取和释放,提高性能。
-
优势:
-
减少锁操作开销:
- 合并锁操作,减少锁获取和释放的次数,降低性能开销。
-
提高代码执行效率:
- 通过减少同步操作的频率,提升代码的整体执行效率。
-
简化锁管理:
- 通过锁粗化,避免锁的嵌套和频繁切换,提高锁管理的简洁性。
-
-
示例:
未锁粗化的代码:
public class LockCoarseningExample { private final Object lock = new Object(); private int count = 0; public void increment() { synchronized (lock) { count++; } } public void decrement() { synchronized (lock) { count--; } } public static void main(String[] args) { LockCoarseningExample example = new LockCoarseningExample(); example.increment(); example.decrement(); } }锁粗化后的代码:
public class LockCoarseningExample { private final Object lock = new Object(); private int count = 0; public void modify() { synchronized (lock) { // 合并多个锁操作为一个锁操作 count++; count--; } } public static void main(String[] args) { LockCoarseningExample example = new LockCoarseningExample(); example.modify(); } } -
注意:
- 锁粗化通常由编译器或JIT优化器自动完成,开发者无需手动干预。
- 在设计并发程序时,合理划分锁的粒度,结合锁粗化优化,提升程序性能。
46. 如何选择合适的并发集合类?
回答:
选择合适的并发集合类需要根据具体的应用场景、并发需求和性能要求来决定。以下是一些常见的选择标准和建议:
-
集合类型:
-
Map接口:-
ConcurrentHashMap:- 适用于高并发的键值对存储,支持快速读写。
- 不允许
null键和null值。
-
ConcurrentSkipListMap:- 适用于需要有序键的高并发场景。
- 支持范围查询和有序操作。
-
Collections.synchronizedMap(new HashMap<>()):- 适用于低并发或小规模的线程安全需求。
- 读写操作需要手动同步。
-
-
List接口:-
CopyOnWriteArrayList:- 适用于读多写少的场景,提供线程安全的
List实现。 - 写操作开销较大,不适用于频繁修改的场景。
- 适用于读多写少的场景,提供线程安全的
-
Collections.synchronizedList(new ArrayList<>()):- 适用于低并发或小规模的线程安全需求。
- 需要在迭代时手动同步。
-
CopyOnWriteArraySet:- 适用于读多写少的唯一元素集合。
-
-
Queue接口:-
ConcurrentLinkedQueue:- 适用于高并发的无界非阻塞队列。
- 支持快速的并发读写操作。
-
LinkedBlockingQueue:- 适用于生产者-消费者模型,支持有界或无界阻塞队列。
- 支持线程阻塞等待任务的入队和出队。
-
ArrayBlockingQueue:- 适用于需要固定容量的阻塞队列,控制资源的并发访问。
-
PriorityBlockingQueue:- 适用于需要按优先级排序的阻塞队列。
-
SynchronousQueue:- 适用于直接传递任务给消费者,常用于线程池的工作队列。
-
-
Deque接口:-
ConcurrentLinkedDeque:- 适用于高并发的无界双端队列。
-
LinkedBlockingDeque:- 适用于需要阻塞双端队列的生产者-消费者模型。
-
-
-
其他考虑因素:
-
有界 vs 无界:
- 根据任务量和资源限制选择有界或无界的集合实现,避免资源耗尽或任务丢失。
-
有序性:
- 选择支持有序操作的集合,如
ConcurrentSkipListMap、PriorityBlockingQueue等,满足特定的顺序需求。
- 选择支持有序操作的集合,如
-
性能需求:
- 对于高并发读写,选择非阻塞的集合实现,如
ConcurrentHashMap、ConcurrentLinkedQueue等。 - 对于生产者-消费者模式,选择阻塞队列,如
LinkedBlockingQueue、ArrayBlockingQueue等。
- 对于高并发读写,选择非阻塞的集合实现,如
-
线程安全性:
- 确保所选集合类本身是线程安全的,或通过同步包装器进行保护。
-
-
示例选择:
-
高并发键值对存储,无需有序:
- 使用
ConcurrentHashMap。
- 使用
-
高并发键值对存储,需要有序:
- 使用
ConcurrentSkipListMap。
- 使用
-
读多写少的线程安全列表:
- 使用
CopyOnWriteArrayList。
- 使用
-
生产者-消费者模型,固定容量队列:
- 使用
ArrayBlockingQueue。
- 使用
-
需要按优先级处理的任务队列:
- 使用
PriorityBlockingQueue。
- 使用
-
高并发双端队列:
- 使用
ConcurrentLinkedDeque。
- 使用
-
总结:根据应用场景、并发需求和性能要求,选择最适合的并发集合类,以实现高效、线程安全的数据处理。
47. 什么是Exchanger?它的典型应用场景是什么?
回答:
-
Exchanger<V>:- 是
java.util.concurrent包下的一个同步工具类,用于在两个线程之间交换数据。 - 每个参与交换的线程调用
exchange(V x)方法,将自己的数据与对方线程的数据交换。 - 线程在交换点会阻塞,直到两个线程都到达交换点并完成数据交换。
- 是
-
工作机制:
- 当一个线程调用
exchange()方法时,它会等待另一个线程也调用exchange()。 - 两个线程一旦都到达交换点,会交换它们传递的数据,并继续执行。
- 当一个线程调用
-
典型应用场景:
-
双线程数据交换:
- 两个线程需要互相传递数据,如生产者与消费者之间的双向数据交换。
-
协作计算:
- 两个线程需要在某个阶段交换中间结果,协同完成任务。
-
同步通信:
- 在并行计算中,多个线程需要在特定阶段同步并交换数据,以保持协作的一致性。
-
-
示例:两个线程交换数据
import java.util.concurrent.Exchanger; public class ExchangerExample { public static void main(String[] args) { Exchanger<String> exchanger = new Exchanger<>(); // 线程A:发送数据 new Thread(() -> { String data = "Data from Thread A"; try { System.out.println("Thread A sending: " + data); String received = exchanger.exchange(data); System.out.println("Thread A received: " + received); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }, "Thread-A").start(); // 线程B:发送数据 new Thread(() -> { String data = "Data from Thread B"; try { System.out.println("Thread B sending: " + data); String received = exchanger.exchange(data); System.out.println("Thread B received: " + received); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }, "Thread-B").start(); } }
输出:
Thread A sending: Data from Thread A
Thread B sending: Data from Thread B
Thread A received: Data from Thread B
Thread B received: Data from Thread A
注意:
- 线程配对:
Exchanger只支持两个线程之间的数据交换,无法同时与多个线程交换。 - 超时控制:可以使用带有超时参数的
exchange(V x, long timeout, TimeUnit unit)方法,避免线程无限等待。
48. 解释CountDownLatch和CyclicBarrier的区别及其各自的应用场景。
回答:
-
CountDownLatch:- 是
java.util.concurrent包下的一个同步工具类,允许一个或多个线程等待,直到其他线程完成一组操作。 - 计数器一次性,从初始值减到零后不可重用。
特点:
- 计数器在初始化后无法修改。
- 适用于等待固定数量的事件或任务完成。
- 是
-
CyclicBarrier:- 是
java.util.concurrent包下的一个同步工具类,允许一组线程互相等待,直到所有线程都到达某个共同的屏障点。 - 计数器可以重用,适用于多阶段的同步控制。
特点:
- 计数器可以重用,支持多次等待和同步。
- 可以设置一个回调,当所有线程到达屏障点时执行。
- 是
-
主要区别:
特性 CountDownLatchCyclicBarrier重用性 不可重用,一次性 可重用,多次使用同一个屏障点 参与者注册 在创建时指定固定数量,不能动态变化 在创建时指定固定数量,可在运行时动态注册 额外动作 不支持,只有等待和计数 支持,当所有线程到达屏障点时可执行回调 使用场景 等待多个线程完成初始化或任务 同步多个线程在多个阶段的任务执行 -
应用场景:
CountDownLatch:- 等待多个线程完成:主线程等待多个子线程完成初始化或任务后继续执行。
- 并行测试:等待测试中的多个步骤完成后进行结果验证。
- 启动信号:一个线程等待其他线程准备完毕后一起开始执行。
CyclicBarrier:- 多阶段任务同步:在多个阶段的并行任务中,等待所有线程完成一个阶段后再继续下一阶段。
- 并行计算:在并行计算中,等待所有线程完成某个计算步骤后进行汇总。
- 集体行动:一组线程在某个共同点同步执行集体行动。
-
示例:
CountDownLatch示例:import java.util.concurrent.CountDownLatch; public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { int numberOfWorkers = 3; CountDownLatch latch = new CountDownLatch(numberOfWorkers); for (int i = 0; i < numberOfWorkers; i++) { final int workerId = i; new Thread(() -> { System.out.println("Worker " + workerId + " is doing work."); try { Thread.sleep(1000 + workerId * 500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Worker " + workerId + " finished work."); latch.countDown(); }).start(); } System.out.println("Main thread waiting for workers to finish."); latch.await(); System.out.println("All workers have finished. Main thread proceeding."); } }CyclicBarrier示例:import java.util.concurrent.CyclicBarrier; import java.util.concurrent.BrokenBarrierException; public class CyclicBarrierDemo { public static void main(String[] args) { int numberOfParties = 3; CyclicBarrier barrier = new CyclicBarrier(numberOfParties, () -> { System.out.println("All parties have arrived at the barrier. Proceeding..."); }); for (int i = 0; i < numberOfParties; i++) { final int threadId = i; new Thread(() -> { try { System.out.println("Thread " + threadId + " is performing part 1."); Thread.sleep(1000 + threadId * 500); System.out.println("Thread " + threadId + " is waiting at the barrier."); barrier.await(); // 等待所有线程到达 System.out.println("Thread " + threadId + " is performing part 2."); } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } }).start(); } } }
输出:
Worker 0 is doing work.
Worker 1 is doing work.
Worker 2 is doing work.
Main thread waiting for workers to finish.
Worker 0 finished work.
Worker 1 finished work.
Worker 2 finished work.
All workers have finished. Main thread proceeding.
Thread 0 is performing part 1.
Thread 1 is performing part 1.
Thread 2 is performing part 1.
Thread 0 is waiting at the barrier.
Thread 1 is waiting at the barrier.
Thread 2 is waiting at the barrier.
All parties have arrived at the barrier. Proceeding...
Thread 0 is performing part 2.
Thread 1 is performing part 2.
Thread 2 is performing part 2.
注意:
- 可重用性:
CyclicBarrier在所有线程到达屏障点后,可以继续使用,适用于多阶段同步;CountDownLatch一旦计数为零,无法重用。 - 异常处理:在使用
CyclicBarrier时,需处理BrokenBarrierException和InterruptedException。
49. 什么是Phaser?它与CyclicBarrier和CountDownLatch有何不同?
回答:
-
Phaser:- 是
java.util.concurrent包下的一个同步工具类,支持多阶段的同步控制。 - 可以动态地注册和注销参与者,适用于可变数量的线程和多阶段任务。
- 继承自
ForkJoinTask,与ForkJoinPool紧密集成。
- 是
-
与
CyclicBarrier和CountDownLatch的区别:特性 CountDownLatchCyclicBarrierPhaser重用性 不可重用,一次性 可重用,多次使用同一个屏障点 可重用,多阶段同步,动态参与者 参与者注册 固定数量,创建时指定 固定数量,创建时指定 可动态注册和注销参与者,适应运行时变化 额外动作 不支持,只有等待和计数 支持,当所有线程到达屏障点时可执行回调 支持,当阶段完成时可执行回调 多阶段控制 不支持 支持一个屏障点 支持多个阶段的同步控制 适用场景 等待固定数量的线程完成某个操作 同步一组线程在某个屏障点 多阶段任务同步,动态参与者,多线程间的复杂同步需求 -
优势:
- 灵活性:
Phaser支持动态注册和注销参与者,适用于参与者数量不固定的场景。 - 多阶段同步:可以控制多个阶段的同步点,适用于多阶段的任务执行。
- 适应性强:支持灵活的线程同步,适用于复杂的并发任务控制。
- 灵活性:
-
使用示例:多阶段任务同步
import java.util.concurrent.Phaser; public class PhaserDemo { public static void main(String[] args) { Phaser phaser = new Phaser(1); // 注册主线程 int numberOfParties = 3; for (int i = 0; i < numberOfParties; i++) { phaser.register(); // 动态注册参与者 final int taskId = i; new Thread(() -> { System.out.println("Task " + taskId + " is performing Phase 1."); try { Thread.sleep(1000 + taskId * 500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } phaser.arriveAndAwaitAdvance(); // 到达阶段1并等待其他参与者 System.out.println("Task " + taskId + " is performing Phase 2."); try { Thread.sleep(1000 + taskId * 500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } phaser.arriveAndDeregister(); // 到达阶段2并注销 }, "Thread-" + i).start(); } // 等待所有参与者到达阶段1 phaser.arriveAndAwaitAdvance(); System.out.println("All tasks have completed Phase 1."); // 等待所有参与者到达阶段2并注销 phaser.arriveAndAwaitAdvance(); System.out.println("All tasks have completed Phase 2. Phaser terminating."); phaser.arriveAndDeregister(); // 主线程注销 } }
输出:
Task 0 is performing Phase 1.
Task 1 is performing Phase 1.
Task 2 is performing Phase 1.
All tasks have completed Phase 1.
Task 0 is performing Phase 2.
Task 1 is performing Phase 2.
Task 2 is performing Phase 2.
All tasks have completed Phase 2. Phaser terminating.
注意:
- 动态参与者:
Phaser允许在运行时动态注册和注销参与者,适应线程数量的变化。 - 阶段控制:通过多个
arriveAndAwaitAdvance()调用,实现多阶段的同步控制。
50. 解释StampedLock的工作机制及其优缺点。
回答:
-
StampedLock:- 是
java.util.concurrent.locks包下的一个锁实现,Java 8引入。 - 提供了一种更加灵活和高效的读写锁机制,支持乐观读锁。
- 适用于高并发读写场景,提升读操作的性能。
- 是
-
工作机制:
-
锁模式:
-
写锁(Write Lock):
- 独占锁,只有一个线程可以持有。
- 阻塞所有读锁和其他写锁的获取。
-
悲观读锁(Read Lock):
- 多个线程可以同时持有读锁,前提是没有线程持有写锁。
-
乐观读锁(Optimistic Read):
- 线程尝试在无需锁定的情况下读取数据,通过验证确保读取过程中数据未被修改。
- 适用于读多写少的场景,提升读操作的性能。
-
-
标记(Stamp):
- 每次获取锁时,返回一个
stamp值,代表锁的状态。 - 通过
stamp进行锁的释放和验证。
- 每次获取锁时,返回一个
-
锁获取与释放:
-
写锁:
- 使用
writeLock()方法获取写锁,返回stamp。 - 使用
unlockWrite(stamp)方法释放写锁。
- 使用
-
悲观读锁:
- 使用
readLock()方法获取读锁,返回stamp。 - 使用
unlockRead(stamp)方法释放读锁。
- 使用
-
乐观读锁:
- 使用
tryOptimisticRead()方法获取乐观读锁,返回stamp。 - 使用
validate(stamp)方法验证读取过程中数据是否被修改。
- 使用
-
-
-
优点:
-
高效的读操作:
- 乐观读锁允许在无需锁定的情况下进行读取,减少了锁的开销,提升了读操作的性能。
-
灵活的锁模式:
- 提供了写锁、悲观读锁和乐观读锁,适应不同的并发需求。
-
减少锁竞争:
- 乐观读锁在读多写少的场景下,显著减少锁竞争,提高并发性能。
-
-
缺点:
-
不可重入:
StampedLock不支持重入,即同一线程无法多次获取同一种锁。
-
复杂性:
- 使用
StampedLock需要管理stamp值,增加了编程复杂度。
- 使用
-
ABA问题:
- 乐观读锁可能受到ABA问题的影响,需谨慎处理。
-
不支持条件变量:
StampedLock不支持条件变量(Condition),无法实现更复杂的线程间通信。
-
-
示例:
import java.util.concurrent.locks.StampedLock; public class StampedLockExample { private final StampedLock sl = new StampedLock(); private int count = 0; // 写操作 public void increment() { long stamp = sl.writeLock(); try { count++; System.out.println(Thread.currentThread().getName() + " incremented count to " + count); } finally { sl.unlockWrite(stamp); } } // 悲观读操作 public int pessimisticRead() { long stamp = sl.readLock(); try { System.out.println(Thread.currentThread().getName() + " read count as " + count); return count; } finally { sl.unlockRead(stamp); } } // 乐观读操作 public int optimisticRead() { long stamp = sl.tryOptimisticRead(); int currentCount = count; if (!sl.validate(stamp)) { stamp = sl.readLock(); try { currentCount = count; } finally { sl.unlockRead(stamp); } } System.out.println(Thread.currentThread().getName() + " optimistically read count as " + currentCount); return currentCount; } public static void main(String[] args) throws InterruptedException { StampedLockExample example = new StampedLockExample(); // 创建写线程 Runnable writer = () -> { for (int i = 0; i < 5; i++) { example.increment(); try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }; // 创建读线程 Runnable reader = () -> { for (int i = 0; i < 5; i++) { example.optimisticRead(); example.pessimisticRead(); try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }; new Thread(writer, "Writer").start(); new Thread(reader, "Reader-1").start(); new Thread(reader, "Reader-2").start(); } }
输出:
Writer incremented count to 1
Reader-1 optimistically read count as 1
Reader-2 optimistically read count as 1
Reader-1 read count as 1
Reader-2 read count as 1
Writer incremented count to 2
Reader-1 optimistically read count as 2
Reader-2 optimistically read count as 2
Reader-1 read count as 2
Reader-2 read count as 2
...
总结:StampedLock通过支持乐观读锁和更细粒度的锁控制,提供了高效的并发读写能力,适用于读多写少的高并发场景,但使用时需注意其复杂性和不可重入性。
51. 什么是FutureTask?它如何与Callable和Runnable结合使用?
回答:
-
FutureTask<V>:- 是
java.util.concurrent包下的一个类,实现了RunnableFuture<V>接口,结合了Runnable和Future的功能。 - 代表一个可取消的异步计算任务,能够执行
Callable或Runnable任务,并返回结果或状态。
- 是
-
与
Callable和Runnable的结合使用:-
Callable<V>:- 是
java.util.concurrent包下的一个接口,代表一个可以返回结果并可能抛出异常的任务。
- 是
-
Runnable:- 是
java.lang包下的一个接口,代表一个不返回结果、不抛出异常的任务。
- 是
-
FutureTask的构造方法:- 可以接收
Callable<V>或Runnable和一个结果对象。
- 可以接收
示例:
import java.util.concurrent.*; public class FutureTaskExample { public static void main(String[] args) throws InterruptedException, ExecutionException { // 使用Callable构造FutureTask Callable<Integer> callable = () -> { Thread.sleep(1000); return 42; }; FutureTask<Integer> futureTask = new FutureTask<>(callable); // 启动线程执行FutureTask new Thread(futureTask).start(); // 主线程执行其他操作 System.out.println("Main thread is doing other work."); // 获取FutureTask的结果,可能会阻塞 int result = futureTask.get(); System.out.println("Result from FutureTask: " + result); // 使用Runnable构造FutureTask Runnable runnable = () -> System.out.println("Runnable task executed."); FutureTask<Void> runnableTask = new FutureTask<>(runnable, null); // 启动线程执行FutureTask new Thread(runnableTask).start(); // 等待Runnable任务完成 runnableTask.get(); System.out.println("Runnable task completed."); } } -
输出:
Main thread is doing other work.
Runnable task executed.
Runnable task completed.
Result from FutureTask: 42
注意:
- 任务取消:可以通过
futureTask.cancel(true)取消任务,true表示允许中断正在执行的任务。 - 结果获取:调用
get()方法获取任务结果,会阻塞直到任务完成;也可以使用带超时参数的get(long timeout, TimeUnit unit)方法。 - 与线程池结合:
FutureTask可以提交到ExecutorService,用于获取异步任务的结果。
52. 如何实现线程间的协作?请举例说明。
回答:
-
线程间的协作:
- 通过共享变量、同步工具类和通信机制,让多个线程协调完成复杂的任务。
- 主要方法包括:
- 使用锁(
synchronized、ReentrantLock)同步访问共享资源。 - 使用条件变量(
Condition、wait()、notify())实现线程间通信。 - 使用同步工具类(
CountDownLatch、CyclicBarrier、Semaphore、Exchanger)控制线程同步。 - 使用并发集合和原子变量,实现高效的线程间数据共享。
- 使用锁(
-
示例:生产者-消费者模型实现线程间协作
import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; public class ProducerConsumerCollaboration { private static final int BUFFER_SIZE = 5; private final BlockingQueue<Integer> buffer = new LinkedBlockingQueue<>(BUFFER_SIZE); // 生产者 class Producer implements Runnable { private final int id; public Producer(int id) { this.id = id; } @Override public void run() { try { for (int i = 0; i < 10; i++) { int item = id * 100 + i; buffer.put(item); // 生产并放入缓冲区,可能阻塞 System.out.println("Producer " + id + " produced: " + item); Thread.sleep((long) (Math.random() * 500)); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } // 消费者 class Consumer implements Runnable { private final int id; public Consumer(int id) { this.id = id; } @Override public void run() { try { while (true) { int item = buffer.take(); // 消费并取出缓冲区,可能阻塞 System.out.println("Consumer " + id + " consumed: " + item); Thread.sleep((long) (Math.random() * 1000)); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } public void start() { // 启动生产者线程 for (int i = 1; i <= 2; i++) { new Thread(new Producer(i), "Producer-" + i).start(); } // 启动消费者线程 for (int i = 1; i <= 3; i++) { new Thread(new Consumer(i), "Consumer-" + i).start(); } } public static void main(String[] args) { ProducerConsumerCollaboration pc = new ProducerConsumerCollaboration(); pc.start(); } }
输出示例:
Producer 1 produced: 100
Producer 2 produced: 200
Consumer 1 consumed: 100
Producer 1 produced: 101
Consumer 2 consumed: 200
Producer 2 produced: 201
Consumer 3 consumed: 101
...
注意:
- 阻塞行为:
BlockingQueue的put()和take()方法会根据队列的状态自动阻塞和唤醒线程,实现生产者和消费者的协作。 - 线程管理:在实际应用中,可以使用
ExecutorService管理生产者和消费者线程,简化线程创建和管理。 - 终止条件:示例中的消费者线程为无限循环,实际应用中需要设计合理的终止条件,如使用特殊的“结束”信号或中断机制。
1436

被折叠的 条评论
为什么被折叠?



