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. 线程池的工作原理

  1. 如果当前运行的线程数小于核心线程数,创建新线程执行任务
  2. 如果当前运行的线程数等于或大于核心线程数,将任务加入工作队列
  3. 如果工作队列已满,且运行的线程数小于最大线程数,创建新线程执行任务
  4. 如果工作队列已满,且运行的线程数等于最大线程数,执行拒绝策略

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多线程与并发是面试中的重点内容,掌握以上知识点将有助于应对大多数并发相关的面试题。关键是理解基本概念,熟悉常用工具类,并能分析常见的并发问题及其解决方案。