在上面几篇文章中,我们深入刨析了常见的java面试知识点。如果说前面几篇文章如果是类似国服安琪拉,国服典韦定位的话,那么这一期的话可以定位是国服干将,国服司空震。因为国服安琪拉,国服典韦在低端局很好使,就好比我们低年限(三年以下)的面试一样,那么高端局(三年以上工作经验)还是需要一些手法的操作来拿到你的面试官一血(一面)甚至通关(斩获offer)。
一、什么是线程?(烂大街,但是面试中很多人会吃亏,建议详细看下本文)
这个问题看起来很简单,但是如果深究的话其实还是有很多挖掘点的,特别是大厂的面试,会经常有那种特别开放的提问,比如你对线程有什么看法,再或者就是有没有看过线程的源码。这种问题虽然平时开发的时候基本没什么用,但是一般这样问的时候,你是要有知识储备的。哪怕是有点工作年限很久的员工也没有去深入的了解过,那我接下来就开始解释线程在java中到底是什么。
官方语言:
线程是程序执行的一个最小的单元,是被系统独立调度和分派的基本单位
。
线程在定义上类似化学反应中的的原子,不可再分的基本微粒。但是其实在物理状态里面,原子可以分为原子核
+电子
,原子核
又可以分为中子
+质子
。同理线程也是一样,线程在程序中的定义是被系统独立调度和分派的最小单位,就是说不能再有其他的比线程更细腻的东西出现在进程中,但是从物理组成上看,线程由堆栈寄存器
和线程控制表TCB
组成,寄存器用来存储线程中的局部变量。其中的堆栈寄存器
和线程控制表TCB
类似原子钟的原子核
+电子
。所以为什么解释线程是被调度和分派的最小单位而不是物理存储的最小单位,进程中所有的操作都是由各个线程完成的,各个线程中的组成部分是一个整体,所以线程又可以被称为进程中的‘原子’。到这里已经可以完全理解线程的本质是什么,那么该如何优雅的回答面试中的你对线程有什么看法
这个问题呢?在这里可以提供一种思路,三段式术语。首先是官方或者业内对于该问题的一种官方描述,其次加入自己对该问题的一个深层次的理解,最好能抛出一些有价值的东西,最后做一个简单的总结。
那么可以提供一个基于博主自己理解并且多次运用到面试实战中的回答,获得多次面试官的肯定。
关于你对线程的理解可以如下回答:
- 线程是程序执行的一个最小单元,是被系统独立调度和分派的最小基本单位。
- 线程在程序中的作用类似化学反应中的原子,逻辑上不可再分,但是物理上是由堆栈寄存器和线程控制表TCB组成。线程是一种运行态,依附进程的存在而存在,是进程中的实体。进程中的线程存在竞争关系,共享进程资源。线程具有完整的生命周期,例如新建,就绪,运行,阻塞,销毁等,通过不同的状态来配合完成进程中的代码逻辑。
- 得益于线程之间的竞争,等待唤醒等操作,最终配合完成进程的所有指令。
二、 线程的四种常见的创建方式?(项目中不推荐单独创建,只有应届生
面试会提一下)
- 继承Thread类:创建一个继承自Thread类的子类,并重写run()方法。然后创建该子类的对象并调用start()方法来启动线程。
- 实现Runnable接口:创建一个实现了Runnable接口的类,并实现run()方法。然后创建该类的对象,并将其作为参数传递给Thread对象的构造函数,最后调用start()方法。
- 实现Callable接口:类似于Runnable接口,但它可以返回结果并且能够抛出异常。配合线程池使用。
- 使用线程池:(下一篇文章会详细讲解并提供美团线程池源码,业界大佬)
代码展示如下:
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 1.继承Thread类
MyThread1 myThread1 = new MyThread1();
myThread1.start();
// 2.实现Runnable接口
MyThread2 myThread2 = new MyThread2();
Thread thread2 = new Thread(myThread2);
thread2.start();
// 3.实现callable接口 配合线程池使用
MyThread3 myThread3 = new MyThread3();
ExecutorService ex = Executors.newFixedThreadPool(4);
// 提交任务并获取到对象
Future future = ex.submit(myThread3);
// 测试会阻塞直到任务完成
Object o = future.get();
System.out.println(o.toString());
}
}
class MyThread1 extends Thread{
@Override
public void run() {
System.out.println("当前线程:"+Thread.currentThread().getName());
}
}
class MyThread2 implements Runnable{
@Override
public void run() {
System.out.println("当前线程:"+Thread.currentThread().getName());
}
}
class MyThread3 implements Callable {
@Override
public Object call() throws Exception {
System.out.println("当前线程:"+Thread.currentThread().getName());
return Thread.currentThread().getName();
}
}
测试结果如下:
当前线程:Thread-0
当前线程:Thread-1
当前线程:pool-1-thread-1
pool-1-thread-1
三、 如何调度线程?(重点,涉及线程的拓扑图
)
讲到线程调度的话首先需要掌握所有的处理线程的方法,目前我列举一些方法并深入剖析一些用法,不熟悉的同学建议实操!
其实我一直的观点就是在背一些八股文的同时,一定要培养自己的实操能力,这个对于程序员的提升是很大的,理论是基础,在平时开发的时候可能用不到,但是对于有想往上走的同学,理论和实操缺一不可,八股文的好处只有当你真的成长为一名高级开发或者架构师的时候才会体现其中的好处,就像你学习数学到了巅峰的时候,掌握的公式原理越多,就越能悟出一些自己的东西甚至创造一些新东西,所以除了对线生活的面试,更建议自己实操,为自己以后的创业或者理想做准备。
-
start()
启动一个新的线程,并在新线程中调用run()方法。注意不能多次启动同一个线程对象。 -
run()
定义了线程的执行体。通常你不会直接调用这个方法,而是通过调用start()间接调用它。 -
sleep(long millis)
使当前正在执行的线程暂停指定的毫秒数,让其他线程有机会执行。这是一个静态方法。 -
join()
等待该线程死亡。调用某个线程的join方法会阻塞调用此方法的线程(通常是主线程),直到被调用join方法的线程运行结束。 -
interrupt()
中断线程。这并不会立即停止另一个线程,而是设置一个标志位表示该线程应该停止运行。被中断的线程可以通过检查这个标志位并采取适当的动作。 -
isInterrupted()
测试线程是否已经接收到中断请求。与静态方法Thread.interrupted()不同,该方法不会清除中断状态。 -
yield()
暂停当前正在执行的线程对象,允许其他具有相同优先级的线程获得执行的机会。这也是一个静态方法。 -
setPriority(int newPriority) 和 getPriority()
设置和获取线程的优先级。线程的优先级是一个整数,范围从1到10,默认值为5。 -
setName(String name) 和 getName()
设置和获取线程的名字。名字可以用来识别线程。 -
currentThread()
返回对当前正在执行的线程对象的引用。这也是一个静态方法。
序号 1-3 代码简单基础不在详细刨析,代码展示如下
:
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
MyThread1 myThread1 = new MyThread1();
// 1.start
myThread1.start();
}
}
class MyThread1 extends Thread{
// 2.run
@Override
public void run() {
try {
// 3.线程执行到这里会休眠一秒钟
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("当前线程:"+Thread.currentThread().getName());
}
}
序号4代码详解(重点,某书面试曾经深层挖掘过)
:
join是一个final方法,作用就是当你在线程1中调用线程2的方法,线程2会等待线程1执行完之后才会开始执行
,源码如图:
现在给定一些场景来进行join方法的详解。
场景:目前有一个线程的拓扑图,线程1和线程2并行,等待线程1和线程2都执行完之后才能执行线程3。
代码展示:
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 创建3个线程
Thread thread1 = new Thread(() -> {
try {
// 为了凸显线程1和线程2是并行的,在线程1中加入睡眠时间,此时一般情况是线程2先执行完,因为1还要等待1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("执行线程1");
});
Thread thread2 = new Thread(() -> {
System.out.println("执行线程2");
});
Thread thread3 = new Thread(() -> {
System.out.println("执行线程3");
});
thread1.start();
thread2.start();
// 使用join方法在这里阻塞main方法
// 先执行完1和2之后在启动线程3
thread1.join();
thread2.join();
thread3.start();
}
}
代码运行结果:
执行线程2
执行线程1
执行线程3
序号5-6刨析代码刨析
:
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 理解interrrupt需要分俩步
// 1.打断正常的线程
// 2.打断sleep的线程
// 1.开始解释打断正常的线程
Thread thread1 = new Thread(() -> {
System.out.println("线程1执行中.....");
while (true) {
// 查看打断的状态
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
System.out.println("线程1被打断,那就结束吧!");
break;
}
}
});
thread1.start();
// main线程3秒之后执行打断操作
Thread.sleep(3000);
System.out.println("在main线程开始执行interrupt操作");
thread1.interrupt();
// 2.开始解释打断sleep的线程
Thread thread2 = new Thread(() -> {
System.out.println("线程2 sleep 5秒.....");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
thread2.start();
// main线程3秒之后执行打断操作
Thread.sleep(3000);
System.out.println("在main线程开始执行interrupt操作");
thread2.interrupt();
System.out.println("查看此时线程2中打断状态:"+thread2.isInterrupted());
}
}
测试结果如下:
线程1执行中.....
在main线程开始执行interrupt操作
线程1被打断,那就结束吧!
线程2 sleep 5秒.....
在main线程开始执行interrupt操作
查看此时线程2中打断状态:false
Exception in thread "Thread-1" java.lang.RuntimeException: java.lang.InterruptedException: sleep interrupted
at Main.lambda$main$1(Main.java:36)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at Main.lambda$main$1(Main.java:34)
... 1 more
此时有俩个可以挖掘点(重点需要自己上手写代码体验
):
1.interrupt打断正常的线程时会将该线程的打断状态变成true,但是当遇到休眠的线程时不仅会抛出异常,并且不会更新打断状态。
2.引发的思考如何优雅的终止线程?使用两阶段终止模式
(重点中的重点,掌握可以将多线程的面试推向高潮,分水岭)
使用stop()
方法终止线程,这显然是不好的行为,因为一个线程调用了stop()
方法,那么这个线程就被彻底杀死了,如果该线程正在访问一些共享资源并且加了锁,那么stop()之后该线程将再也不能释放锁,其他线程也就无法访问那些共享资源了。使用System.exit()
方法,这也是错误的选择,这会终止整个程序!
两阶段终止模式流程图
:
 throws InterruptedException {
MonitorThread monitorThread = new MonitorThread();
monitorThread.start();
// 过五秒钟之后查看下
Thread.sleep(5000);
monitorThread.stop();
}
}
// 线程管理类
class MonitorThread {
// 监控线程
private Thread monitor;
public void start() {
monitor = new Thread(() -> {
// 死循环一直监控线程
while(true) {
// 获取当前线程
Thread thread = Thread.currentThread();
// 查看中断标记
boolean interrupted = thread.isInterrupted();
if(interrupted) {
System.out.println(thread.getName()+"线程已中断,执行中断后的逻辑...");
System.out.println(thread.getName()+"线程结束了!");
// 结束后推出循环
break;
}
try {
// 每隔1秒监控一次 可根据实际情况设置
Thread.sleep(1000);
System.out.println("线程监控中:"+thread.getName()+"正常");
} catch (InterruptedException e) {
// 打印异常
e.printStackTrace();
// 如果在sleep状态被打断的,中断标记为false,所以这里需要手动打上标记
thread.interrupt();
}
}
},"th1");
// 启动线程
monitor.start();
}
// 终止方法
public void stop() {
// 并不是真正的去结束,而是打断监控线程
monitor.interrupt();
}
}
运行结果如下:
线程监控中:th1正常
线程监控中:th1正常
线程监控中:th1正常
线程监控中:th1正常
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at MonitorThread.lambda$start$0(Main.java:35)
at java.lang.Thread.run(Thread.java:748)
th1线程已中断,执行中断后的逻辑...
线程结束了!
序号7-8代码刨析
:
线程的优先级随机性的特点,即便你设置了更高的优先级也不一定会执行,所以一般不推荐使用。相当于你不能完全掌控他,无法对线程进行完全调度,你让他往东他不一定往东的感觉,将在外军令有所不受。代码如下:
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("1");
});
Thread thread2 = new Thread(() -> {
System.out.println("2");
});
// 优先级1-10 但是不一定就是按照优先级来处理的 只能保证优先级高的会优先处理
thread1.setPriority(5);
thread1.start();
// 对于同一优先级的 让出正在执行的cpu 开始和其他的相同的优先级线程重新竞争
Thread.yield();
thread2.start();
}
}
序号9-10代码刨析
:
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
System.out.println("线程执行打印:"+Thread.currentThread().getName());
});
// 给线程设置名字
thread1.setName("测试线程1");
// 获取线程名字
System.out.println("main方法执行打印:"+thread1.getName());
thread1.start();
}
}
测试结果如下:
main方法执行打印:测试线程1
线程执行打印:测试线程1
四、wait、notify、notifyall(重点
,也是面试中最有可能让你手撕代码的地方)?
wait()
、notify()
和notifyAll()
是用于线程间通信的关键方法,它们都定义在Object类
中,因此任何对象都可以调用这些方法。这些方法必须在同步上下文中使用,即在synchronized
方法或块内部,以确保只有一个线程可以执行相应的代码段。
- wait()
作用:使当前正在执行的线程等待,直到另一个线程调用同一个对象上的notify()或notifyAll()方法。
注意:当一个线程调用wait()时,它会释放该对象的锁,并进入“等待”状态。这意味着其他线程可以获得这个锁并访问该对象。 - notify()
作用:唤醒在此对象监视器上等待的单个线程。如果有多个线程都在等待,则会选择其中一个线程来唤醒,但具体哪个线程被选中是不可预测的。
注意:调用notify()并不会立即释放对象的锁;锁只会在调用notify()的线程退出同步方法或同步块后才会释放。 - notifyAll()
作用:唤醒在此对象监视器上等待的所有线程。所有被唤醒的线程将竞争此对象的锁,最终只有一个线程能够获取到锁并继续执行。
应用场景:当你不确定哪个等待的线程需要首先处理资源时,或者有多个不同类型的等待条件时,使用notifyAll()更合适。
使用wait()
、notify()
和notifyAll()
来完成生产者-消费者模型代码模板(全文的精华,一定要把这一块的代码搞懂
):
**public class Main {
public static void main(String[] args) throws InterruptedException {
Buffer buffer = new Buffer();
Thread producerThread = new Thread(new Producer(buffer), "Producer");
Thread consumerThread = new Thread(new Consumer(buffer), "Consumer");
producerThread.start();
consumerThread.start();
}
}
class Buffer {
private String content;
private boolean available = false;
// 方法上的锁锁定的是对象,所以可以保证produce和下面的consume方法只有一个可以被访问
public synchronized void produce(String content) throws InterruptedException {
while (available) {
// 等待直到资源可用
wait();
}
this.content = content;
System.out.println(Thread.currentThread().getName() + " produced: " + content);
available = true;
// 通知所有等待的线程
notifyAll();
}
public synchronized String consume() throws InterruptedException {
while (!available) {
// 等待直到有内容可以消费
wait();
}
System.out.println(Thread.currentThread().getName() + " consumed: " + content);
available = false;
// 通知所有等待的线程
notifyAll();
return content;
}
}
class Producer implements Runnable {
private Buffer buffer;
public Producer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
buffer.produce("Item " + i);
// 模拟生产时间
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
class Consumer implements Runnable {
private Buffer buffer;
public Consumer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
buffer.consume();
// 模拟消费时间
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}**
测试结果如下:
Producer produced: Item 0
Consumer consumed: Item 0
Producer produced: Item 1
Consumer consumed: Item 1
Producer produced: Item 2
Consumer consumed: Item 2
Producer produced: Item 3
Consumer consumed: Item 3
Producer produced: Item 4
Consumer consumed: Item 4
使用wait()
、notify()
和notifyAll()
来完成三个不同线程交替打印A、B、C(辅助理解,曾经在某银行面试中考题
)
代码如下:
public class Main {
public static void main(String[] args) throws InterruptedException {
PrintManager manager = new PrintManager();
Thread threadA = new Thread(new Printer(manager, "A"), "Thread-A");
Thread threadB = new Thread(new Printer(manager, "B"), "Thread-B");
Thread threadC = new Thread(new Printer(manager, "C"), "Thread-C");
threadA.start();
threadB.start();
threadC.start();
}
}
class PrintManager {
private int state = 0; // 0: A, 1: B, 2: C
public synchronized void printA() throws InterruptedException {
while (state != 0) {
wait(); // 等待直到轮到A打印
}
System.out.println(Thread.currentThread().getName()+":A");
state = 1; // 轮到B打印
notifyAll(); // 唤醒所有等待的线程
}
public synchronized void printB() throws InterruptedException {
while (state != 1) {
wait(); // 等待直到轮到B打印
}
System.out.println(Thread.currentThread().getName()+":B");
state = 2; // 轮到C打印
notifyAll(); // 唤醒所有等待的线程
}
public synchronized void printC() throws InterruptedException {
while (state != 2) {
wait(); // 等待直到轮到C打印
}
System.out.println(Thread.currentThread().getName()+":C");
state = 0; // 轮到A打印
notifyAll(); // 唤醒所有等待的线程
}
}
class Printer implements Runnable {
private final PrintManager manager;
private final String letter;
public Printer(PrintManager manager, String letter) {
this.manager = manager;
this.letter = letter;
}
@Override
public void run() {
try {
for (int i = 0; i < 5; i++) { // 每个线程打印10次
switch (letter) {
case "A":
manager.printA();
break;
case "B":
manager.printB();
break;
case "C":
manager.printC();
break;
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
测试结果如下:
Thread-A:A
Thread-B:B
Thread-C:C
Thread-A:A
Thread-B:B
Thread-C:C
Thread-A:A
Thread-B:B
Thread-C:C
Thread-A:A
Thread-B:B
Thread-C:C
Thread-A:A
Thread-B:B
Thread-C:C
五、 浅谈面试中关于线程比较奇葩的问题?
因为考虑到有些面试官为了彰显自己的实力或者公司提供的是范文式的面试,来分享一些比较没用而且奇葩的问题:
- 什么是精灵线程?
精灵线程
也叫守护线程
,这样说大家就知道了吧。守护线程和用户线程是很基础的概念了。
守护线程是用来守护主线程的,我们创造的所有线程都叫做用户线程,如果主线程执行完了自己的代码,而用户线程没有执行完毕,这时主线程就会卡住,等待用户线程执行完毕再结束程序。代码中通过setDaemon(true)
将线程设置为守护线程。 - 什么是虚拟线程?
如果是使用的java21的版本,虚拟线程
可以挖掘出来很多有用的东西,它是java21的新特性,是一种轻量级的实现,可以高效的处理大量的并发任务,非常契合高并发的场景,但是为什么说它鸡肋呢,因为现在绝大部分的公司不可能使用这么高的版本,所以说只能作为加分项,锦上添花。 - 什么是协程?
协程
是java19提出的一种概念,在java21之后的虚拟线程
可以理解成协程
的一种实现方式。这里不过多描述,有兴趣的选手可以研究,或许可以在面试中起到意想不到的效果,属于那种高风险可能高回报的投资。 - java中线程是如何通信的?
这种问题就很宽泛,上面所有讲的内容都是java通信中的一种或者多种,所以回答这个的时候就把所有你知道的线程之间的调度方法全部回答,越详细越好。但是这种没有切入实际的问题其实意义不大,只能泛泛而谈。