1.Java 的内存模型(JMM)介绍一下
Java 内存模型(JMM)是一套规范,主要为了屏蔽硬件差异。
它规定了所有的变量都存储在主内存中,每个线程有自己的工作内存,线程对变量的操作必须在工作内存中进行。
JMM 的核心是为了解决多线程并发下的原子性、可见性、有序性问题。
比如通过 volatile 关键字可以保证可见性和有序性,通过 synchronized 可以保证这三大特性。
同时 JMM 还定义了 Happens-Before 原则来辅助判断并发安全性。
2.java多线程是什么?需要注意什么?
Java 多线程就是在一个 Java 程序(进程)中,同时运行多个‘子任务’(线程)。 这些线程共享同一块内存空间(如堆内存),但每个线程有自己独立的栈和程序计数器。

核心作用:
- 提高效率: 充分利用多核 CPU。比如一边在后台下载文件,一边在前台响应用户操作。
- 异步处理: 把耗时的操作(如发邮件、写日志)扔给子线程去做,不卡主线程。
其次,关于使用时需要注意的问题,主要有三点:
- 第一,最重要的是线程安全问题(Safety)。
因为多个线程共享内存,如果同时修改同一个变量(比如计数器),很容易出现数据不一致的情况。 所以我们在开发中必须保证操作的原子性、可见性和有序性,常用的手段是加锁(如
synchronized、ReentrantLock)或者使用JUC包下的原子类。 - 第二,是性能与资源消耗问题(Performance)。
线程不是越多越好。创建线程有内存开销,且线程间的上下文切换非常消耗 CPU 资源。 如果在生产环境中频繁手动创建线程,可能会导致 OOM 或 CPU 飙高。所以我们严禁手动
new Thread,而是必须使用线程池来管理和复用线程。 - 第三,是活跃性问题(Liveness)。 主要得防范死锁(Deadlock)。比如两个线程互相持有对方需要的锁不释放,程序就卡死了。这通常需要我们在设计时注意加锁的顺序。
总结来说,多线程能提升性能,但同时也引入了复杂性,必须通过合理的锁机制和线程池来驾驭它。”
3.java里面的线程和操作系统的线程一样吗?
“是的。在 JDK 1.2 之后,Java 采用的是 1:1 线程模型。 也就是说,我们 new Thread().start() 之后,JVM 会在底层调用操作系统的 pthread_create,真正创建一个内核级线程。所以 Java 线程的调度完全依赖于操作系统的调度器。” (注:Java 21 引入的虚拟线程是 M:N 模型,打破了这个规则,提一句会很加分)
4.使用多线程要注意哪些问题?
5.保证数据的一致性有哪些方案呢?
“在并发环境下保证数据一致性,通常有三种主流方案:
第一种是:事务管理 (Transaction Management) —— 数据库层面 这是最底层的保障。利用数据库的 ACID 特性(原子性、一致性、隔离性、持久性)。 比如转账操作,A 扣钱和 B 加钱必须在一个事务里,要么全部成功提交,要么全部失败回滚,确保数据最终是一致的。
第二种是:锁机制 (Locking Mechanisms) —— 代码/内存层面 也就是我们常说的悲观锁策略。 通过互斥访问来保证同一时刻只有一个线程能修改数据。 在 Java 中,我们可以使用 synchronized 关键字或者 ReentrantLock 来实现。这能确保多线程并发修改共享变量时的安全性。
第三种是:版本控制 (Version Control) —— 业务逻辑层面 也就是我们常说的乐观锁策略。 它假设冲突很少发生,所以不加锁,而是在更新数据时检查一下:“现在的版本号和我想修改时的版本号一致吗?” 如果一致就修改,不一致就重试。通常通过在数据库表中加一个 version 字段或使用时间戳来实现。”
| 方案 | 核心思想 | 典型实现 | 适用场景 |
|---|---|---|---|
| 事务管理 | ACID | MySQL 事务 (@Transactional) | 强一致性的数据库操作 (如支付) |
| 锁机制 | 互斥 (悲观锁) | synchronized, ReentrantLock | 写多读少,竞争激烈的内存操作 |
| 版本控制 | 重试 (乐观锁) | CAS, 数据库版本号字段 | 读多写少,竞争不激烈的场景 |
6.线程的创建方式有哪些?
| 方式 | 核心方法 | 返回值 | 异常处理 | 继承限制 | 评价 |
|---|---|---|---|---|---|
| 继承 Thread | run() | 无 | 只能 try-catch | 有 (单继承) | 简单但局限,很少用 |
| 实现 Runnable | run() | 无 | 只能 try-catch | 无 | 标准解耦写法 |
| 实现 Callable | call() | 有 (泛型) | 能抛出 | 无 | 需要返回值时用 |
| 线程池 | - | - | - | - | 性能最高,生产必备 |
物流站 (线程池) → 管理 → 快递员 (Thread) → 执行 → 订单 (Runnable)
1. 继承 Thread 类
- 实现: 定义一个类继承
Thread,重写run()方法。 - 缺点: Java 是单继承的,继承了
Thread就不能继承其他类了,耦合度太高,不太灵活。
2. 实现 Runnable 接口 (最常用)
- 实现: 定义一个类实现
Runnable,重写run()方法,然后传给Thread对象。 - 优点: 避免了单继承的局限性,而且适合多个线程处理同一份资源(资源共享)。
- 缺点:
run()方法没有返回值,也不能抛出 checked 异常
3. 使用线程池 (Executor 框架) (生产环境标准)
- 实现: 使用
Executors工具类或ThreadPoolExecutor创建线程池。 - 优点: 复用线程,避免频繁创建和销毁带来的性能开销,还能控制并发数。这是实际开发中唯一推荐的方式。”
7.怎么启动线程 ?
启动线程的通过Thread类的start()。
8.如何停止一个线程的运行?
“Java 中不能强制杀死线程,stop() 方法已经废弃。正确的方式是使用中断机制 (Interrupt)。 我们通过调用目标线程的 interrupt() 方法给它发一个信号。 目标线程需要自己去响应这个信号:
- 如果它在运行中,可以通过
isInterrupted()检查标志位,然后主动退出。 - 如果它在阻塞中(如 sleep),会抛出
InterruptedException,我们需要捕获这个异常,并在catch块中处理退出逻辑(通常需要再次重置中断状态)。 这样能保证线程在结束前有机会完成资源释放(如关闭文件),实现优雅停机。”
9.调用 interrupt 是如何让线程抛出异常的?
“interrupt() 方法本身只是设置了一个布尔类型的标志位,不会直接抛异常。 异常的抛出是由 sleep()、wait() 这些阻塞方法内部实现的。 当线程处于这些阻塞方法中时,JVM 会检测到中断标志位变成了 true,于是它会做两件事:首先清除标志位(重置为 false),然后抛出 InterruptedException。 这样线程就会从阻塞状态中强制唤醒,进入 catch 块进行处理。”
10.Java线程的状态有哪些?
Java 线程的状态在 Thread.State 枚举中定义,总共有 6 种。
- NEW (新建状态)
- 线程被创建了出来(
new Thread()),但还没有调用start()方法。 - 此时它只是 Java 堆上的一个对象,操作系统里还没有真正的线程。
- 线程被创建了出来(
- RUNNABLE (运行/就绪状态)
- 这是面试中最容易混淆的点。Java 中的 RUNNABLE 对应操作系统的 Ready(就绪) 和 Running(运行) 两种状态。
- 只要线程调用了
start(),无论它是在 CPU 上跑,还是在排队等 CPU 时间片,在 Java 里都叫RUNNABLE。
- BLOCKED (阻塞状态)
- 特指: 线程正在等待获取 synchronized 锁(监视器锁)。
- 场景: 别人拿着锁没释放,你在外面排队。
- WAITING (无限等待状态)
- 线程正在“死等”另一个线程的通知,如果没有唤醒,它会一直等下去。
- 场景: 调用了
Object.wait()(不带超时)、Thread.join()(不带超时)或LockSupport.park()。
- TIMED_WAITING (超时等待状态)
- 线程在等待,但是带了超时时间,时间到了会自动醒来。
- 场景: 调用了
Thread.sleep(time)、Object.wait(time)、Thread.join(time)。
- TERMINATED (终止状态)
- 线程的
run()方法执行完毕,或者抛出异常导致线程结束。线程一旦终止,就不能复生。
- 线程的
11.sleep 和 wait的区别是什么?
“这两者最大的区别在于对锁(Monitor)的处理方式不同。 1. 核心区别:是否释放锁 (最重要!)
sleep():是 Thread 类的方法。它只是让线程‘小憩’一会儿,抱着锁睡觉。也就是说,线程暂停执行,但不会释放它持有的锁,别的线程依然进不来。wait():是 Object 类的方法。它是让线程‘等待’。调用后,线程会主动释放锁,并进入等待池(Wait Set),让出资源给其他线程执行。 2. 使用位置不同sleep():可以在任何地方使用。wait():必须在 synchronized 同步代码块或同步方法中使用。因为它涉及到锁的操作(释放锁、重新抢锁),如果不加锁直接调wait(),会抛出IllegalMonitorStateException异常。 3. 唤醒方式不同sleep(time):时间到了自动醒来,或者被中断(interrupt)。wait():如果没设置超时时间,它会一直死等,必须依靠其他线程调用同一个对象的notify()或notifyAll()才能被唤醒。 4. 归属类不同sleep定义在java.lang.Thread类中(静态方法)。wait定义在java.lang.Object类中(成员方法,所有对象都能调)。 总结一句话:sleep是抱着锁睡觉(不释放资源),而wait是交出锁等待(释放资源)。”
| 特性 | Thread.sleep() | Object.wait() |
|---|---|---|
| 锁的处理 | 不释放锁 | 释放锁 |
| 使用范围 | 任何地方 | 只能在 synchronized 中 |
| 唤醒机制 | 时间到自动醒 | 需别人 notify 或时间到 |
| 来源 | Thread 静态方法 | Object 成员方法 |
| 状态转换 | 变为 TIMED_WAITING | 变为 WAITING (或 TIMED_WAITING) |
| |
sleep会释放cpu吗?
“会释放 CPU,但是不会释放锁。 调用 sleep() 后,线程会让出 CPU 时间片,进入超时等待状态,让其他线程有机会执行。 但要注意,如果该线程持有 synchronized 锁,它在睡眠期间依然持有锁,其他需要该锁的线程会被阻塞,无法执行。”
12.blocked和waiting有啥区别
| 维度 | BLOCKED | WAITING |
|---|---|---|
| 状态含义 | 阻塞 (抢锁失败) | 等待 (死等信号) |
| 触发场景 | synchronized | wait(), join(), park() |
| 主动性 | 被动 (想进进不去) | 主动 (自己让出 CPU) |
| 特例 | 仅限 Java 内置锁 (Monitor) | 包含 JUC 锁 (ReentrantLock) |
| |
13. wait 状态下的线程如何进行恢复到 running 状态?
[WAITING]
| (被 notify/notifyAll 唤醒)
v
[BLOCKED] <-- 正在和别人抢锁 (Entry List)
| (抢到锁了 Monitor Acquired)
v
[RUNNABLE] <-- 等待 CPU 调度
| (获得 CPU 时间片)
v
[RUNNING] <-- 执行 wait() 后面的代码
notify 和 notifyAll 的区别?
notify:随机唤醒等待池中的一个线程。被唤醒的那个去抢锁,其他的继续睡。notifyAll:唤醒等待池中的所有线程。所有线程一起进入锁池去竞争锁(会发生锁竞争),抢到锁的执行,抢不到的在 BLOCKED 状态排队
notify 选择哪个线程?
notify在源码的注释中说到notify选择唤醒的线程是任意的,但是依赖于具体实现的jvm。 JVM有很多实现,比较流行的就是hotspot,hotspot对notofy()的实现并不是我们以为的随机唤醒,,而是“先进先出”的顺序唤醒。
14.**不同的线程之间如何通信?
第一种是基于‘共享变量’(最基础的方式)。 因为同一进程下的多个线程是共享堆内存的,所以它们可以直接通过读写同一个变量来交换信息。 不过为了保证安全,我们通常需要配合 volatile 关键字来保证可见性,或者使用 synchronized 加锁来保证原子性。
第二种是基于‘等待/通知机制’(控制顺序的方式)。 这就好比线程之间互相打电话。 最经典的是使用 Object 类的 wait() 和 notify(),或者使用 ReentrantLock 结合 Condition 的 await() 和 signal()。 相比之下,Condition 更加灵活,支持多个等待队列,能实现更精准的唤醒。
第三种是基于‘JUC 并发工具类’(实际开发最常用的方式)。 Java 封装了很多高级工具来简化通信。 比如我们要实现生产者-消费者模型,通常直接用 BlockingQueue(阻塞队列),它自动帮我们处理了队列满或空的阻塞逻辑,解耦效果最好。 另外还有 CountDownLatch 用于等待多个线程任务结束,或者 CyclicBarrier 用于多线程同步到达屏障点等。”
15.线程间通信方式有哪些?
16.如何停止一个线程?
“在 Java 中停止线程,主要有 2 种 正确的方式,同时要避免 1 种 错误的方式:
1. 坚决避免使用 stop() 方法(错误方式) 虽然 Thread 类里有 stop() 方法,但它已经被废弃(Deprecated)了。 因为它太暴力,会立即终止线程并释放所有锁,可能导致数据只写了一半,破坏了数据的一致性,非常不安全。
2. 使用中断机制 interrupt()(核心标准方式) 这是最推荐的做法。我们通过调用目标线程的 interrupt() 方法给它发一个信号。
- 如果线程在运行中:它需要自己去轮询检查
isInterrupted()标志位,如果为true就主动退出。 - 如果线程在阻塞中(如
sleep,wait):它会抛出InterruptedException,我们捕获异常后停止任务即可。 - 优势: 它把停止的控制权交给了线程自己,让线程有机会去清理资源(比如关闭文件),实现优雅停机。
3. 使用 volatile 标志位(简单场景) 对于简单的循环任务,我们可以定义一个 volatile boolean flag = true。 线程循环读取这个变量,当外部把它置为 false 时,线程退出循环。
- 缺点: 如果线程正卡在
sleep或wait状态,它无法读取标志位,就永远停不下来了。所以还是推荐用interrupt。”