JavaEE必会面试题,从线程讲到线程安全,一文带你通过多线程面试 📅 2026/6/27 6:14:00 目录一、讲讲线程与进程的联系二、线程创建的五种写法三、产生线程安全问题的五大原因四、线程的状态有哪几种五、死锁产生的四个必要条件[背下来]六、如何避免死锁七、请你写一个懒汉模式线程安全的代码并解释你这样写的原因你的回答从这个专题开始我将开始讲解一些Java后端面试准备过程中必须具备的素养与能力废话不多说今天主要学习的是Java多线程编程中最最重要的基础面试题也是工作中最可能用到的内容。一、讲讲线程与进程的联系首先解释定义即进程是计算机运行起来的应用程序如下图线程是轻量级进程和进程的关系是一个进程包含多个线程。他们之间的区别重点记忆1. 每个进程有自己独立的资源进程与进程间不能共享而同一个进程的线程之间共享相同的资源。2. 进程之间不会相互影响进程挂了不会影响其他进程而同一个进程的某个线程挂了有可能把其他线程带走。3. 进程之间通常不会有“资源冲突”的情况而同一个进程的线程之间特别容易产生资源访问冲突。4. 进程是操作系统资源分配的基本单位线程是操作系统调度执行的基本单位二、线程创建的五种写法1. 继承Thread重写run方法run方法是多线程代码执行的入口由start调用package thread; // 创建一个新的类, 让这个类继承标准库的 Thread 类 class MyThread extends Thread { // 重写父类的 run 方法. Override public void run() { while (true) { System.out.println(hello thread); // 休息 1000ms 1s try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } // Demo 例子~~ Example Sample Tutor 入门的例子 public class Demo1 { public static void main(String[] args) throws InterruptedException { // 1. 创建 Thread 的实例 // MyThread t new MyThread(); Thread t new MyThread(); Thread t2 new MyThread(); Thread t3 new MyThread(); // 2. 启动线程 t.start(); t2.start(); t3.start(); // t.run(); while (true) { System.out.println(hello main); Thread.sleep(1000); } } }2. 实现Runnable接口搭配Threadpackage thread; import java.util.Scanner; class MyRunnable implements Runnable { Override public void run() { while (true) { System.out.println(hello thread); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class Demo2 { public static void main(String[] args) throws InterruptedException { MyRunnable r new MyRunnable(); Thread t new Thread(r); t.start(); while (true) { System.out.println(hello main); Thread.sleep(1000); } } }3. 继承Thread使用匿名内部类package thread; public class Demo3 { public static void main(String[] args) throws InterruptedException { Thread t new Thread() { Override public void run() { while (true) { System.out.println(hello thread); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }; // 记住, 调用 start 才是真正创建线程. t.start(); while (true) { System.out.println(hello main); Thread.sleep(1000); } } }4. 继承Runnable使用匿名内部类package thread; public class Demo4 { public static void main(String[] args) throws InterruptedException { // Runnable runnable new Runnable() { // Override // public void run() { // // } // }; // Thread t new Thread(runnable); Thread t new Thread(new Runnable() { Override public void run() { while (true) { System.out.println(hello thread); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }); t.start(); while (true) { System.out.println(hello main); Thread.sleep(1000); } } }5. Lambda表达式最常用package thread; public class Demo5 { public static void main(String[] args) throws InterruptedException { Thread t new Thread(() - { while (true) { System.out.println(hello thread); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); t.start(); while (true) { System.out.println(hello main); Thread.sleep(1000); } } }三、产生线程安全问题的五大原因1.根本原因操作系统对于线程的调度是随机的这个我们改不了2.两个线程对同一个变量进行修改操作不是常见的Java应对方式2.1 一个线程针对一个变量进行修改 没问题2.2 两个线程针对不同变量进行修改 没问题2.3 两个线程针对同一个变量进行读取 没问题针对2.1-不使用多线程 单线程 没法充分利用多核 CPU 资源~针对2.2-不使用同一个变量, 每个线程搞一个变量 (比较吃的需求和逻辑的)3.修改操作不是原子的-解决方式使用锁比如synchronized4.内存可见性问题由于编译器优化产生的线程安全问题可以从JMM模型主内存和工作内存或者硬件的角度去给面试官解释一句话“从寄存器或者缓存中读数据而不是从内存中读数据”--解决使用volatile5.指令重排序问题只有单例模式一个孤证后面会讲有关懒汉模式的线程安全的代码四、线程的状态有哪几种1宏观上理解分为阻塞态和就绪态2微观上理解分为NEW创建了线程但是还没有start()TERMINATED线程结束了但是Thread对象还没有销毁RUNNABLE线程正在CPU上运行或者正在就绪队列中即将被操作系统调度到CPU上执行WAITING死等--join()TIMED_WATING带有超时时间的等待--join(填写毫秒)BLOCKED与锁有关的阻塞五、死锁产生的四个必要条件[背下来]1. 锁是互斥的--同一时刻只能有一个线程拿到锁2. 锁是不可被抢占的1、2对于synchronized是改不了的3. 请求和保持在已经持有一把锁且还未释放的前提下又去申请另一把锁典型的“吃着碗里的想着锅里的”4. 循环等待 / 环路等待类比车钥匙锁家里家钥匙锁车里六、如何避免死锁1. 打破请求和保持避免“锁的嵌套”2. 打破循环等待约定好一个加锁的顺序这里附上一个典型的示例代码如下package thread; //5.多线程3 解决典型死锁--打破循环等待--按照一定的顺序来加锁即可 public class Demo18_2 { private static void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { throw new RuntimeException(e); } } public static void main(String[] args) { Object locker1 new Object(); Object locker2 new Object(); Thread t1 new Thread(() - { synchronized (locker1) { System.out.println(t1 获取到 locker1); sleep(1000); synchronized (locker2) { System.out.println(t1 获取到 locker2); } } }); Thread t2 new Thread(() - { synchronized (locker1) { System.out.println(t2 获取到 locker1); sleep(1000); synchronized (locker2) { System.out.println(t2 获取到 locker2); } } }); t1.start(); t2.start(); } }七、请你写一个懒汉模式线程安全的代码并解释你这样写的原因package thread; // 懒汉的单例模式 class SingletonLazy { private volatile static SingletonLazy instance null; // 作为要加锁的对象. 由于是要在 static 方法中使用锁对象, 对象本身也得是 static 的. private static Object locker new Object(); // 懒汉模式的关键在于, 把实例的创建时机推迟了, 推迟到第一次使用的时候, 创建. public static SingletonLazy getInstance() { // 这个条件判定是否需要加锁 if (instance null) { synchronized (locker) { // 判定是否需要创建实例 if (instance null) { instance new SingletonLazy(); } } } return instance; } private SingletonLazy() { } } public class Demo25 { public static void main(String[] args) { SingletonLazy s1 SingletonLazy.getInstance(); SingletonLazy s2 SingletonLazy.getInstance(); System.out.println(s1 s2); } }你的回答“面试官你好这个懒汉模式的线程安全代码的要点有这么三点第一使用synchronized保证判断和赋值操作在一起是原子的保证线程安全第二使用双重if内层if用来判断是否需要创建实例而外层if判断用来判断是否需要加锁因为加锁涉及到开销和效率问题第三为instance加上volatile关键字来防止因为指令重排序导致的线程安全问题”特别注意懒汉模式只有在第一次创建实例之前会产生线程安全问题一旦创建实例成功了线程安全问题就消失了所以为了避免反复加锁带来的开销需要在synchronized的外层也加上一个if判断语句