1.Java 的内存模型(JMM)介绍一下

Java 内存模型(JMM)是一套规范,主要为了屏蔽硬件差异。 它规定了所有的变量都存储在主内存中,每个线程有自己的工作内存,线程对变量的操作必须在工作内存中进行。

JMM 的核心是为了解决多线程并发下的原子性、可见性、有序性问题。 比如通过 volatile 关键字可以保证可见性和有序性,通过 synchronized 可以保证这三大特性。

同时 JMM 还定义了 Happens-Before 原则来辅助判断并发安全性。

2.java多线程是什么?需要注意什么?

Java 多线程就是在一个 Java 程序(进程)中,同时运行多个‘子任务’(线程)。 这些线程共享同一块内存空间(如堆内存),但每个线程有自己独立的程序计数器

核心作用:

  1. 提高效率: 充分利用多核 CPU。比如一边在后台下载文件,一边在前台响应用户操作。
  2. 异步处理: 把耗时的操作(如发邮件、写日志)扔给子线程去做,不卡主线程。

其次,关于使用时需要注意的问题,主要有三点:

  • 第一,最重要的是线程安全问题(Safety)。 因为多个线程共享内存,如果同时修改同一个变量(比如计数器),很容易出现数据不一致的情况。 所以我们在开发中必须保证操作的原子性、可见性和有序性,常用的手段是加锁(如 synchronizedReentrantLock)或者使用 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.使用多线程要注意哪些问题?

2.java多线程是什么?需要注意什么?

5.保证数据的一致性有哪些方案呢?

​ “在并发环境下保证数据一致性,通常有三种主流方案: 第一种是:事务管理 (Transaction Management) —— 数据库层面 这是最底层的保障。利用数据库的 ACID 特性(原子性、一致性、隔离性、持久性)。 比如转账操作,A 扣钱和 B 加钱必须在一个事务里,要么全部成功提交,要么全部失败回滚,确保数据最终是一致的。 第二种是:锁机制 (Locking Mechanisms) —— 代码/内存层面 也就是我们常说的悲观锁策略。 通过互斥访问来保证同一时刻只有一个线程能修改数据。 在 Java 中,我们可以使用 synchronized 关键字或者 ReentrantLock 来实现。这能确保多线程并发修改共享变量时的安全性。 第三种是:版本控制 (Version Control) —— 业务逻辑层面 也就是我们常说的乐观锁策略。 它假设冲突很少发生,所以不加锁,而是在更新数据时检查一下:“现在的版本号和我想修改时的版本号一致吗?” 如果一致就修改,不一致就重试。通常通过在数据库表中加一个 version 字段或使用时间戳来实现。”

方案核心思想典型实现适用场景
事务管理ACIDMySQL 事务 (@Transactional)强一致性的数据库操作 (如支付)
锁机制互斥 (悲观锁)synchronized, ReentrantLock写多读少,竞争激烈的内存操作
版本控制重试 (乐观锁)CAS, 数据库版本号字段读多写少,竞争不激烈的场景

6.线程的创建方式有哪些?

方式核心方法返回值异常处理继承限制评价
继承 Threadrun()只能 try-catch有 (单继承)简单但局限,很少用
实现 Runnablerun()只能 try-catch标准解耦写法
实现 Callablecall()有 (泛型)能抛出需要返回值时用
线程池----性能最高,生产必备

物流站 (线程池) 管理 快递员 (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() 方法给它发一个信号。 目标线程需要自己去响应这个信号:

  1. 如果它在运行中,可以通过 isInterrupted() 检查标志位,然后主动退出。
  2. 如果它在阻塞中(如 sleep),会抛出 InterruptedException,我们需要捕获这个异常,并在 catch 块中处理退出逻辑(通常需要再次重置中断状态)。 这样能保证线程在结束前有机会完成资源释放(如关闭文件),实现优雅停机。”

9.调用 interrupt 是如何让线程抛出异常的?

​ “interrupt() 方法本身只是设置了一个布尔类型的标志位,不会直接抛异常。 异常的抛出是由 sleep()wait() 这些阻塞方法内部实现的。 当线程处于这些阻塞方法中时,JVM 会检测到中断标志位变成了 true,于是它会做两件事:首先清除标志位(重置为 false),然后抛出 InterruptedException。 这样线程就会从阻塞状态中强制唤醒,进入 catch 块进行处理。”

10.Java线程的状态有哪些?

Java 线程的状态在 Thread.State 枚举中定义,总共有 6 种

  1. NEW (新建状态)
    • 线程被创建了出来(new Thread()),但还没有调用 start() 方法。
    • 此时它只是 Java 堆上的一个对象,操作系统里还没有真正的线程。
  2. RUNNABLE (运行/就绪状态)
    • 这是面试中最容易混淆的点。Java 中的 RUNNABLE 对应操作系统的 Ready(就绪) 和 Running(运行) 两种状态
    • 只要线程调用了 start(),无论它是在 CPU 上跑,还是在排队等 CPU 时间片,在 Java 里都叫 RUNNABLE
  3. BLOCKED (阻塞状态)
    • 特指: 线程正在等待获取 synchronized 锁(监视器锁)。
    • 场景: 别人拿着锁没释放,你在外面排队。
  4. WAITING (无限等待状态)
    • 线程正在“死等”另一个线程的通知,如果没有唤醒,它会一直等下去。
    • 场景: 调用了 Object.wait()(不带超时)、Thread.join()(不带超时)或 LockSupport.park()
  5. TIMED_WAITING (超时等待状态)
    • 线程在等待,但是带了超时时间,时间到了会自动醒来。
    • 场景: 调用了 Thread.sleep(time)Object.wait(time)Thread.join(time)
  6. 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有啥区别

维度BLOCKEDWAITING
状态含义阻塞 (抢锁失败)等待 (死等信号)
触发场景synchronizedwait(), 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 结合 Conditionawait()signal()。 相比之下,Condition 更加灵活,支持多个等待队列,能实现更精准的唤醒。 第三种是基于‘JUC 并发工具类’(实际开发最常用的方式)。 Java 封装了很多高级工具来简化通信。 比如我们要实现生产者-消费者模型,通常直接用 BlockingQueue(阻塞队列),它自动帮我们处理了队列满或空的阻塞逻辑,解耦效果最好。 另外还有 CountDownLatch 用于等待多个线程任务结束,或者 CyclicBarrier 用于多线程同步到达屏障点等。”

15.线程间通信方式有哪些?

16.如何停止一个线程?

8.如何停止一个线程的运行?

“在 Java 中停止线程,主要有 2 种 正确的方式,同时要避免 1 种 错误的方式: 1. 坚决避免使用 stop() 方法(错误方式) 虽然 Thread 类里有 stop() 方法,但它已经被废弃(Deprecated)了。 因为它太暴力,会立即终止线程并释放所有锁,可能导致数据只写了一半,破坏了数据的一致性,非常不安全。 2. 使用中断机制 interrupt()(核心标准方式) 这是最推荐的做法。我们通过调用目标线程的 interrupt() 方法给它发一个信号。

  • 如果线程在运行中:它需要自己去轮询检查 isInterrupted() 标志位,如果为 true 就主动退出。
  • 如果线程在阻塞中(如 sleep, wait):它会抛出 InterruptedException,我们捕获异常后停止任务即可。
  • 优势: 它把停止的控制权交给了线程自己,让线程有机会去清理资源(比如关闭文件),实现优雅停机

3. 使用 volatile 标志位(简单场景) 对于简单的循环任务,我们可以定义一个 volatile boolean flag = true。 线程循环读取这个变量,当外部把它置为 false 时,线程退出循环。

  • 缺点: 如果线程正卡在 sleepwait 状态,它无法读取标志位,就永远停不下来了。所以还是推荐用 interrupt。” ​

​ ​