并发编程面试
- 并发基础
- 为什么要使用并发编程
- 并发编程有什么缺点
- 并发的三个必要因素是什么?
- java程序如何保证多线程的运行安全
- 什么是进程,什么是线程
- 什么是上下文切换
- 守护线程和用户线程的区别?
- 什么是死锁?
- 形成死锁的四个必要条件是什么?
- 如何避免线程死锁?
- Java线程有几种状态,分别是什么?
- 线程状态如何流转
- Java创建线程的方式
- 说一下runnable和callable有什么区别和相同点?
- 什么是callable和Future,什么是FutureTask.
- sleep()和wait()有什么区别
- 为什么线程通信的方法wait,notify,notifyAll被定义在Object类里
- 为什么wait,notify,notifyAll必须在同步方法或者同步块中被调用
- sleep和yield方法有什么区别
- 如何停止一个正在运行的线程
- 捋明白线程之间的状态转变
并发基础
为什么要使用并发编程
提升计算能力和性能
现在的主机一般都是多个CPU,操作系统可以将多个线程分配给不同CPU执行,每个CPU执行一个线程,充分利用多核CPU的计算能力
处理复杂业务
它允许将复杂的业务流程拆分为多个并行执行的子任务.
对于复杂的业务模型,并行程序比串行程序更适合业务需求,提升应用性能.
并发编程有什么缺点
复杂性增加
最大缺点之一就是复杂性增加.
写代码时我们管理多个线程的生命周期,状态,还有交互,这肯定是增加了程序的复杂度.
还有就是调试和测试比单线程更困难,因为并发执行可能导致难以重现的错误,比如竞争条件和死锁.
线程安全问题
多个线程可能会同时访问共享资源,这就可能导致数据不一致或者错误.
所以确保线程安全一般使用锁机制或者其他同步工具,可能会导致性能瓶颈,尤其是高并发的情况下.
性能开销
使用不当可能导致性能下降,比如频繁的上下文切换,和锁竞争会消耗大量的CPU资源.
另外,过多的线程创建和销毁也会增加系统的负担.
并发的三个必要因素是什么?
原子性,可见性和有序性.
原子性: 指的是一个或多个操作全部执行成功或者全部失败
可见性: 一个线程对共享变量的修改,另一个线程能够马上看到
有序性: 程序执行的顺序按照先后顺序执行.
java程序如何保证多线程的运行安全
一般来说出现线程安全问题都是三个原因:
- 线程切换带来的原子性问题
解决方式:
多线程之间同步synchronized或者用锁(Lock) - 缓存导致可见性的问题
解决方法:
synchronized,volatile,Lock都可以解决 - 重排序带来的有序性问题
解决方法:
Happens-Before规则可以解决.
什么是进程,什么是线程
进程是运行时程序的实例, 是系统进行资源分配和调度的基本单位,实现了操作系统的并发(指用户/cpu可以在多个应用程序间切换).
进程的特点:
- 资源独立性
每个进程都有独立的内存空间和资源,进程间通信需要通过特定的机制,如管道,消息队列等 - 状态管理
进程可以处于不同的状态,如就绪,运行,阻塞等 - 开销
因为需要分配独立的内存空间和资源,所以创建和管理进程的开销较大.
线程是进程的子任务,是CPU调度的基本单位,用于保证程序的实时性,实现进程内部的并发,是操作系统的最小执行和调度单位.
特点:
- 共享资源
同一个进程中线程可以直接访问共享的内存空间 - 轻量级
因为共享进程资源,所以创建和管理开销小 - 并发执行
多个线程可以并发执行,提高程序响应速度和资源利用率
什么是上下文切换
一个CPU核心任意时刻只能被一个线程使用.
为了让线程都能得到有效执行,CPU采取为每个线程分配时间片并进行轮转的策略.
一个线程时间片使用完后重新处于就绪状态,把CPU让给其他线程使用.
这个过程属于一次上下文切换
目的是当前任务执行完CPU时间片后切换走了,下次再切换回自己时可以再加载这个任务状态.
守护线程和用户线程的区别?
- 守护(Daemon)线程
运行在后台,为其他前台线程服务.一旦用户线程都结束,守护线程会随JVM一起结束工作 - 用户线程
运行在前台,执行具体任务,比如程序主线程
什么是死锁?
死锁指两个或两个以上的进程(线程)执行过程中,出现阻塞的现象,没有外力作用,都它们无法推进下去,永远在互相等待对方.
形成死锁的四个必要条件是什么?
- 互斥条件
多个线程不能同时使用一个资源 - 占有并等待条件
线程1 在等待 资源2的同时,并不会释放自己已有的资源1 - 不可抢占条件
别人已经占有某个资源,不能因为自己需要,就去抢夺别人占有的资源 - 循环等待条件
两个线程获取资源的顺序构成了环形链.
如何避免线程死锁?
只要破坏死锁四个必要条件中任意一个就可以避免死锁.
- 死锁检测
一个线程请求锁失败时,这个线程可以遍历锁关系图看看是否有死锁发生.如果有的话,就释放锁资源.类似mysql里的死锁检测机制. - 加锁时限
尝试获取锁的时候加一个超时时间,如果时间到了,就放弃对锁的请求.等待一段时间再试.
Java线程有几种状态,分别是什么?
- 新建状态(New)
线程对象创建但没有调用start方法时,这个状态下线程尚未开始执行 - 运行状态(RUNNABLE)
包含两个情况:
- 线程在就绪状态(准备运行但尚未获得CPU时间)
- 实际正在运行.
调用start方法后.线程进入就绪状态,在调度器的控制下获得CPU后变为运行状态.
- 阻塞状态
线程试图获取一个已经被其他线程占用的锁时,进入阻塞状态,线程无法继续执行,直到它成功获取锁. - 等待状态
线程在等待另一个线程执行特定操作(如调notify()/notifyAll() ) 时,它会进入阻塞状态. 线程不会占用CPU资源. - 超时等待状态
等待状态的一种变体,线程调用带有超时参数的方法(sleep,join)等时进入这种状态.
如果超时,线程自动返回就绪状态. - 终止状态
线程完成执行或因异常退出时,线程进入终止状态.
线程状态如何流转
新建状态 —> start方法 --> 运行状态
运行状态 —> 等待锁 --> 阻塞状态 —>获得锁 —> 运行状态
运行状态 --> 等待其他线程通知 —>等待状态 —收到通知–>运行状态(超时等待同理)
结束 --> 终止状态
Java创建线程的方式
- 继承Thread类
- 定义一个继承Thread的类
- 重写run方法
- 创建该类的实例
- 调用该实例的start方法启动线程
class MyThread extends Thread {
@Override
public void run(){
System.out.println("线程执行了");
}
}
public class Main{
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
- 实现Runnable接口
更灵活的方式,适用于需要共享资源的场景.
a. 定义一个类实现Runnable接口
b. 重写run方法
c. 创建该实现类实例,并将其作为参数传递给Thread类构造函数
d. 调用线程对象的start方法启动线程
class MyRunnable implements Runnable{
@Override
public void run(){
System.out.println("线程执行了");
}
}
public class Main{
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
- 实现Callable接口并结合Future实现
与Runnable接口类似,但是它可以返回结果并且可以抛出异常.
通常配合Future来获取线程的返回值,具体步骤:
- 定义一个类实现Callable接口,实现call方法
- 创建FutureTask对象,包装Callable对象
- 将FutureTask对象作为参数传递给Thread类构造函数
- 启动线程并使用Future获取结果
class MyCallable implements Callable<Integer>{
@Override
public Integer call(){
return 5;
}
}
public class Main{
public static void main(String[] args) throws Exception {
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
System.out.println("子线程的返回值:" + futureTask.get());
}
}
说一下runnable和callable有什么区别和相同点?
区别:
一:
Runnable接口的run方法没有返回值
Callable接口call方法有一个泛型返回值,和Future,FutureTask配合用来获取异步执行的结果
二:
Runnable接口run方法只能抛出运行时异常,且无法捕获处理.
Callable接口call方法允许抛出异常,可以获取异常信息
相同点:
- 都是接口
- 都可以编写多线程
- 采用Thread.start启动线程
什么是callable和Future,什么是FutureTask.
-
Callable
代表一个可执行并返回结果的任务.
基本特点是有返回值和可以抛出异常. -
Future
表示异步计算的结果.
它提供了一些方法来取消任务(cancel),获取结果(get))以及检查任务的状态(isDone,isCancelled). -
FutureTask
表示一个异步计算的任务
里面传入Callable具体实现类,可以对这个异步运算任务进行一些操作
比如:
任务结果的等待获取
判断是否已经完成
取消任务等.
只有当任务完成时才能取回,如果运算尚未完成,执行get方法会阻塞.
sleep()和wait()有什么区别
它们两个都可以暂停线程的执行
- 类的不同:
sleep()是Thread线程类的静态方法
wait是Object类的方法 - 是否释放锁
sleep不释放锁;wait释放锁 - 用途
wait通常用于线程间交互/通信
sleep通常用于暂停执行. - 用法不同
因为wait通常用于线程间的交互/通信,所以调用wait后线程没办法自动苏醒,需要别的线程调用同一个对象的notify或者notifyAll方法.
wait有个方法后面跟timeout,超时后自动苏醒
sleep执行完成后,线程会自动苏醒.
为什么线程通信的方法wait,notify,notifyAll被定义在Object类里
定义在Object类的方法比较特殊,因为每个类都会继承Object.
java想让任何对象都可以作为锁,wait这些方法可以用于等待对象的锁或者唤醒线程.
java线程类并没有可供所有对象使用的锁,所以要实现任意对象都可调用的方法一定定义在Object类中.
当然其实也可以放在thread类里面,但是有个大问题,线程可能持有很多锁,当一个线程放弃锁的时候,到底要放弃哪个锁?
管理起来会更复杂.
为什么wait,notify,notifyAll必须在同步方法或者同步块中被调用
一个线程需要调用对象的wait方法时,这个线程必须拥有该对象的锁.
调用wait方法后线程释放对象锁进入等待状态.
直到其他线程调用这个对象上的notify方法.
同理,当线程调用notify方法时,它会释放这个对象的锁.
综上来说,所有方法执行前都需要线程持有对象的锁,这样只能通过同步来实现.
sleep和yield方法有什么区别
- 释放后优先级问题
sleep方法让出CPU时不考虑线程的优先级,会给低优先级线程运行的机会.
yield方法只会给相同优先级或更高优先级的线程以运行的机会. - 线程状态转换
线程执行sleep后 —>转入阻塞
执行yield后 —>转入就绪 - 异常抛出
sleep 声明抛出 InterruptException
yield方法没有声明任何异常
如何停止一个正在运行的线程
- 正常退出
run方法完成后线程终止 - 使用stop方法强行终止
这个方法已经过期作废了,一般不推荐 - 使用interrupt方法中断线程