【八股文】并发编程相关考点

1.线程和进程和协程的区别

  • 进程操作系统中资源分配和调度的基本单位,是程序的一次执行过程,因此是动态的,即一个进程从创建到运行再到消亡。每个进程都有独立的内存空间,一位置一个进程的变量修改不会影响到其他经常。进程之间的同学通常需要通过操作系统提供的机制,如管道、消息队列和共享内存等。进程之间切换的开销较大,因为进程之间是完全隔离的,每次切换都需要保存当前进程的上下文并加载新进程的上下文。
  • 线程是一个比进程更小的执行单元,是CPU调度和分派的基本单位。一个进程的过程可以产生多个线程,这些线程共享同一进程的资源,如堆和方法区资源。线程之间的通信和数据共享要比进程间容易得多。线程切换的开销比进程小,因为它们共享相同的内存上下文,但多个线程同时访问共享资源时,需要同步机制来防止数据竞争和不一致的情况,比如使用锁或者信号量。
  • 协程是一个比线程更轻量级的执行单元,它在用户空间内调度和管理。协程的控制权转移由协程自身决定,而不是由操作系统调度器决定。这就意味着协程可以在任何地方暂停和恢复执行,协程之间的切换开销远低于线程。所以协程非常适合用于I/O密集型的任务,因为它们可以在等待I/O操作完成时主动放弃控制权,然后在完成后恢复执行,这样可以避免线程在等待I/O时的阻塞状态,提高系统的并发性能。
进程线程协程
定义操作系统资源分配和调度的基本单位进程内执行单元,CPU 调度和分派的基本单位用户空间内的轻量级执行单元
内存拥有独立的内存空间共享所属进程的内存空间共享所属进程和线程的内存空间
开销创建和切换成本高创建和切换成本较低创建和切换成本基地
通信需要通过进程间进行通信可以直接访问共享数据可以直接访问共享数据
并发能力较低,受限于系统调度和资源限制较高,受限于系统调度和资源限制极高,几乎无限制,受限于系统资源
调度由操作系统调度由操作系统调度由用户空间调度
隔离性最强,完全隔离较弱,共享内存和资源最弱,共享内存和资源
使用场景高隔离需求,多核CPU利用高效资源共享,多任务处理I/O密集型任务,异步编程

2.堆和方法区的区别

堆和方法区都是所有线程共享的资源堆是进程中最大的一块内存,主要存放用户新创建的对象。方法区主要用于存放已被加载的类信息、常量、静态变量等。

3.并发和并行的区别

在多核CPU下

  • 并发:两个及其以上的作业在同一时间段执行
  • 并行:两个及其以上的作业在同一时刻执行

4.同步和异步的区别

同步:发出一个调用后,在没有得到结果之前,会一直等待
异步:发出一个调用后,不用等待结果,该调用直接返回
在项目中,例如注册完成后会给邮箱发一条注册成功的信息,假设注册完成需要50ms,而发一条注册成功的信息到邮箱需要50ms,如果是同步则一共需要50ms,而异步的话,就只需要50ms,因为只需要注册完成就行了,发送信息由消息队列来异步发送。

5.多线程相关问题

  • 为什么使用多线程:为了减少线程上下文切换的开销,现在的系统要求百万甚至千万级并发量,利用多线程机制可以大大提高系统的并发能力和性能。
  • 多线程带来的问题:内存泄漏、死锁、线程不安全(对同一份数据进行访问,是否能够保证数据的正确性和一致性)
  • 单核CPU运行多个线程效率一定会高吗?不会,要看是CPU密集型还是IP密集型,如果是CPU密集型,多个线程同时运行会导致频繁的线程切换,会影响效率,而IO密集型会有很多线程来提高效率。

6.创建线程的方法有哪些?

  • 继承Tread类重写run方法
  • 实现Runnable接口重写run方法
  • 实现Callable接口,重写call方法,通过FutureTask类来传入到Thread中。与Runnable的区别
    • call() 方法有返回值(String类型),而run方法没有
    • call() 方法可以抛异常,而run方法不行
    • callable接口有泛型,而Runnable没有
  • 使用线程池
    在这里插入图片描述

7.什么是线程的上下文切换

是指线程在占用CPU状态中退出,从运行态->其他状态(就绪态或者阻塞态,例如调用sleep和wait往返主动让出,或者是时间片用完,以及线程被阻塞等),需要保存当前线程的上下文,线程下次占用CPU的时候进行恢复现场。

8.什么是死锁,怎么避免死锁?

  • 死锁的定义:一个线程或多个线程在等待某个资源被释放,多个线程同时阻塞。
  • 产生死锁的四个条件:
    • 互斥条件:该资源任意时刻只被一个线程占用
    • 请求与保持条件:一个线程因请求资源而被阻塞时,对已获得的资源保持不放
    • 不剥夺条件:只要该线程在使用资源,其他线程不能剥夺
    • 循环等待条件:若干个线程之间形成一种头尾相接的等待资源
  • 预防死锁:破坏请求与保持条件(一次性申请所有资源),破坏剥夺条件(占用部分资源的线程申请资源,如果申请不到则释放该资源),破坏循环等待条件(按某一顺序申请资源, 反序释放资源)
  • 避免死锁:在资源分配时,接祖算法对资源进行评估(银行家算法),使得进入安全状态。
    死锁诊断:
    • 通过jdk自带工具
      • jps:输出JVM运行的进程信息(可以查看进程id)
      • jstack:查看Java进程内线程的堆栈信息(jstack -l 进行id)
    • jdk自带可视化工具,通过jdk安装的bin目录
      • jconsole:可以通过提供的日志信息检查死锁
      • VisualVM:能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈。

9.线程包括哪些状态,状态之间是怎么变换的

  • NEW(新建):当一个线程对象被创建当尚未启动时,他处于新建状态,此时线程还未调用start()方法
  • RUNNABLE(可运行):当线程调用了start()方法后,线程将进入可运行状态。而在这个状态下有两个子状态(就绪态和运行态),当时间片用完时就会回到就绪队列里,以便继续执行。如果线程的run()方法已经完成执行,那么线程也将回到非活动状态,最终会被垃圾回收器回收。
  • BLOCKED(阻塞)或WAITING(等待):当线程因为某些原因不能继续执行时,它会进入阻塞或等待状态。这可能是因为:
    • synchronized阻塞,被另外一个线程只有锁
    • 调用了wait、join或sleep方法,使得线程主动放弃CPU并等待特定条件发生
    • I/O操作或其他系统调用导致的阻塞
  • TIMED-WAITING(定时等待):当线程调用了一个带时间限制的等待方法时的状态,例如Thread.sleep(300)或者Object.wait(300)。线程在指定时间内完成。
  • TERMINTED(终止):当线程的run()方法正常完成执行,或者线程由于未捕获的异常而提前结束时,就会进入终止状态。
    状态之间的转换如图所示。
    在这里插入图片描述
    追问:sleep和wait方法有什么不同
  • 都是使线程暂停,进入等待
  • sleep没有释放锁,而wait释放了锁(其他线程可以获得该对象锁)
  • wait方法常用于线程之间的交互,而sleep方法用于暂停线程执行
  • wait方法被调用后不会自动苏醒,需要通过其他对象调用notify或notifyAll方法将其唤醒
  • sleep是Thread的静态方法,wait是Object的本地方法
  • sleep是让当前线程停止执行,不涉及对象类,也不需要获得对象锁。

10.可以直接调用Thread的run方法吗

  • new Thread方法线程进入新建态,调用start方法线程进入就绪态,分配到时间片就可以运行。start会执行线程的相应准备工作,然后执行run方法的内容
  • 而直接调用run方法会把run方法当做main线程下的普通方法执行,并不是多线程工作。
  • run方法封装了要被线程执行的代码,可以被调用多次,而start方法用来启动线程,通过该线程调用run方法执行run方法中的逻辑,start方法只能被调用一次

11.乐观锁和悲观锁是什么

  • 悲观锁:假设总是会发生最坏的情况,共享资源每次被访问都会出现问题,所以每次获取资源的操作都加了锁,直到该线程用完这个资源后才会释放锁,其他线程才有机会拿到这个资源。Java中的synchronized和ReentrantLock可以实现悲观锁。在高并发的情况下,如果使用悲观锁,大量线程阻塞会导致上下文切换。
  • 乐观锁:假设总是会发生最好的情况,共享资源每次被访问都不会出现问题,无需加锁,也不要等待,线程一直执行。只是提交资源时要去验证该资源是否被其他资源修改。
  • 乐观锁多用于多读写少的场景
  • 悲观锁多用于多写少读的场景

12.如何实现乐观锁

  • 版本号机制:在表中加一个版本号字段用int修饰,当线程去修改该表数据后,会去判断当前版本号和数据库里的版本号是否相同,如果相同则更新,否则重试更新操作,直到更新成功。然后版本号+1
  • CAS算法(compare and swap):CAS算法是用一个预期值和要更新的值进行比较,相等才更新,在无锁的情况下保证线程操作的原子性。CAS算法是一个原子操作,依赖于一条CPU的原子指令。Unsafe类提供了compareAndSwapObject(Int,Long)方法来实现Object,Int和Long类型的CAS操作。
    • 场景:AQS框架,AtomicXXX类
    • 在操作共享变量的时候使用自旋锁,效率上更高一些

13.CAS算法存在哪些问题

  • 但CAS算法存在场景的ABA问题,有可能将V值从A改为B再改回A,然后与E值对比相同,认为没有修改,则更新。解决ABA问题在变量前面加上版本号或者时间戳。使用AtomicStampedReference类,其中的compareAndSet方法会检查当前引用是否等于预期引用,当前标识是否等于预期标识。
  • 循环时间长开销大:CAS经常会用到自旋操作来重试,不成功就一直循环执行下去直到成功,这样会给CPU带来很大的执行开销。JVM提供的pause指令会提升效率。
  • 只能保证一个共享变量的原子操作,CAS算法只对单个变量有用,如果有多个变量,可以封装到compareAndSwapObject里,可以利用AtomicReference类把多个共享变量合成一个共享变量来操作。

14.解释一下volatile关键字

  • volatile 修饰的变量,该变量是共享的但是不稳定,每次使用都要在主存中进行读取
  • volatile关键字可以保证数据的可见性(线程间是否可见,指的是第一个线程对该数据进行修改后,第二个线程看不到第一个线程修改的数据,而使用了原来的数据),但不能保证数据的原子性,而synchronized关键字都能保证。
  • 防止指令重排序,被volatile修饰的变量在进行读写操作时,会通过插入特定的内存屏障来禁止指令重排序。volatile使用技巧:
    • 写变量让volatile修饰的变量的在代码最后位置
    • 读变量让volatile修饰的变量在代码最开始位置
  • 使用场景:单例模式的成员属性需要用volatile修饰,禁止指令重排序,在new一个对象的时候。有三部分组成,第一为该对象分配内存空间,第二初始化该对象,第三将该对象指向分配的内存地址。如果不禁止重排序,执行顺序可能会是1->3->2,这样在单线程下不会有问题,如果在多线程下,T1先执行1和3,T2调用实例化方法后发现这个对象不为空,直接返回,但没被初始化。
    单例模式下的双重检查锁定:
public class Singleton {
    private static volatile Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

15.解释一下synchronized关键字

  • 主要解决多线程之间访问共享资源的同步性,被synchronized关键字修饰的方法和代码块在任意时刻只能被一个线程执行。
  • 修饰静态方法(Class锁)、修饰实例方法(对象锁)、修饰代码块(Class锁、对象锁),构造方法不能用synchronized修饰,因为本身就是安全的。
  • synchronized底层是用的一个monitorenter和monitorexit指令,保证锁在同步代码块代码正常运行,以及出现异常都能被正常释放。jdk1.6之后,synchronized引入了自旋锁、适应性自旋锁等。在这里插入图片描述
  • Monitor实现的锁属于重量级锁,你了解过锁升级吗?
    请添加图片描述
    锁升级策略的目的是尽量避免使用重量级锁,因为重量级锁的性能开销相对较大。通过先使用偏向锁和轻量级锁,只有在确实需要的情况下才升级到重量级锁,可以显著提高多线程程序的执行效率。然而,锁升级的具体实现细节依赖于JVM的版本和具体的实现,不同版本的JVM可能有不同的锁升级策略。

16 synchronized和volatile关键字的区别?

  • volatile是线程同步的轻量级实现,其性能要比synchronized高,volatile只能用于变量,而synchronized关键字可以用于代码块和方法
  • volatile保证数据的可见性,不保证原子性,而synchronized都能保证
  • volatile解决变量在多个线程之间的可见性,synchronized解决多个线程访问资源的同步性。

17 什么是ReentrantLock锁?

  • ReentrantLock实现Lock接口,是一个可冲入独占式的锁,和synchronized关键字类似。ReentrantLock更灵活、更强大,增加了轮询,超时,中断等。ReentrantLock里有一个内部类Sync,继承了AQS(抽象队列同步器),添加锁和释放锁都在Sync内部类里完成。Sync有公平锁和非公平锁两个子类
  • 公平锁:锁被释放后,先申请的线程先得到,性能要差一些,上下文切换更加频繁。
  • 非公平锁:锁被释放后,会随机或者按照一些优先级排序来申请线程,但可能会导致一些线程永远无法获取到锁。
  • AQS基本工作机制
    • 作为基础框架使用的,是一种锁机制(多线程的队列同步器)
    • AQS内部维护一个先进先出的双向队列,队列中存储的排队线程
    • 在AQS内部还有一个state属性,相当于一个资源,默认为0(无锁状态),如果队列中的一个线程修改成功了,state为1在这里插入图片描述
  • 语法
    ReentrantLock lock = new ReentrantLock(); try{lock.lock();//获取锁} finally{lock.unlock() //释放锁}
  • 原理:CAS+AQS队列实现,支持非公平锁和公平锁。
    在这里插入图片描述

18 ReentrantLock和synchronized有什么区别

  • 都是可重入锁,指线程可以再次获取到自己的内部锁。可重入锁是指,一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其还想获取这个对象锁时是可以获取的。
  • synchronized有依赖于JVM(monitor监视器中的monitorenter和monitorexit指令),而ReentrantLock依赖于API
  • ReentrantLock比synchronized增加了一些高级功能
    • 等待可中断,获取锁的过程可以被中断,可以通过Lock.lockInterruptibly()来实现,而synchronized是不可中断锁。
    • 可实现公平锁(需要传入参数,默认为非公平锁),ReentrantLock可以通过公平锁子类继承Sync内部类来实现公平锁,synchronized只能是非公平锁。
    • 支持多个条件变量
    • 在竞争激烈时,ReentrantLock能够提供更好的性能,而非竞争时,ReentrantLock的性能也可以,因为做了一些偏向锁和轻量级锁的优化。
  • synchronized与AQS的区别
synchronizedAQS
关键字,C++实现Java实现,非关键字
悲观锁,自动释放锁悲观锁,手动开启和关闭
锁竞争激烈都是重量级锁,性能差锁竞争激烈的情况下,提供了多种解决方案

19 你对线程池了解多少,以及一些相关问题

  • 线程池:
    • 线程池管理一系列线程的资源池,当有任务要处理时,直接从线程池中获取线程来处理,处理完后线程并不会理解被摧毁,而是等待下一个任务。
    • 线程池的好处,降低资源的损耗,提高响应速度,提高线程的可管理性。
    • 两种实现方式,ThreadPoolExecutor和Executors。
    • ThreadPoolExecutor有哪些参数
      • 核心线程数(corePoolSize)
      • 最大线程数(maximumPoolSize):也就是核心线程+救急线程的最大数目
      • 时间单位(unit):救急线程的生存时间单位,如秒,毫秒等
      • 工作队列(workQueue):当没有空闲的核心线程时,新来的任务会加入到此队列排队,队列满会创建救急线程执行任务
      • 线程工厂(threadFactory):可以定制线程对象的创建,例如设置线程名字、是否是守护线程等。
      • 拒绝策略(handler):当所有线程都在繁忙,workQueue也放满时,会出发拒绝策略。
  • 线程池执行流程
    在这里插入图片描述
  • 线程中有哪些阻塞队列
    • ArrayBlockingQueue:基于数据结构的有界阻塞队列,FIFO
    • LinkedBlockingQueue(使用最多):基于链表结构的有界阻塞队列,FIFO,默认容量是Integer的最大值
    • 还有两种不常用的阻塞队列,DelayedWorkQueue和SynchronousQueue
    • 两者的区别:
      在这里插入图片描述
  • 如何确定核心线程数(N表示CPU核数)
    • IO密集型任务:文件读写,DB读写,网络请求等。核心线程数为2*N + 1
    • CPU密集型任务:计算型代码、BitMap转换等。核心线程数为N + 1
    • 高并发、任务执行时间短 -> (N + 1),减少线程上下文的切换
    • 高并发、任务务执行时间长,解决这种类型的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存时第一步,增加服务器是第二步,再考虑线程池的设置。
  • 线程种类有哪些?
    • java.util.concurrent.Executors类提供了大量创建连接池的静态方法,常见的就有四种
    • 创建使用固定线程数的线程池 通过ThreadPoolExecutor(五个参数),没有救急线程(适用场景:任务量已知,相对耗时的任务)
    • 单线程化的线程池:核心线程数和最大线程数都是1,阻塞队列为LinkedBlockingQueue,最大容量为Integer.MAX_VALUE(适用场景:按照顺序执行的任务)
    • 可缓存线程池:没有核心线程数,最大线程数为Integer.MAX_VALUE,阻塞队列为SynchronousQueue:不对出元素的阻塞队列,每个插入操作都必须等待一个移出操作。(使用场景:任务数比较密集,但每个任务执行时间较短的情况)
    • 提供了“延迟”和“周期执行”的线程池
  • 为什么不建议用Executors创建线程池,而是使用ThreadPoolExecutor的方式
    • 阿里巴巴开发手册-嵩山版(强制要求):线程池不允许使用Executors创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让开发人员更加明确线程池的运行规则,规避资源耗尽的风险。
      • FixedThreadPool和SingleThreadPool:运行的请求队列长度为int的最大值,可能会堆积大量的请求,从而导致OOM(内存溢出)
      • CacheThreadPool:运行创建线程数量为int的最大值,可能会创建大量的线程,从而导致OOM
  • 线程池的使用
    • CountDownLatch(封闭/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或多个线程,等待其他多个线程完成某件事之后才能执行):
      • 其中构造参数用来初始化等待计数值
      • await() 用来等待计数归零
      • countDown() 用来让计数减一
      • 使用场景一:(es数据批量导入)在这里插入图片描述
      • 使用场景二:Future,在实际的开发中,会调用多个接口来汇总数据,所有接口没有依赖关系,就可以通过线程池+future来提升性能。(数据汇总,报表汇总),注:在智慧教育平台中,我们在实现优惠卷算法时,由于优惠的方案众多,我们先用排列组合筛选出满足的优惠卷组合,然后用for循环计算出所有的优惠卷组合计算出的金额。这样使用for循环计算效率不高,我们可以利用多线程并行计算,for循环将每个方案交给一个线程去任务执行。
      • 使用场景三:异步调用,为了避免下一级方法影响上一级方法(性能考虑),可以使用异步线程调用下一个方法(不需要下一级方法返回值),可以提升方法相应时间。(文章搜索与历史记录)
  • 如何控制某个方法允许并发访问线程数量
    • Semaphore信号量,是JUC包下的工具类,步骤
      • 创建信号量,给一个容量
      • 请求信号量,信号量个数-1,如果个数变为负数,则请求会阻塞
      • 释放信号量,信号量个数+1

20 ThreadLocal的原理

  • 定义:为每一个线程分配一个独立的线程副本,从而解决变量并发访问冲突问题。ThreadLocal实现了线程内的资源共享。(例如登录后的用户,将这个用户ID存到ThreadLocal里,以后可以直接使用这个用户ID,不用去判断当前是哪个ID在使用)【本质上就是一个线程内部存储类,让多个线程只能操作自己内部的值,从而实现多线程数据隔离】
  • 原理:每个线程持有一个ThreadLocalMap的成员变量,用于存储资源对象
    • 调用set方法,以ThreadLocal作为key,资源对象作为value,放入当前线程的ThreadLocalMap集合中
    • 调用get方法,以ThreadLocal作为key,获取到当前线程的资源值
    • 调用remove方法,以ThreadLocal作为key,移出当前线程关联的资源值
  • ThreadLocal内存泄漏问题
    • 强引用:一个对象处理有用且必须得状态,GC不会回收它,即使内存不足,会出现OOM也不会回收。
    • 弱引用:一个对象处于可能有用且不必须的状态,在GC线程扫描内存区域发现弱引用时,就会回收该对象。
    • 其中key为弱引用,而value是强引用,内存中如果内存不够,GC会进行回收,而value是强引用不会被回收,就会存在内存泄漏。
    • 解决办法:当使用ThreadLocal时,先使用remove方法,将(key和value)清除

21 JMM(Java内存模型)

  • Java内存模型定义了共享内存中多线程程序读写操作的规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。
  • JMM把内存分成了两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
    线程与线程之间是相互隔离,线程与线程交互需要通过主内存。

22 其他

  • 如何保证T1、T2、T3三个线程按顺序进行
    • 解决:可以用join方法,当重写T2的run方法时,加一个T1.join。当重写T3的run方法时,加一个t2.join。
  • 如何停止一个正在运行的线程?
    • 使用退出标志,例如while(flag),然后如果使用完该线程后将flag=false
    • 使用stop强制停止(不推荐)
    • 使用interrupt方法中断线程
  • 导致并发程序出现问题的根本原因是什么?
    • 原子性:一个线程在CPU中操作不能暂停、中断。要么全执行,要么都不执行。
      • 解决方案:使用synchronized或者ReentrantLock
    • 内存可见性:让一个线程对共享变量的修改对另外一个线程可见
      • 解决方案:加一个volatile关键字修饰,当然锁也可以实现,但是性能很低。
    • 有序性:指令重排序,有可能程序中的代码执行的先后顺序不同
      • 解决方案:加一个volatile关键字修饰,volatile是静止指令重排序的。

参考

黑马程序员面经

  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

星空皓月

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

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

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

打赏作者

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

抵扣说明:

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

余额充值