面试题系列之并发面试题

1、初级系列

  1. 线程和进程的区别或关系?
    答:进程相当于一个程序,程序由指令和数据构成,CPU执行指令,内存加载数据。一条指令相当于一个线程,所有说一个进程可能包含多个线程。进程和进程之间不共享公共内存,而线程和线程之间就共享。线程是最小的执行单元,进程是最小的资源管理单元

  2. 讲一下协程?
    答:协程,英文Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

  3. 并发和并行的区别?
    答:并发:一段时间周期执行多条指令,按照时间片交替执行指令;
           并行:同一时刻执行多条指令。

  4. 为什么要是用多线程(多线程的好处和坏处)?
    答:随着计算机硬件技术的飞快提升,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。利用好多线程机制可以大大提高系统整体的并发能力以及性能。
           好处:能提高程序的执行效率提高程序运行速度,
           坏处:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。

  5. 并发编程的三要素是什么?
    答:原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
           可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
           有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
    出现线程安全问题的原因:
           线程切换带来的原子性问题
           缓存导致的可见性问题
           编译优化带来的有序性问题
    解决办法:
           JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
           synchronized、volatile、LOCK,可以解决可见性问题
           Happens-Before 规则可以解决有序性问题

  6. 线程的生命周期有哪些?并且是怎么转换的?
    答:操作系统层面(五种):初始化状态,可运行状态,运行中状态,阻塞状态终止状态。
    Java线程层面(六种):初始化状态(NEW),运行状态(RUNNABLE),等待状态(WAITING),超时等待状态(TIMED_WAITING),阻塞状态(BLOCKED),终止状态(TERMINATED)。
    在这里插入图片描述

  7. 讲一下创建线程的几种方式?
    答:四种,继承THREAD(不推荐),实现接口RUNNABLE(将任务和线程分开),实现接口CALLABLE(有返回值),使用EXEXUTORS创建线程(推荐)。

  8. 线程池的优点?
    答:降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
           提高响应速度:可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞,当任务到达时,任务可以不需要的等到线程创建就能立即执行。
           提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
           附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。

  9. 线程池的状态?
    答: RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务
           SHUTDOWN:关闭状态,不能接受新提交的任务,但却可以继续处理阻塞队列中的任务
           STOP:不能接受新提交的任务,也不能处理阻塞队列中的任务,会中断处理任务的线程
           TIDYING:所有任务都已终止了,workerCount为0
           TERMINATED:在terminated()方法执行完成后进入该状态
    在这里插入图片描述

  10. 创建线程池的常用方法?
    答:newSingleThreadExecutor:创建一个单线程的线程池。
           newFixedThreadPool:创建固定大小的线程池。
           newCachedThreadPool:创建一个可缓存的线程池。
           newScheduledThreadPool:创建一个大小无限的线程池

2、中级系列

  1. 说一下synchronized的实现原理?
    答:执行同步代码块后首先要先执行monitorenter指令,退出的时候monitorexit指令。通过分析之后可以看出,使用Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。
           任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取该对象的监视器才能进入同步块和同步方法,如果没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,进入到BLOCKED状态。在这里插入图片描述在这里插入图片描述

  2. 说一下volatile怎么维护可见性和有序性的?
    答:可见性:如果想成A改变了flag的值,就会通知线程b更新flag的值,从而实现可见性。
    在这里插入图片描述
           有序性:volatile 包含禁止指令重排的语义,Java 内存模型会限制编译器重排序和处理器重排序,重排序的目的是编译器和处理器为了优化程序性能而对指令序列进行重排序,但在单线程和单处理器中,重排序不会改变有数据依赖关系的两个操作顺序。
    在这里插入图片描述
    Java 内存模型在指令序列中插入内存屏障来处理 volatile 重排序规则,策略如下:
           1、volatile 写操作前插入一个 StoreStore 屏障
           2、volatile 写操作后插入一个 StoreLoad 屏障
           3、volatile 读操作后插入一个 LoadLoad 屏障,
           4、volatile 读操作后插入一个 LoadStore 屏障

  3. 说一下synchronized和volatile的区别?
    答:1、使用上区别:synchronized能修饰方法和方法块,volatile只能修饰变量
           2、原子性上区别:synchronized可以保证原子性,volatile不能保证原子性
           3、可见性的实现原理区别:synchronized使用monitorEnter和monitorExit,volatile对变量加了lock,ACC_VOLATILE,采用lock 汇编指令操作
           4、有序性上的区别:synchronized重并发退化到串行,会引发阻塞,volatile不会引发阻塞。

  4. 说一下lock的实现?
    答:Lock接口是对锁操作的基本定义,它提供了synchronized关键字所具备的全部功能方法,另外我们可以借助Lock创建不同的Condtion对象进行多线程间的通信操作。
           1、lock():尝试获取锁。如果锁已经被另一个线程持有,那么该线程会进入阻塞状态,直到获取到锁。
           2、lockInterruptibly():尝试获取锁,进入阻塞的线程是可以被中断的,该方法可以获取中断信号。
           3、tryLock():尝试获取锁,调用该方法获取锁无论是否成功都会立即返回,线程不会进入阻塞状态。
           4、boolean tryLock(long time, TimeUnit unit):该方法与tryLock()方法类似,只是多了获取锁时间的限制,如果在限制的时间内没有获取到锁,则结果返回false。
           5、unlock():释放锁,在持有锁的线程运行结束后,应该确保对锁资源的释放。
           6、newCondition():创建一个与Lock相关的Condition对象。
           ReentrantLock提供了两种锁机制:公平锁和非公平锁。公平锁通过类FairSync提供的方法实现,非公平锁通过NonfairSync提供的方法实现。ReentrantLock将同步状态state用于保存锁获取操作的次数,并且还维护了一个owner变量用来保存当前所有者线程的标识符。

  5. 说一下lock和synchronized的区别?
    答:1、实现层面:lock是接口,synchronized是关键字
           2、用法上:lock只能作用到代码上,synchronized可以修饰方法和代码块
           3、是否自动释放锁:lock必须优代码释放锁,synchronized系统会自动释放锁
           4、是否可中断和公平:synchronized 加锁可重入、不可中断、非公平;Lock 可重入、可中断、可公平和不公平、细分读写锁提高效率

  6. 说一下对AQS和CAS的理解?
    答:AQS:AQS内部使用了一个volatile的变量state来作为资源的标识。同时定义了几个获取和改版state的protected方法,子类可以覆盖这些方法来实现自己的逻辑(这三种叫做均是原子操作,其中compareAndSetState的实现依赖于Unsafe的compareAndSwapInt()方法。):

    CAS:CAS的全称是:比较并交换(Compare And Swap)。在CAS中,有这样三个值:
           V:要更新的变量(var)
           E:预期值(expected)
           N:新值(new)
    比较并交换的过程如下:
           判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。所以这里的预期值E本质上指的是“旧值”。

  7. 说一下原子操作类的原理?
    答:原子量底层的实现均是采用CAS非阻塞算法实现的,是无锁(lock-free)算法中最有名的一种(无锁算法:不使用锁机制来实现线程安全的算法,采用锁机制都会存在线程为请求锁而产生阻塞的情况),CAS不会阻塞线程从而不会带来CPU上下文切换的性能开销。

  8. 说一下常见的几种锁?
    答:乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制,
           悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
           公平锁:指多个线程按照申请锁的顺序来获取锁。
           非公平锁:指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
           偏向锁:指的是当前只有这个线程获得,没有发生争抢,此时将方法头的markword设置成0,然后每次过来都cas一下就好,不用重复的获取锁.指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价
           轻量级锁:在偏向锁的基础上,有线程来争抢,此时膨胀为轻量级锁,多个线程获取锁时用cas自旋获取,而不是阻塞状态
           重量级锁:轻量级锁自旋一定次数后,膨胀为重量级锁,其他线程阻塞,当获取锁线程释放锁后唤醒其他线程。(线程阻塞和唤醒比上下文切换的时间影响大的多,涉及到用户态和内核态的切换
           独享锁:独享锁是指该锁一次只能被一个线程所持有。
           共享锁:共享锁是指该锁可被多个线程所持有。
           可重入锁:如果一个线程获得过该锁,可以再次获得,主要是用途就是在递归方面,还有就是防止死锁,比如在一个同步方法块中调用了另一个相同锁对象的同步方法块

  9. 线程池的submit和execute方法区别
    答:1、接收的参数不一样
    2、submit有返回值,而execute没有
    3、submit方便Exception处理
    意思就是如果你在你的task里会抛出checked或者unchecked exception,
    而你又希望外面的调用者能够感知这些exception并做出及时的处理,那么就需要用到submit,通过捕获Future.get抛出的异常。

  10. 线程池的拒绝策略?
    答:ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务 ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务

  11. 线程池的状态?
    答:

// RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务
private static final int RUNNING    = -1 << COUNT_BITS;
// SHUTDOWN:关闭状态,不能接受新提交的任务,但却可以继续处理阻塞队列中的任务
private static final int SHUTDOWN   =  0 << COUNT_BITS;
// STOP:不能接受新提交的任务,也不能处理阻塞队列中的任务,会中断处理任务的线程
private static final int STOP       =  1 << COUNT_BITS;
// TIDYING:所有任务都已终止了,workerCount为0
private static final int TIDYING    =  2 << COUNT_BITS;
// TERMINATED:在terminated()方法执行完成后进入该状态
private static final int TERMINATED =  3 << COUNT_BITS;

在这里插入图片描述

3、高级系列

  1. 线程池几个参数的意义?
    答:corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
           maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
           keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
           unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
           workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
           threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
           handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。

  2. 如何合理配置线程线程池参数?
    答:从以下几个角度来进行分析:
           任务的性质:CPU 密集型任务,IO 密集型任务和混合型任务。任务的优先级:高,中和低。任务的执行时间:长,中和短。任务的依赖性:是否依赖其他系统资源,如数据库连接。
           任务性质不同的任务可以用不同规模的线程池分开处理。CPU 密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO 密集型任务则由于需要等待 IO 操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2xNcpu。混合型的任务,如果可以拆分,则将其拆分成一个 CPU 密集型任务和一个 IO 密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的 CPU 个数。
           优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
    执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
           依赖数据库连接池的任务,因为线程提交 SQL 后需要等待数据库返回结果,如果等待的时间越长 CPU 空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用 CPU。并且,阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。
           线程池参数动态化配置(推荐)。
    在这里插入图片描述

  3. 线程进入线程池是怎么个流程和工作原理?
    答:在这里插入图片描述
    在这里插入图片描述
    execute 方法执行逻辑有这样几种情况:
           如果当前运行的线程少于 corePoolSize,则会创建新的线程来执行新的任务;如果运行的线程个数等于或者大于 corePoolSize,则会将提交的任务存放到阻塞队列 workQueue 中;如果当前 workQueue 队列已满的话,则会创建新的线程来执行任务;如果线程个数已经超过了 maximumPoolSize,则会使用饱和策略 RejectedExecutionHandler 来进行处理。

  4. 如何自定义实现线程池?
    答:继承ThreadPoolExecutor,参数可以自定义

  5. ThreadLocal的讲解一下?
    答:ThreadLocal用于创建线程的本地变量,我们知道一个对象的所有线程会共享它的全局变量,所以这些变量不是线程安全的,我们可以使用同步技术。但是当我们不想使用同步的时候,我们可以选择ThreadLocal变量。
           每个线程都会拥有他们自己的Thread变量,它们可以使用get()\set()方法去获取他们的默认值或者在线程内部改变他们的值。ThreadLocal实例通常是希望它们同线程状态关联起来是private static属性。

  6. Futrue讲解一下
    答:Futrue可以监视目标线程调用call的情况,当你调用Future的get()方法以获得结果时,当前线程就开始阻塞,直接call方法结束返回结果。


4、往期佳文

4.1、面试系列

1、吊打面试官之一面自我介绍
2、吊打面试官之一面项目介绍
3、吊打面试官之一面系统架构设计
4、吊打面试官之一面你负责哪一块
5、吊打面试官之一面试官提问
6、吊打面试官之一面你有什么问题吗

······持续更新中······


4.2、技术系列

1、吊打面试官之分布式会话
2、吊打面试官之分布式锁
3、吊打面试官之乐观锁
4、吊打面试官之幂等性问题
5、吊打面试关之分布式事务
6、吊打面试官之项目线上问题排查

······持续更新中······

4.3、源码系列

1、源码分析之SpringBoot启动流程原理
2、源码分析之SpringBoot自动装配原理
3、源码分析之ArrayList容器
4、源码分析之LinkedList容器
5、源码分析之HashMap容器
6、源码分析之ConcurrentHashMap容器
7、源码分析之五种Map容器的区别

······持续更新中······

4.4、数据结构和算法系列

1、数据结构之八大数据结构
2、数据结构之动态查找树(二叉查找树,平衡二叉树,红黑树)

······持续更新中······

4.5、并发系列

1、并发系列之初识多线程
2、并发系列之JMM内存模型
3、并发系列之synchronized解析
4、并发系列之volatile解析
5、并发系列之synchronized与volatile的区别
6、并发系列之Lock解析
7、并发系列之synchronized与lock的区别
8、并发系列之CAS与原子操作
9、并发系列之AQS分析
10、并发系列之线程池解析
11、并发系列之锁的知识梳理

······持续更新中······


  • 0
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值