Java多线程与并发常见考题
基础概念
1. 进程与线程的区别
- 进程:操作系统资源分配的基本单位,拥有独立的内存空间
- 线程:CPU调度的基本单位,共享所属进程的内存空间
- 区别:
- 进程是资源分配的最小单位,线程是CPU调度的最小单位
- 一个进程可以包含多个线程,线程依赖于进程存在
- 进程间通信较为复杂,线程间通信相对简单
- 进程切换开销大,线程切换开销小
2. 创建线程的方式
- 继承Thread类
class MyThread extends Thread {
@Override
public void run() {
// 线程执行代码
}
}
// 使用
MyThread t = new MyThread();
t.start();
- 实现Runnable接口
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行代码
}
}
// 使用
Thread t = new Thread(new MyRunnable());
t.start();
- 实现Callable接口(有返回值)
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "线程执行结果";
}
}
// 使用
FutureTask<String> task = new FutureTask<>(new MyCallable());
Thread t = new Thread(task);
t.start();
String result = task.get(); // 获取返回值
- 使用线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> {
// 线程执行代码
});
3. 线程的生命周期
- 新建(New): 创建线程对象
- 就绪(Runnable): 调用start()方法后等待CPU调度
- 运行(Running): 获得CPU时间片正在执行
- 阻塞(Blocked): 等待获取锁
- 等待(Waiting): 调用wait()等方法进入等待状态
- 超时等待(Timed Waiting): 调用sleep(time)等方法进入超时等待
- 终止(Terminated): 线程执行完毕
线程同步
4. 线程同步的方式
- synchronized关键字
- 修饰实例方法:锁是当前对象实例
- 修饰静态方法:锁是当前类的Class对象
- 修饰代码块:锁是括号里的对象
// 同步方法
public synchronized void method() {
// 临界区代码
}
// 同步代码块
public void method() {
synchronized(this) {
// 临界区代码
}
}
- Lock接口
Lock lock = new ReentrantLock();
try {
lock.lock();
// 临界区代码
} finally {
lock.unlock(); // 必须在finally中释放锁
}
- volatile关键字:保证变量的可见性,但不保证原子性
private volatile boolean flag = false;
- ThreadLocal:为每个线程提供独立的变量副本
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
threadLocal.set(123); // 设置值
Integer value = threadLocal.get(); // 获取值
- 原子类:如AtomicInteger、AtomicLong等
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 原子操作
5. synchronized和Lock的区别
- 获取锁方式:synchronized自动获取释放,Lock需手动获取释放
- 尝试非阻塞获取锁:synchronized不支持,Lock支持tryLock()
- 获取锁超时机制:synchronized不支持,Lock支持tryLock(time)
- 公平锁:synchronized非公平,Lock可以是公平的
- 中断机制:synchronized不可中断,Lock可以lockInterruptibly()
- 锁状态:synchronized无法判断,Lock可以isLocked()
6. volatile关键字的作用
- 保证变量的可见性:修改立即对其他线程可见
- 禁止指令重排序:通过内存屏障实现
- 不保证原子性:i++这样的复合操作仍然不是原子的
- 适用场景:状态标志、DCL单例模式等
7. 死锁
- 定义:两个或多个线程互相等待对方持有的锁,导致永久阻塞
- 产生条件:
- 互斥条件:资源同一时刻只能被一个线程使用
- 请求与保持:线程已获得资源又提出新请求
- 不剥夺条件:线程获得的资源只能由自己释放
- 循环等待:存在循环等待链
- 预防措施:
- 固定加锁顺序
- 使用tryLock()尝试获取锁
- 设置超时时间
- 使用死锁检测工具
// 死锁示例
public class DeadLockDemo {
private static Object resource1 = new Object();
private static Object resource2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized(resource1) {
System.out.println("线程1获取resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(resource2) {
System.out.println("线程1获取resource2");
}
}
}).start();
new Thread(() -> {
synchronized(resource2) {
System.out.println("线程2获取resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(resource1) {
System.out.println("线程2获取resource1");
}
}
}).start();
}
}
线程通信
8. 线程间通信的方式
- wait/notify机制
synchronized(obj) {
while(!condition) {
obj.wait(); // 释放锁并等待
}
// 条件满足,继续执行
obj.notify(); // 通知其他等待的线程
}
- Condition接口
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
while(!conditionMet) {
condition.await(); // 等待
}
// 条件满足,继续执行
condition.signal(); // 通知
} finally {
lock.unlock();
}
- BlockingQueue:线程安全的队列,用于生产者-消费者模式
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
// 生产者
queue.put("数据"); // 队列满时阻塞
// 消费者
String data = queue.take(); // 队列空时阻塞
- CountDownLatch:等待多个线程完成
CountDownLatch latch = new CountDownLatch(3);
// 线程中调用
latch.countDown(); // 计数减一
// 主线程等待
latch.await(); // 等待计数为0
- CyclicBarrier:让一组线程到达屏障时被阻塞,直到最后一个线程到达
CyclicBarrier barrier = new CyclicBarrier(3);
// 线程中调用
barrier.await(); // 等待所有线程到达
- Semaphore:控制同时访问特定资源的线程数量
Semaphore semaphore = new Semaphore(3); // 3个许可
semaphore.acquire(); // 获取许可
try {
// 访问资源
} finally {
semaphore.release(); // 释放许可
}
9. 生产者-消费者问题
public class ProducerConsumer {
private static final int CAPACITY = 10;
private static BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(CAPACITY);
public static void main(String[] args) {
// 生产者线程
new Thread(() -> {
try {
for (int i = 0; i < 100; i++) {
queue.put(i);
System.out.println("生产: " + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 消费者线程
new Thread(() -> {
try {
while (true) {
Integer value = queue.take();
System.out.println("消费: " + value);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
并发工具类
10. 常用的并发容器
- ConcurrentHashMap:线程安全的HashMap
- CopyOnWriteArrayList:线程安全的ArrayList,适用于读多写少
- CopyOnWriteArraySet:线程安全的Set,基于CopyOnWriteArrayList
- ConcurrentLinkedQueue:线程安全的无界队列
- BlockingQueue:接口,常用实现有ArrayBlockingQueue、LinkedBlockingQueue等
11. 线程池
-
核心参数:
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:空闲线程存活时间
- workQueue:工作队列
- threadFactory:线程工厂
- handler:拒绝策略
-
常用线程池:
- FixedThreadPool:固定大小线程池
- CachedThreadPool:可缓存线程池
- SingleThreadExecutor:单线程池
- ScheduledThreadPool:定时线程池
- WorkStealingPool:工作窃取线程池(JDK 8)
// 自定义线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(100), // 工作队列
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
- 拒绝策略:
- AbortPolicy:抛出异常(默认)
- CallerRunsPolicy:由调用线程执行任务
- DiscardPolicy:丢弃任务
- DiscardOldestPolicy:丢弃队列最前面的任务
12. CompletableFuture
- Java 8引入的异步编程工具,结合了Future和回调
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 异步任务
return "结果";
});
future.thenAccept(result -> {
// 处理结果
}).exceptionally(ex -> {
// 处理异常
return null;
});
并发设计模式
13. 单例模式(线程安全)
- 饿汉式:类加载时创建实例,线程安全
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
- 懒汉式(双重检查锁):延迟加载,线程安全
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
- 静态内部类:延迟加载,线程安全
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 method() {
// 方法实现
}
}
14. 不可变对象模式
- 所有字段都是final和private
- 不提供修改状态的方法
- 确保所有可变组件不会被修改
- 防止子类重写方法
public final class ImmutableClass {
private final int value;
private final List<String> list;
public ImmutableClass(int value, List<String> list) {
this.value = value;
// 创建副本,防止外部修改
this.list = new ArrayList<>(list);
}
public int getValue() {
return value;
}
public List<String> getList() {
// 返回副本,防止通过getter修改
return new ArrayList<>(list);
}
}
常见面试题
15. 如何确保线程安全?
- 使用同步机制:synchronized、Lock
- 使用线程安全的集合类:ConcurrentHashMap等
- 使用原子类:AtomicInteger等
- 使用volatile保证可见性
- 使用不可变对象
- 使用ThreadLocal隔离线程数据
- 使用并发工具类:CountDownLatch等
16. 如何避免死锁?
- 固定锁的获取顺序
- 减少锁的持有时间
- 使用tryLock()方法
- 使用定时锁
- 避免嵌套锁
- 使用死锁检测工具
17. ConcurrentHashMap的实现原理
- JDK 7:分段锁(Segment)机制,将数据分为多个段
- JDK 8:取消分段锁,使用CAS + synchronized实现
- 不允许null键和null值
- 高并发读操作无需加锁
- 写操作使用CAS + synchronized保证线程安全
18. volatile和synchronized的区别
- volatile只能修饰变量,synchronized可修饰方法和代码块
- volatile保证可见性和有序性,不保证原子性;synchronized保证原子性、可见性和有序性
- volatile不会造成线程阻塞,synchronized可能造成线程阻塞
- volatile适用于一个变量被多个线程读,但只有一个线程写的场景
19. ThreadLocal的原理和内存泄漏问题
- 原理:每个Thread维护一个ThreadLocalMap,key是ThreadLocal对象,value是线程本地变量
- 内存泄漏:ThreadLocalMap的key是弱引用,但value是强引用,如果ThreadLocal对象被回收,而Thread对象仍存活,会导致value无法被回收
- 解决方法:使用完ThreadLocal后调用remove()方法
20. AQS(AbstractQueuedSynchronizer)原理
- 是Java并发包中锁和同步器的基础框架
- 通过一个int类型的state变量表示同步状态
- 通过FIFO队列管理等待的线程
- 子类通过重写tryAcquire()和tryRelease()等方法实现具体同步逻辑
- ReentrantLock、CountDownLatch、Semaphore等都基于AQS实现
21. 线程池的工作原理
- 如果当前运行的线程数小于核心线程数,创建新线程执行任务
- 如果当前运行的线程数等于或大于核心线程数,将任务加入工作队列
- 如果工作队列已满,且运行的线程数小于最大线程数,创建新线程执行任务
- 如果工作队列已满,且运行的线程数等于最大线程数,执行拒绝策略
22. 如何合理配置线程池参数?
- CPU密集型任务:线程数 = CPU核心数 + 1
- IO密集型任务:线程数 = CPU核心数 * (1 + IO耗时/CPU耗时)
- 混合型任务:根据任务特性调整
- 考虑因素:任务类型、系统资源、任务优先级等
23. Java内存模型(JMM)
- 定义了线程和主内存之间的抽象关系
- 主内存:所有变量的主存储
- 工作内存:线程的本地内存,存储变量的副本
- 主要规则:
- 原子性:基本操作保证原子性,复合操作需要同步措施
- 可见性:一个线程修改变量后,其他线程能立即看到最新值
- 有序性:程序执行的顺序按照代码的先后顺序执行
24. 实现一个线程安全的计数器
public class ThreadSafeCounter {
// 方式一:使用AtomicInteger
private AtomicInteger atomicCount = new AtomicInteger(0);
public int incrementAndGetAtomic() {
return atomicCount.incrementAndGet();
}
// 方式二:使用synchronized
private int syncCount = 0;
public synchronized int incrementAndGetSync() {
return ++syncCount;
}
// 方式三:使用Lock
private int lockCount = 0;
private Lock lock = new ReentrantLock();
public int incrementAndGetLock() {
lock.lock();
try {
return ++lockCount;
} finally {
lock.unlock();
}
}
}
25. Fork/Join框架
- 用于并行执行任务,将大任务分割成小任务并行处理
- 工作窃取算法:空闲线程从其他线程队列中窃取任务执行
- 适用于计算密集型任务
public class ForkJoinExample extends RecursiveTask<Integer> {
private final int threshold = 10;
private int[] array;
private int start;
private int end;
public ForkJoinExample(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
if (end - start <= threshold) {
// 小任务直接计算
for (int i = start; i < end; i++) {
sum += array[i];
}
} else {
// 大任务分割
int middle = (start + end) / 2;
ForkJoinExample leftTask = new ForkJoinExample(array, start, middle);
ForkJoinExample rightTask = new ForkJoinExample(array, middle, end);
leftTask.fork(); // 异步执行左边任务
int rightResult = rightTask.compute(); // 同步执行右边任务
int leftResult = leftTask.join(); // 获取左边任务结果
sum = leftResult + rightResult;
}
return sum;
}
public static void main(String[] args) {
int[] array = new int[100];
for (int i = 0; i < array.length; i++) {
array[i] = i + 1;
}
ForkJoinPool pool = new ForkJoinPool();
ForkJoinExample task = new ForkJoinExample(array, 0, array.length);
int result = pool.invoke(task);
System.out.println("结果: " + result);
}
}
总结
Java多线程与并发是面试中的重点内容,掌握以上知识点将有助于应对大多数并发相关的面试题。关键是理解基本概念,熟悉常用工具类,并能分析常见的并发问题及其解决方案。
浙公网安备 33010602011771号