问题 1:请你解释一下 Java 中 “进程” 与 “线程” 的核心区别,以及为什么我们开发中更倾向于使用多线程而非多进程?

考察点

  • 对 “进程” 和 “线程” 本质概念的理解(是否明确两者在资源分配任务调度中的核心角色)。
  • 能否从 “资源开销”“通信效率”“调度成本” 三个关键维度对比差异,避免仅停留在 “进程是线程的容器” 这类表面描述。
  • 结合开发场景说明多线程的优势,体现 “理论联系实际” 的思维。

参考答案

        进程和线程是操作系统中任务执行的基本单位,核心区别体现在资源归属调度粒度上,具体对比及多线程的优势如下:

1. 核心区别(3 个关键维度)
对比维度 进程(Process) 线程(Thread)
资源分配单位 操作系统资源分配的最小单位(拥有独立的内存空间、CPU 寄存器、文件描述符等) 进程内的资源共享单位(共享进程的内存空间、文件描述符,仅拥有独立的程序计数器 PC、栈空间)
任务调度单位 操作系统调度的基本单位(调度粒度粗) 操作系统调度的最小单位(调度粒度细)
资源开销 (创建 / 销毁需分配 / 释放全套资源,内存占用大) 低(创建 / 销毁仅需操作独立栈和 PC,内存占用小)
通信方式 复杂(需依赖进程间通信 IPC 机制,如管道、Socket、共享内存) 简单(可直接访问进程共享内存,如通过 volatile、锁同步访问共享变量)
独立性 (一个进程崩溃不影响其他进程) (一个线程崩溃可能导致整个进程崩溃,因共享进程资源)
2. 开发中倾向用多线程的原因
  • 资源开销低:多线程共享进程资源,创建 / 销毁速度比多进程快 10-100 倍,适合高频创建销毁任务(如 Web 服务处理请求)。
  • 通信效率高:线程间可直接操作共享内存,无需额外 IPC 机制,减少数据拷贝开销(如电商系统中 “订单线程” 与 “库存线程” 共享商品库存变量)。
  • 调度响应快:线程调度粒度细,操作系统可在同一进程内快速切换线程,减少 CPU 空闲时间(如 UI 程序中 “界面渲染线程” 与 “数据加载线程” 并行,避免界面卡顿)。

指导

  • 回答时避免只罗列定义,需先明确 “进程是资源单位,线程是调度单位” 这一核心结论,再展开对比,逻辑更清晰。
  • 易混淆点:“线程共享进程内存”≠“线程间无需同步”—— 正因为共享资源,才需要 volatile、synchronized 等机制保证线程安全,这点需在回答中隐含,体现对多线程安全的认知。

问题 2:Java 线程有哪几种状态?请详细说明每种状态的含义,以及状态之间的转换条件(举例说明)?

考察点

  • 对 Java 线程状态模型(而非操作系统线程状态)的精准掌握,是否明确 JDK 定义的 6 种状态(避免混淆 “就绪”“运行” 等操作系统概念)。
  • 能否清晰梳理状态转换的 “触发动作” 与 “转换路径”,例如 “WAITING 状态如何回到 RUNNABLE 状态”。
  • 区分易混淆状态(如 BLOCKED vs WAITING),体现对线程阻塞原因的理解。

参考答案

        根据 JDK 源码(java.lang.Thread.State枚举),Java 线程分为6 种状态,状态转换需依赖特定 API 调用或系统事件,具体如下:

1. 6 种线程状态及含义
状态枚举 核心含义 典型场景
NEW(新建) 线程对象已创建(如new Thread()),但未调用start()方法,未与操作系统线程绑定 Thread t = new Thread();后,未执行t.start();
RUNNABLE(可运行)runnable 线程已调用start(),与操作系统线程绑定,处于 “就绪” 或 “运行” 状态(Java 层面不区分两者) 执行t.start()后,线程等待 CPU 调度(就绪)或正在 CPU 上执行(运行)
BLOCKED(阻塞) 线程因等待 synchronized 锁而阻塞(获取锁前的临时状态) 线程 A 进入synchronized块,线程 B 也尝试进入该块,线程 B 会进入 BLOCKED 状态
WAITING(无限等待)waiting 线程因调用无超时 API(如wait()join()),需等待其他线程显式唤醒才会恢复 线程调用object.wait()(无超时),或thread.join()(无超时)
TIMED_WAITING(计时等待)time_waiting 线程因调用有超时 API(如sleep(long)wait(long)),等待超时或被唤醒 线程调用Thread.sleep(1000),或object.wait(5000)
TERMINATED(终止) 线程执行完成(run()方法结束)或因异常退出,生命周期结束 线程run()方法执行完return,或执行中抛未捕获异常
2. 核心状态转换路径(举例)
  1. NEW → RUNNABLE:调用线程对象的start()方法(如t.start()),底层会调用操作系统create_thread,绑定内核线程。
  2. RUNNABLE → BLOCKED:线程尝试进入synchronized块 / 方法,但锁已被其他线程持有(如线程 B 抢线程 A 的 synchronized 锁)。
  3. RUNNABLE → WAITING:线程在synchronized块内调用object.wait()(无超时),会释放锁并进入 WAITING 状态。
  4. WAITING → RUNNABLE:其他线程调用同一objectnotify()/notifyAll(),唤醒该线程,线程重新竞争锁,获锁后回到 RUNNABLE。
  5. RUNNABLE → TIMED_WAITING:线程调用Thread.sleep(1000),无需释放锁,直接进入计时等待。
  6. TIMED_WAITING → RUNNABLEsleep(1000)超时,或其他线程调用thread.interrupt()(线程需处理 InterruptedException)。
  7. RUNNABLE → TERMINATED:线程run()方法正常执行完毕,或抛未捕获异常(如NullPointerException)导致线程终止。

指导

  • 易混淆点 1:BLOCKED仅因 “等 synchronized 锁”,而WAITING/TIMED_WAITING因 “等其他线程动作”(如唤醒、超时),需明确区分阻塞原因
  • 易踩坑点:调用start()后线程不会立即运行,而是进入 RUNNABLE 的 “就绪” 状态,等待 CPU 调度;直接调用run()方法不会创建新线程,仅在当前线程执行run()逻辑,状态始终是 RUNNABLE(非 NEW→RUNNABLE),需特别提醒。

问题 3Java 中创建线程的方式有哪几种?分别说明每种方式的实现步骤、优缺点,以及适用场景?

考察点

  • 对 “创建线程” 的全面掌握,是否涵盖 “基础方式”(Thread、Runnable)与 “进阶方式”(Callable、线程池),避免遗漏 Callable。
  • 能否深入分析每种方式的核心差异(如是否有返回值、是否能抛异常、是否支持线程复用),而非仅描述实现代码。
  • 结合实际开发场景(如 “需要任务结果”“高并发请求处理”)说明适用场景,体现技术选型能力。

参考答案

Java 创建线程的核心方式有4 种,本质可分为 “手动创建线程” 和 “线程池管理线程” 两类,具体实现、优缺点及场景如下:

1. 方式 1:继承 Thread 类
  • 实现步骤
    1. 自定义类继承Thread,重写run()方法(线程执行逻辑);
    2. 创建自定义类实例,调用start()方法启动线程
    • 代码示例: 
      class MyThread extends Thread {
          @Override
          public void run() {
              System.out.println("线程执行逻辑");
          }
      }
      // 启动
      MyThread t = new MyThread();
      t.start();
      
  • 优缺点
    • 优点:实现简单,直接调用start()即可启动。
    • 缺点:1. Java 单继承限制,自定义类继承 Thread 后无法再继承其他类;2. run()无返回值,无法获取线程执行结果;3. 无异常抛出声明,无法向上传递 checked 异常
  • 适用场景:简单的无返回值任务,且无需继承其他类(如简单的日志打印线程)。
2. 方式 2:实现 Runnable 接口
  • 实现步骤
    1. 自定义类实现Runnable接口,重写run()方法
    2. 创建Runnable实例,作为参数传入Thread构造器,调用Threadstart()
    • 代码示例:
      class MyRunnable implements Runnable {
          @Override
          public void run() {
              System.out.println("线程执行逻辑");
          }
      }
      // 启动
      Thread t = new Thread(new MyRunnable());
      t.start();
      
  • 优缺点
    • 优点:1. 规避单继承限制,自定义类可同时继承其他类;2. 可共享Runnable实例(如多个 Thread 传入同一 MyRunnable,共享实例变量)。
    • 缺点:1. run()仍无返回值无法抛 checked 异常;2. 启动需依赖 Thread 类,无独立启动方法。
  • 适用场景需共享资源的无返回值任务(如多线程卖票,共享 “剩余票数” 变量)
3. 方式 3:实现 Callable 接口(结合 FutureTask)
  • 实现步骤
    1. 定义类实现Callable<V>接口(V 为返回值类型),重写call()方法有返回值、可抛 checked 异常);
    2. 创建Callable实例,传入FutureTask<V>构造器(FutureTask 实现 RunnableFuture,可作为 Thread 参数);
    3. FutureTask传入 Thread 构造器,调用start();通过futureTask.get()call()返回值。
    • 代码示例:
      class MyCallable implements Callable<Integer> {
          @Override
          public Integer call() throws Exception {
              return 1 + 1; // 有返回值,可抛异常
          }
      }
      // 启动与获取结果
      FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
      Thread t = new Thread(futureTask);
      t.start();
      Integer result = futureTask.get(); // 阻塞等待结果,可处理InterruptedException
      
  • 优缺点
    • 优点:1. 有返回值(通过get()获取);2. call()可抛 checked 异常(调用get()时需捕获);3. 规避单继承限制。
    • 缺点:1. 实现较复杂;2. get()方法会阻塞当前线程,需注意超时控制(可用get(long timeout, TimeUnit unit))。
  • 适用场景:需获取任务执行结果的场景(如多线程计算数据后汇总结果,如统计多个订单的总金额)。
4. 方式 4:使用线程池(如 ThreadPoolExecutor)
  • 实现步骤
    1. 通过ThreadPoolExecutor构造器创建线程池(指定核心参数:核心线程数、最大线程数等);
    2. 调用线程池的submit(Runnable)submit(Callable<V>)提交任务;
    3. 任务执行后,通过Future获取结果(若为 Callable);线程池使用完后调用shutdown()关闭。
    • 代码示例:
      // 创建线程池(核心2,最大4,空闲线程存活10s,队列容量10)
      ExecutorService executor = new ThreadPoolExecutor(
          2, 4, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10)
      );
      // 提交Callable任务,获取Future
      Future<Integer> future = executor.submit(new MyCallable());
      Integer result = future.get();
      // 关闭线程池
      executor.shutdown();
      
  • 优缺点
    • 优点:1. 线程复用(避免频繁创建销毁线程,降低资源开销);2. 可统一管理线程(如控制并发数、任务排队、拒绝策略);3. 支持批量提交任务,简化线程管理
    • 缺点:1. 配置复杂(需合理设置核心参数,否则可能导致性能问题);2. 若未正确关闭,可能导致线程泄漏。
  • 适用场景高并发、多任务场景(如 Web 服务处理 HTTP 请求、批量处理数据库数据),是实际开发的首选方式(避免手动创建线程的资源浪费)。

指导

  • 易混淆点:“前 3 种是手动创建线程,第 4 种是线程池管理线程”—— 实际开发中几乎不使用前 3 种手动方式,而是优先用线程池,需强调线程池的 “线程复用” 核心优势。
  • 易踩坑点:FutureTask.get()会阻塞,若任务执行时间过长,可能导致当前线程卡死,建议使用带超时的get()方法;线程池提交 Runnable 任务时,submit()返回的 Futureget()结果为 null,需注意区分。

问题 4:synchronized 关键字的作用是什么?它有几种用法?底层是如何实现的(JDK1.6 及以后的锁升级机制)?

考察点

  • 对 synchronized 核心功能(原子性、可见性、有序性)的理解,是否明确其是 Java 中的 “内置锁”(隐式锁)。
  • 能否区分 synchronized 的三种用法(锁对象、锁方法、锁类),以及每种用法对应的 “锁对象” 是什么。
  • 对 JDK1.6 锁升级机制(偏向锁→轻量级锁→重量级锁)的掌握,体现对 synchronized 性能优化的理解(避免认为 synchronized 只是 “重量级锁”)。

参考答案

        synchronized 是 Java 的内置同步锁(隐式锁,无需手动释放锁),核心作用是保证多线程访问共享资源时的原子性、可见性、有序性,避免线程安全问题(如 i++ 的原子性问题)。

1. synchronized 的 3 种用法(及锁对象)

synchronized 的锁对象必须是 “引用类型”(不能是基本类型,如 int),具体用法如下:

用法分类 代码示例 锁对象(关键)
1. 同步代码块(锁对象)

synchronized (lockObj) {

// 共享资源操作(如 count++)

}

显式指定的lockObj(通常是thisstatic final Object,建议用独立对象避免锁竞争)
2. 同步实例(非静态)方法

public synchronized void add() {

// 共享资源操作

}

当前对象实例(this—— 锁的是 “对象”,不同实例间无锁竞争
3. 同步静态方法

public static synchronized void add() {

// 共享资源操作

}

当前类的 Class 对象(如MyClass.class)—— 锁的是 “类”,所有实例共享同一把锁
2. 底层实现(JDK1.6 锁升级机制)

        JDK1.6 前,synchronized 是 “重量级锁”(依赖操作系统的互斥量 Mutex,切换线程需从用户态到内核态,开销大);JDK1.6 为优化性能,引入锁升级机制(从低开销到高开销,按需升级,不可降级),核心是通过 “对象头中的 Mark Word” 存储锁状态,具体升级路径如下:

(1)无锁状态
  • 场景:对象未被任何线程锁定,Mark Word 存储对象哈希码、GC 分代年龄等信息。
(2)偏向锁(Biased Locking)
  • 核心目标:减少无竞争场景下的锁开销(单线程反复获取释放同一把锁时,无需 CAS 操作)。
  • 原理:
    1. 第一个线程获取锁时,通过 CAS 将 Mark Word 中的 “锁标志位” 设为 “偏向锁”,并记录当前线程 ID;
    2. 后续该线程再次获取锁时,无需 CAS,直接判断 Mark Word 中的线程 ID 是否为当前线程,即可快速获取锁;
    3. 释放锁时无需操作(仅当其他线程竞争时才会释放)。
  • 触发升级:当有其他线程尝试获取该锁时,偏向锁会升级为轻量级锁。
(3)轻量级锁(Lightweight Locking)
  • 核心目标:处理短时间的轻度竞争(线程交替执行,无长时间阻塞)。
  • 原理:
    1. 线程获取锁时,先将 Mark Word 复制到线程栈的 “锁记录(Lock Record)” 中,然后通过 CAS 尝试将 Mark Word 中的 “锁标志位” 设为 “轻量级锁”,并指向当前线程的锁记录;
    2. CAS 成功则获取锁;CAS 失败(说明有其他线程竞争),先自旋(循环 CAS 尝试获取锁,避免立即升级为重量级锁);
    3. 释放锁时,通过 CAS 将栈中保存的 Mark Word 恢复到对象头,成功则释放。
  • 触发升级:1. 自旋次数超过阈值(JDK1.6 默认 10 次,或自适应自旋);2. 有更多线程参与竞争(如 3 个线程抢锁),轻量级锁升级为重量级锁。
(4)重量级锁(Heavyweight Locking)
  • 核心目标:处理长时间、高竞争场景(线程需阻塞等待锁)。
  • 原理:
    1. 升级为重量级锁后,Mark Word 中的 “锁标志位” 设为 “重量级锁”,并指向操作系统的 “互斥量(Mutex)”;
    2. 未获取锁的线程会被阻塞(从用户态切换到内核态),放入锁的 “阻塞队列”;
    3. 持有锁的线程释放锁时,会唤醒阻塞队列中的线程,竞争锁的线程需重新进入内核态参与调度。
  • 特点:开销大(内核态切换),但适合长时间竞争场景。

指导

  • 易混淆点:synchronized 的 “锁对象” 决定了锁的范围 —— 同步实例方法锁this,同步静态方法锁Class对象,同步代码块锁显式指定的对象,需明确 “锁的是谁”,避免多线程竞争时锁失效(如多个线程用不同锁对象同步同一资源)。
  • 易踩坑点:JDK1.6 后的锁升级是 “单向的”(偏向→轻量→重量,不可降级),目的是 “按需优化性能”,而非所有场景下都是重量级锁,回答时需体现这一优化,避免让面试官觉得对 synchronized 的理解停留在旧版本。

问题 5:volatile 关键字的作用是什么?它能保证原子性吗?为什么?请举例说明。

考察点

  • 对 volatile 核心功能(内存可见性、禁止指令重排序)的精准理解,是否明确其与 synchronized 的功能差异(volatile 无原子性保证)。
  • 能否从 “Java 内存模型(JMM)” 的角度解释 volatile 的实现原理(如内存屏障),避免仅停留在 “表面作用”。
  • 通过具体案例(如 i++ 问题)说明 volatile 无法保证原子性,体现对 “原子操作” 定义的理解(不可分割的操作)。

参考答案

volatile 是 Java 中用于修饰实例变量或静态变量的关键字,核心作用是保证多线程间的内存可见性禁止指令重排序,但不能保证原子性,具体分析如下:

1. volatile 的两大核心作用
(1)保证内存可见性
  • 问题背景:JMM 中,线程操作变量时会先将主内存的变量加载到线程私有工作内存,修改后再刷新回主内存;若变量无 volatile 修饰,线程 A 修改后可能未及时刷新主内存,线程 B 读取的仍是旧值(内存不可见)。
  • volatile 的解决:volatile 修饰的变量,线程修改后会立即刷新到主内存,且其他线程读取时会直接从主内存加载(跳过工作内存缓存),确保多线程看到的是变量的最新值。
  • 示例场景:
    class VolatileDemo {
        private volatile boolean flag = false; // volatile修饰
        
        public void setFlag() {
            flag = true; // 线程A修改后立即刷主内存
        }
        
        public void doWork() {
            while (!flag) { // 线程B每次读取都从主内存获取最新值
                // 若flag无volatile,线程B可能一直读旧值,进入死循环
            }
            System.out.println("执行完成");
        }
    }
    
(2)禁止指令重排序
  • 问题背景:编译器或 CPU 为优化性能,会对无依赖关系的指令重排序(如int a=1; int b=2;可重排为int b=2; int a=1;);但在多线程场景下,重排序可能导致逻辑错误(如单例模式的双重检查锁问题)。
  • volatile 的解决:volatile 修饰的变量会在其前后插入内存屏障(Memory Barrier),禁止编译器和 CPU 对 volatile 变量相关的指令进行重排序,确保指令执行顺序与代码顺序一致。
  • 典型场景(单例模式双重检查锁):
    public class Singleton {
        // volatile修饰,禁止instance实例化指令重排序
        private static volatile Singleton instance; 
        
        private Singleton() {}
        
        public static Singleton getInstance() {
            if (instance == null) { // 第一次检查(无锁)
                synchronized (Singleton.class) { // 加锁
                    if (instance == null) { // 第二次检查(有锁)
                        // 若instance无volatile,new Singleton()可能重排序为:
                        // 1. 分配内存 → 2. 赋值instance → 3. 初始化对象
                        // 线程B可能拿到未初始化的instance(步骤2后步骤3前),调用时抛NPE
                        instance = new Singleton(); 
                    }
                }
            }
            return instance;
        }
    }
    
2. volatile 不能保证原子性的原因(举例说明)
(1)原子性的定义

        原子性是指 “一个操作或多个操作,要么全部执行且执行过程不被中断,要么全部不执行”(不可分割)。

(2)volatile 无法保证原子性的核心原因

        volatile 仅保证 “读写操作的可见性”,但复合操作(如 i++)并非原子操作——i++ 可拆分为 3 步:1. 读取 i 的当前值;2. 计算 i+1;3. 将结果写回 i。这 3 步间可能被其他线程插入操作,导致结果错误。

(3)示例验证(i++ 问题)
class VolatileAtomicDemo {
    private volatile int i = 0;
    
    // 线程不安全的自增方法(i++非原子)
    public void increment() {
        i++; // 拆分为:读i→算i+1→写i,3步非原子
    }
    
    public static void main(String[] args) throws InterruptedException {
        VolatileAtomicDemo demo = new VolatileAtomicDemo();
        // 10个线程,每个线程执行1000次increment
        ExecutorService executor = Executors.newFixedThreadPool(10);
        for (int j = 0; j < 10; j++) {
            executor.submit(() -> {
                for (int k = 0; k < 1000; k++) {
                    demo.increment();
                }
            });
        }
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
        System.out.println(demo.i); // 结果通常小于10000(如9876),证明无原子性
    }
}
  • 结果分析:若 volatile 能保证原子性,i 最终应为 10000(10 线程 ×1000 次);但实际结果小于 10000,因多个线程在 “读→算→写” 过程中相互干扰(如线程 A 读 i=100,线程 B 也读 i=100,均算为 101,写回后 i=101,而非 102)。
(4)解决原子性的方案
  • synchronized 修饰 increment () 方法(保证方法内操作原子);
  • java.util.concurrent.atomic.AtomicInteger(原子类,底层用 CAS 实现原子操作)。

指导

  • 易混淆点:“volatile 有可见性,所以有原子性”—— 这是常见误区,需明确 “可见性≠原子性”,复合操作的原子性需额外通过锁或原子类保证。
  • 易踩坑点:在单例模式双重检查锁中,若 instance 无 volatile 修饰,可能因指令重排序导致拿到未初始化的实例,这是面试高频考点,需熟练掌握其原理。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐