Java面试题:线程专题

封面:Java线程专题.png

王有志,一个分享硬核Java技术的互金摸鱼侠
加入Java人的提桶跑路群:共同富裕的Java人

平时我在网上冲浪的时候,收集了不少八股文和面试文,内容虽然多,但质量上良莠不齐,主打一个不假思索的互相抄,使得很多错误内容一代代得“传承”了下来。所以,我对收集的内容做了归纳和整理,通过查阅资料重新做了解答,并给出了每道八股文评分。
数据来源:

  • 大部分来自于各机构(Java之父,Java继父,某灵,某泡,某客)以及各博主整理文档;
  • 小部分来自于我以及身边朋友的实际经历,题目上会做出标识,并注明面试公司。

叠“BUFF”:

  • 八股文通常出现在面试的第一二轮,是“敲门砖”,但仅仅掌握八股文并不能帮助你拿下Offer;
  • 由于本人水平有限,文中难免出现错误,还请大家以批评指正为主,尽量不要喷~~
  • 本文及历史文章已经完成PDF文档的制作,提取关键字【面霸的自我修养】。

概念篇

这部分是并发编程中的基础概念和理论基础,整体难度较低,并且当你有了一定的工作年限后,很少会涉及这类问题,大家以了解为主。

并发与并行

难易程度:🔥

重要程度:🔥

公司:无

并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
并行,在操作系统中是指,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。对比地,并发是指:在同一个时间段内,两个或多个程序执行,有时间上的重叠(宏观上是同时,微观上仍是顺序执行)
并发在宏观上是同时执行,但微观上是交替执行,而并行无论是宏观还是微观,都是同时执行。
Tips:打个比方,并行像是打开了两盏灯,它们同时处于亮起的状态;而并发就是一盏灯,肉眼看起来是“常亮”状态,但实际在交流电的作用下,灯一直再闪烁,只是肉眼无法观察到。


同步与异步

难易程度:🔥

重要程度:🔥

公司:无

同步:同步,可以理解为在通信时、函数调用时、协议栈的相邻层协议交互时等场景下,发信方与收信方、主调与被调等双方的状态是否能及时保持状态一致。如果一方完成一个动作后,另一方立即就修改了自己的状态,就是同步。
异步:是指调用方发出请求就立即返回,请求甚至可能还没到达接收方,比如说放到了某个缓冲区中,等待对方取走或者第三方转交;而调用结果是通过接收方主动推送,或调用方轮询来得到。


阻塞与非阻塞

难易程度:🔥

重要程度:🔥

公司:无

阻塞与非阻塞指的是程序在等待调用结果时的状态。
阻塞(Blocking):被调用时,线程会被挂起/暂停/阻塞,直到该操作完成,返回结果后再执行后续操作。此时,程序无法进行其它操作,会一直等到调用结果返回。
非阻塞(Non-blocking):被调用时,即便操作尚未完成和拿到结果,线程也不会被挂起/暂停/阻塞,程序可以继续执行后序操作。


线程与进程

难易程度:🔥

重要程度:🔥

公司:无

进程(process),曾经是分时系统的基本运作单位。在面向进程设计的系统中,是程序的基本执行实体;在面向线程设计的系统中,进程本身不是基本执行单位,而是线程的容器
线程(thread),在计算机科学中,是将进程划分为两个或多个线程(实例)或子进程,由单处理器(单线程)或多处理器(多线程)或多核处理系统并发执行。
进程与线程之间的差别:

进程 线程
进程拥有自己的内存空间,文件句柄,系统信号和环境变量等 所有线程共享进程的资源,包括内存空间,文件句柄,系统信号等
进程是独立的执行单元,拥有自己的堆栈空间,需要使用进程间通信机制进行数据交换 线程是进程内部的执行单元,共享进程的地址空间,可以直接访问进程的全局变量和堆空间
进程间切换开销较大。进程间的切换比线程间的切换耗时和开销都大得多,因为进程切换需要保存和恢复更多的状态信息,如内存映像、文件句柄、系统信号等 线程间切换开销较小。线程的切换只需要保存和恢复少量的寄存器和堆栈信息
进程间资源隔离明显,进程间安全性较高 线程间共享资源,容易引起竞态条件,线程间安全性较低

并发编程的3要素

难易程度:🔥🔥

重要程度:🔥🔥🔥

公司:无

原子性:指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行;
可见性:软件工程中,是指对象间的可见性,含义是一个对象能够看到或者能够引用另一个对象的能力;
有序性:有序性是指对于多个线程或进程执行的操作,其执行顺序与程序代码中的顺序保持一致或符合预期的规则。
Tips:未在维基百科和百度百科中查找到有序性的解释,这里采用了ChatGPT的解释。


线程饥饿

难易程度:🔥🔥

重要程度:🔥

公司:无

线程饥饿(Thread Starvation),指的是在多线程的竞争环境中,某个线程长时间无法获取所需资源,或长时间无法得到调度,导致任务无法完成的状态。
常见产生的线程饥饿的原因如下:

  • 资源竞争,多个线程竞争必须资源,某个线程长时间无法获取到资源;
  • 线程优先级,线程优先级设置不当,导致优先级较低的线程长时间无法得到调度;
  • 锁竞争,同资源竞争,只不过此时竞争的是保护资源的锁。

上下文切换

难易程度:🔥

重要程度:🔥

公司:无

多个线程共用同一个CPU时,CPU时间从一个线程切换到另一个线程的过程。在这个过程中,需要保存线程的上下文信息(如:程序计数器,寄存器状态,堆栈指针等),同时加载另一个线程的上线文信息,使得系统能够正确执行。
Tips


🔥死锁及解决死锁

**难易程度:**🔥🔥🔥

**重要程度:**🔥🔥🔥🔥🔥

**公司:**苏宁,质数金融,网易

死锁(deadlock),当两个以上的运算单元,双方都在等待对方停止执行,以获取系统资源,但是没有一方提前退出时,就称为死锁。
形成死锁需要4个条件:

  • 互斥条件(Mutual Exclusion):资源只能被一个进程/线程占用。当一个进程/线程获取了资源,其他进程/线程无法访问该资源,只能等待资源被释放;
  • 请求与保持条件(Hold and Wait):进程/线程在持有资源的同时,继续请求其他资源,并不释放持有的资源;
  • 不剥夺条件(No Preemption):已经被持有的资源不能被强制剥夺,只有进程/线程主动释放后才能被其他进程/线程获取;
  • 循环等待条件(Circular Wait):存在一组进程/线程,互相请求彼此所持有的资源,线程了循环等待的环路。

图1:死锁的形成.png
解决死锁问题的核心是打破4项条件其中的一项即可:

  • 破坏互斥条件:允许资源被同时访问;
  • 破坏请求与保持条件:申请其它资源前,进行/线程需要释放当前资源,避免阻塞其它线程;
  • 破坏不可剥夺条件:允许优先级较高的进程/线程,强制剥夺其它线程持有的资源;
  • 破坏循环等待条件:对资源进行编号,强制获取资源时按照编号顺序进行获取。

🔥线程通信

**难易程度:**🔥🔥🔥🔥

**重要程度:**🔥🔥🔥🔥

**公司:**有利网

并发编程领域常见的2个线程间通信模型:共享内存和消息传递
共享内存:指的是多个线程运行在不同核心上,任何核心缓存上的数据修改后,刷新到主内存后,其他核心更新自己的缓存。
图2:共享内存.png
消息传递:多个线程可以通过消息队列进行通信,线程可以将消息发送到队列中,其他线程可以从队列中获取消息并进行处理。
传统面向对象编程语言通常会采用共享内存的方式进行线程间的通信,如Java,C++等。但Java可以通过Akka实现Actor模型的消息传递。Golang则是消息传递的忠实拥趸,《Go Proverbs》中第一句便是:

Don’t communicate by sharing memory, share memory by communicating.


🔥多线程优势与挑战

**难易程度:**🔥🔥

**重要程度:**🔥🔥

**公司:**苏宁

运用多线程的根本原因是“压榨”硬件性能,提高程序效率

  • 发挥多核CPU的优势:多核CPU下,单线程程序同一时间只会使用一个核心,其余核心处于空闲状态,造成了资源的浪费;
  • 提高系统资源的用率:即便是单核场景下,程序在等待响应或文件IO操作时CPU长时间空闲,此时利用多线程可以充分利用CPU。

但引入多线程也带来了一些挑战:

  • 上下文切换:具体请参考上文;
  • 死锁:具体请参考上文;
  • 资源限制:过多的线程会消耗大量的内存,以及产生更多的CPU竞争,会影响程序的性能。设计时应该根据硬件和程序合理的控制线程数量;
  • 线程安全问题:如果对共享资源进行并发访问,可能会造成数据一致性问题,或其他意料之外的结果。
  • 编程难度的提升:因为线程是并发执行的,并存在不确定行为,如:线程的执行顺序导致结果的差异,这种情况会造成开发与调试的困难。

线程安全

**难易程度:**🔥🔥

**重要程度:**🔥🔥

**公司:**无

线程安全:指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的公用变量,使程序功能正确完成。
通俗点可以理解为,程序在多线程环境中与单线程环境中的执行结果一致。
Tips:这与JMM中提到的终极目标as-if-serial语义稍有差别,as-if-serial语义强调无论如何重排序,单线程场景下的语义不能被改变(或者说执行结果不变)。

原理篇

接下来是Java原理篇,主要是关于Java中线程,Thread类,Runnable接口,Callable接口以及Future接口的内容。

Java中的线程

**难易程度:**🔥🔥🔥🔥

**重要程度:**🔥🔥🔥

**公司:**无

早期的Linux系统并不支持线程,但可以通过编程语言模拟实现“线程”,但其本质还是进程,这时我们认为Java中的线程是用户线程。到了2003年,RedHat初步完成了NPTL(Native POSIX Thread Library)项目,通过轻量级进程实现了服务号POSIX标准的线程,这时Java中的线程是内核线程。因此运行在现代服务器上的Java程序,使用的Java线程都会映射的到一个内核线程上
所以我们可以得到这样一个式子: J a v a 线程 ≈ 操作系统内核线程 ≈ 操作系统轻量级进程 Java线程 \approx 操作系统内核线程 \approx 操作系统轻量级进程 Java线程操作系统内核线程操作系统轻量级进程
那么对于线程的调度方式来说,我们可以得到: J a v a 线程的调度方式 ≈ 操作系统进程的调度方式 Java线程的调度方式 \approx 操作系统进程的调度方式 Java线程的调度方式操作系统进程的调度方式
恰好,Linux中使用了抢占式进程调度方式。因此,并不是JVM中实现了抢占式线程调度方式,而是Java使用了Linux的进程调度方式,Linux选择了抢占式进程调度方式


🔥创建线程的方式

**难易程度:**🔥🔥🔥

**重要程度:**🔥🔥🔥

**公司:**苏宁

Java中只有一种创建线程的方式。从Java层面来看,可以认为执行Thread thread = new Thread()就创建了线程;而调用Thread#start则是操作系统层面的线程创建于启动。

// 创建Java层面的线程
Thread thread = new Thread();
// 创建系统层面的线程
thread.start();

Tips:通常网上会给出至少4种创建线程的方式:

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口
  • 通过线程池创建

但这是一个错误的结论,实现Runnable接口或是实现Callable接口,其主要目的是为了重写Runnable#run方法,以实现业务逻辑,而要真正的创建并启动一个Java线程还是要创建Thread对象,并调用Thread#start方法。
Tips:还有的资料中搞出了6种创建线程的方式~~


🔥线程的状态与状态转换

**难易程度:**🔥🔥🔥

**重要程度:**🔥🔥🔥

**公司:**京东,百度

Java中定义了6种线程状态:

  • NEW(新建):创建线程后尚未启动(未调用Thread.start方法);
  • RUNNABLE(可运行):可运行状态的线程在Java虚拟机中等待调度线程选中获取CPU时间片;
  • BLOCKED(阻塞):等待监视器锁而阻塞的线程状态,处于阻塞状态的线程正在等待监视器锁进入同步的代码块/方法,或者在调用Object.wait之后重新进入同步的代码块/方法;
  • WAITING(等待):线程处于等待状态,处于等待状态的线程正在等待另一个线程执行的特定操作(通知或中断);
  • TIMED_WAITING(超时等待):线程处于超时等待状态,与等待状态不同的是,在指定时间后,线程会被自动唤醒;
  • TERMINATED(终止):线程执行结束。

线程状态定义为Thread的内部类state:

public enum State {
   
	NEW,
	RUNNABLE,
	BLOCKED,
	WAITING,
	TIMED_WAITING,
	TERMINATED;
}

线程状态的转换请参考下图:
图3:线程状态转换.png


🔥**Object#wait**方法的作用

**难易程度:**🔥🔥

**重要程度:**🔥🔥🔥🔥

**公司:**苏宁

Object#wait使线程等待,同时释放锁,线程进入WAITING或TIMED_WAITING状态Object#wait有3个重载方法:

public final void wait() throws InterruptedException;

public final native void wait(long timeoutMillis) throws InterruptedException;

public final void wait(long timeoutMillis, int nanos) throws InterruptedException;

由于**Object#wait**释放锁,因此需要在同步块(synchronized块)中调用,因为只有先获得锁,才有的释放。
Tips:面试中常常用来与**Thread#sleep**进行比较。


**Object#**notify**Object#notifyAll**方法的作用。

**难易程度:**🔥🔥

**重要程度:**🔥🔥🔥

**公司:**无

Object#notifyObject#notifyAll都是用来唤醒线程的。Object#notify随机唤醒一个等待中的线程,Object#notifyAll唤醒所有等待中的线程。通过Object#notifyObject#notifyAll唤醒的线程并不会立即执行,而是加入了争抢内置锁的队列,只有成功获取到锁的线程才会继续执行。


为什么要在循环中调用Object#wait方法?

**难易程度:**🔥🔥

**重要程度:**🔥🔥🔥

**公司:**无

如果不在循环中检查等待条件,等待状态中的线程可能会被错误的唤醒,此时跳过等待条件的检查可能会造成意想不到的问题。例如:生产者与消费者的场景。

public static void main(String[] args) {
   
	Product product = new Product(0);
	new Thread(() -> {
   
		for (int i = 0; i < 3; i++) {
   
			try {
   
				product.decrement();
			} catch (InterruptedException e) {
   
				throw new RuntimeException(e);
			}
		}
		System.out.println(Thread.currentThread().getName() + ",状态:" + Thread.currentThread().getState());
	}, "consumer-1").start();

	new Thread(() -> {
   
		for (int i = 0; i < 3; i++) {
   
			try {
   
				product.decrement();
			} catch (InterruptedException e) {
   
				throw new RuntimeException(e);
			}
		}
	}, "consumer-2").start();

	new Thread(() -> {
   
		for (int i = 0; i < 3; i++) {
   
			try {
   
				product.decrement();
			} catch (InterruptedException e) {
   
				throw new RuntimeException(e);
			}
		}
	}, "consumer-3").start();

	new Thread(() -> {
   
		for (int i = 0; i < 9; i++) {
   
			try {
   
				product.increment();
			} catch (InterruptedException e) {
   
				throw new RuntimeException(e);
			}
		}
		, "producer").start();
}

static class Product {
   
	private int count;
	private Product(int count) {
   
		this.count = count;
	}
	/**
* 生产
*/
	private synchronized void increment() throws InterruptedException {
   
		if (this.count > 0
  • 26
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术范王有志

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值