并发编程-基础八股

并发基础

为什么要使用并发编程

提升计算能力和性能
现在的主机一般都是多个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)
    包含两个情况:
  1. 线程在就绪状态(准备运行但尚未获得CPU时间)
  2. 实际正在运行.
    调用start方法后.线程进入就绪状态,在调度器的控制下获得CPU后变为运行状态.
  • 阻塞状态
    线程试图获取一个已经被其他线程占用的锁时,进入阻塞状态,线程无法继续执行,直到它成功获取锁.
  • 等待状态
    线程在等待另一个线程执行特定操作(如调notify()/notifyAll() ) 时,它会进入阻塞状态. 线程不会占用CPU资源.
  • 超时等待状态
    等待状态的一种变体,线程调用带有超时参数的方法(sleep,join)等时进入这种状态.
    如果超时,线程自动返回就绪状态.
  • 终止状态
    线程完成执行或因异常退出时,线程进入终止状态.
线程状态如何流转

新建状态 —> start方法 --> 运行状态

运行状态 —> 等待锁 --> 阻塞状态 —>获得锁 —> 运行状态

运行状态 --> 等待其他线程通知 —>等待状态 —收到通知–>运行状态(超时等待同理)

结束 --> 终止状态

Java创建线程的方式
  • 继承Thread类
  1. 定义一个继承Thread的类
  2. 重写run方法
  3. 创建该类的实例
  4. 调用该实例的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来获取线程的返回值,具体步骤:
  1. 定义一个类实现Callable接口,实现call方法
  2. 创建FutureTask对象,包装Callable对象
  3. 将FutureTask对象作为参数传递给Thread类构造函数
  4. 启动线程并使用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方法允许抛出异常,可以获取异常信息
相同点:

  1. 都是接口
  2. 都可以编写多线程
  3. 采用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方法中断线程
捋明白线程之间的状态转变
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值