java面试-并发
Java
什么是线程和进程?
进程是程序的一次执行过程,是系统运行程序的基本单位。 一个进程在其执行的过程中可以产生多个线程,与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
并发与并行的区别
- 并发:两个及两个以上的作业在同一 时间段 内执行。
- 并行:两个及两个以上的作业在同一 时刻 执行。
同步和异步的区别
- 同步:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
- 异步:调用在发出之后,不用等待返回结果,该调用直接返回。
为什么使用多线程
- 单核时代:在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。
- 多核时代: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 核心上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。
wait() 和 sleep() 方法的区别?
所属类不同,wait() 是Object 的实例方法,sleep() 是 Thread 的静态方法;
对锁的影响不同,如果当前线程持有锁,wait() 方法会释放锁,让其他线程进入 synchronized 代码块,sleep() 不会释放锁;
使用范围不同,wait() 只能在同步方法和同步控制块中使用,否则报错,sleep() 可以在任何地方使用;
恢复方式不同,wait() 需要其他线程调用同一对象的 nofiy()或 nofifyAll() 方法唤醒,sleep() 只要时间到了就恢复;
线程的sleep() 和yield() 有什么区别?
执行完 sleep() 后当前线程进入到超时等待状态,执行完yield() 状态当前线程进入就绪状态;
sleep() 方法给其他线程执行时,不考虑优先级,低优先级的线程有机会得到执行,yield() 方法只会给同级或更高优先级线程执行机会。
线程的join() 方法作用是啥?
线程A中执行了线程B的join()方法,表示线程A等待线程B终止了才继续执行。
编写多线程有几种方式?
继承 Thread 类
实现 Runnable 接口
实现 Callable 接口
Thread 调用start() 方法和run() 方法的区别?
run() 方法,普通方法调用,不创建线程,在当前线程中执行;
start() 方法,创建新线程,新线程处于就绪状态,当得到执行机会时,开始执行run() 方法。
线程的状态流转
新建(NEW):新建但尚未调用start启动线程;
就绪(READY):可以被执行,但还没有被CPU 执行;
运行(RUNNING):正在被CPU执行;
阻塞(BLOCKED):线程进入临界区,等待获取同步锁的状态;
等待(WAITING):除非被其他线程唤醒,否则一直等待;Object.wait() 需要另一个线程调用 Object.notify() 或 Object.notifyAll() 唤醒;Thread.join() 需要指定线程终止;
限时等待(TIMED_WAITING):超时时间到了,会自动被唤醒;
终止(TERMINATED):线程执行完毕退出的状态;
Synchronized 和 Lock 的区别是啥?
Lock 是一个接口,Synchronized 是 Java 关键字,语言内置支持;
Lock 需要手动获取和释放锁,Synchronized 自动获取和释放锁;
Lock 更灵活,可以响应中断,设置超时时间,Synchronized 无法响应中断;
如何检测死锁?
死锁的四个必要条件:
-
互斥条件:资源只能为进程独占;
-
请求和保持:进程等待某资源时,已经获得的资源不释放;
-
不可剥夺:进程自己的资源只能有自己释放,其他进程不能强占;
-
环路等待:进程A等待进程B占有的资源,进程B等待进程C占有的资源,进程C等待进程A占有的资源;
为什么使用线程池?
-
降低资源消耗,可以重复利用已创建的线程,降低线程创建和销毁的资源消耗;
-
提高响应度,任务可以不用等待线程创建就立即执行;
-
统一管理;
线程池的核心属性有哪些?
-
线程工厂(
threadFactory); -
核心线程数(
corePoolSize):提交线程时,当线程池中的线程数少于核心线程数,创建新线程,即使线程池中存在空闲线程; -
阻塞队列(
workQueue):用于保留任务,以便等待任务被工作线程执行; -
最大线程数(
maximumPoolSize):线程池中允许的最大线程数; -
拒绝策略(
handler):在向线程池添加任务时,执行拒绝策略的情况有两种:-
线程池的运行状态不是
RUNNING; -
线程池线程数已达到最大线程数,并且阻塞队列已满;
-
-
存活时间(
keepAliveTime):如果线程池中当前线程数大于核心线程数,多余的线程空闲时间超过存活时间就会被销毁。
线程池的运作流程?
向线程池提交任务时;
如果线程数没达到核心线程数,就创建线程执行任务;
否则看队列是否已满,如果没满,则加入队列;
如果队列已满看是否已达最大线程数,如果没达到最大线程数,则创建线程执行任务;
如果已达到最大线程数,就执行拒绝策略。

线程池有哪些拒绝策略?
-
终止策略(
AbortPolicy):直接抛出异常,由调用者处理,默认策略; -
抛弃策略(
DiscardPolicy):什么都不做,抛弃被拒绝的任务; -
抛弃最旧的策略(
DiscardOldestPolicy):抛弃队列中最旧的任务(如果是优先级队列,抛弃的是优先级最高的任务),然后重新提交任务; -
调用者运行策略(
CallerRunsPolicy):在调用者的线程中执行任务。
Thread.interrupt() 和Thread.interrupted() 的区别是什么?
Thread.interrupt() 方法,设置线程中断标志,让处于阻塞、有限期等待和无限期等待的线程,抛出 InterruptedException,中断线程执行,但不能中断IO阻塞和 synchronized 锁阻塞,线程没有处于阻塞或等待状态,只会设置中断标志,不会抛出异常,也不会终止执行。
Thread.interrupted()方法,检查线程中断标志,判断线程是否处于中断状态。
Synchronized 和 Reentrantlock 的区别
-
Synchronized是JVM实现,ReentrantLock是JDK 实现 -
Synchronized不可中断,ReentrantLock可中断 -
Synchronized是非公平锁,ReentrantLock可以是非公平锁,也可以是公平锁 -
Synchronized自动释放锁,ReentrantLock需要手动释放锁
什么是偏向锁、轻量级锁、重量级锁
偏向锁,在锁住对象的头部,记录持有锁的线程,持有锁的线程重复请求锁的时候,直接进入临界区;
轻量级锁,线程在请求锁的时候,其他线程已经持有锁,为了减少阻塞和唤醒的消耗,先以 CAS 方式尝试几次,如果持有锁的线程占用时间不长,就可以获取锁
重量级锁,在一定尝试次数内,没有获取到锁,阻塞请求线程,转换成重量级锁
什么是CAS
CAS就是对比交换,确定某个变量的值与预期一致,才更新,整个过程原子性,由硬件支持。
CAS有什么问题
-
ABA 问题,变量从A到B再到A,会认为变量没改变,解决方案,加递增版本号,
AtomicStampedReference -
长时间自旋,浪费CPU资源,有限自旋,设置尝试次数或时间限制。
-
只能保证一个变量的原子性,可以将多个变量放到对象里面,再通过
AtomicReference保证对象操作的原子性。
AQS底层原理

AQS 中的数据包含,一个代表共享资源的 state,双向链表结构的同步队列,队列节点包含代表的线程、线程等待状态以及前驱后继节点引用;AQS 的核心操作为 Acquire() 方法和 Release() 方法。
Acquire()方法的流程:
-
首先尝试申请资源,如果成功直接返回;
-
如果申请资源失败,以当前线程构建节点,以
CAS方式添加到同步队列尾部; -
然后阻塞当前线程,直到获取到资源,恢复执行;
-
恢复执行后,会检查等待过程中是否被中断过,有的话自我中断,否则直接返回;
Release() 方法的流程是,释放当前线程占用的资源,找到同步队列最前面的有效等待的线程唤醒。
Synchronized 的底层原理


对象的内存布局分为对象头、实例数据、对齐填充。对象头包含 MarkWord 和类型数据指针。MarkWord 中存储着对象锁相关的信息,其中锁状态分为无锁、偏向锁、轻量级锁、重量级锁。
加锁时,如果是无锁状态,就将锁升级为偏向锁,以 CAS 方式将 MarkWord 中记录的持锁线程设置为当前线程,偏向锁不会主动释放。
加锁时,如果是偏向锁状态,持锁线程是当前线程时,重入次数加1直接直接同步代码;持锁线程不是当前线程时,进一步确认,如果持锁线程不需要锁,设置锁状态为无锁状态,并重新尝试获锁;如果持锁线程需要锁,则升级为轻量级锁。
升级轻量级锁的过程,先在栈中创建锁记录,将其中的 ower 字段指向锁对象,以 CAS 方式在 MarkWord 中保存栈中锁记录地址,然后改锁状态。在轻量级获锁时,CAS 自旋尝试修改 MarkWord 中保存的栈中锁记录地址,尝试一定次数还未成功,就升级为重量级锁。
升级重量级锁的过程,在堆里面创建锁对象的监视器 moniterobject,然后在锁对象的 MarkWord 里面记录监视器的地址。
ReentrantLock 的底层原理及与 AQS 之间的关系
ReentrantLock 实现了 Lock 接口,Lock接口声明了 lock() 、unLock() 、newCondition() 主要方法,ReentrantLock 内部组合了 Sync 实例,Sync 是继承自 AQS 的抽象类,Sync 有两个具体实现类:一个是非公平锁,另一个是公平锁;非公平锁 lock 的时候,先尝试一次获锁,不成功再排队,公平锁 lock 时,同步队列为空时才尝试获锁,否则排队。
Condition的await/signal的底层原理

await() 方法的流程:
-
以当前线程构建节点,添加到等待队列尾部;
-
释放锁;
-
阻塞当前线程;
signal()方法的流程就是,将等待队列中的首节点,移动到同步队列尾部等待获取到锁,对应的线程才能继续执行。
ConcurrentHashMap底层数据结构是什么?
ConcurrentHashMap底层数据结构是 Node数组+链表+红黑树
ConcurrentHashMap 中的key和value 是否可以为空?
key 和 value 均不能为空
ConcurrentHashMap 中有哪几种节点
有5中节点:
-
Node节点:链表节点 -
TreeNode节点:红黑树节点 -
TreeBin节点:红黑树根节点的代理节点, -
ForwardingNode节点:扩容时,正在迁移的桶下面的链表会插入一个ForwardingNode头节点 -
ReservationNode节点:compute方法的占位节点
ConcurrentHashMap 中链表什么时候转换成红黑树
链表长度大于8,数组长度大于64的时候。
ConcurrentHashMap 中如何保证线程安全
hash冲突时,若桶为空,CAS 方式插入节点;非空就,Synchronized 加锁头节点,细化锁粒度,提升并发性能。
ConcurrentHashMap 的 put 流程
第一种情况数组为空,则初始化数组;
第二种情况数组对应位置为空,则以 CAS 方式设置新值;
第三种情况数组对应位置第一个节点的hash值为-1,表明当前节点正在迁移,则帮助迁移;
其他情况,获取对应位置第一个节点的对象锁,再分两种情况,第一种情况对应位置第一个节点的hash值是大于等于0 的,表明当前位置存的是链表,则遍历链表查找key相等的节点更新,如果没有key相等的节点则新增节点;第二种情况对应位置第一个节点是 TreeBin ,表明当前位置存的红黑树,就深度遍历查找key相等的节点更新,如果没找到key相等的节点则新增节点,添加完需要通过左右旋来重新平衡红黑树。
值放入以后,若容量超过阈值,则进行扩容。
ConcurrentHashMap 扩容流程
ConcurrentHashMap 扩容可以多线程进行,第一个线程开始时,根据CPU核心数和数组总长度确定步长,将迁移任务分为若干小任务,以2倍的容量创建新数组。每个线程每次按步长领取一个迁移任务,并将下一个任务的起始位置累加一个步长,将数据迁移到新数组后,将旧数组相应位置设置为 ForwardingNode,整个数组迁移完成后,将数组引用指向新数组。
ThreadLocal 的使用场景和原理
ThreadLocal 在多线程下,为每个线程保存一个副本,避免线程同步。Thread对象中有一个 ThreadLocalMap 以 ThreadLocal 为key,泛型值为value。
volatile、synchronized与硬件内存模型

现在的计算机处理器一般都有多个CPU,这些CPU除了公用主内存外,还有各自私有的缓存,如果一个CPU对一个变量的更改保存在自己的缓存中,而没有及时刷新到主存,其他CPU就会读取到脏数据,Java中 volatile 关键字确保变量被CPU处理完立即写回主存,对变量的读取直接从主存读取,从而保证变量在多个CPU之间的一致性;
如果一个CPU对共享变量的读取与写回期间,其他CPU 读取了主存中的这个共享变量,会导致当前CPU会的更改会被覆盖掉。使用 synchonized 关键字可以使CPU对共享变量的处理串行化,保证数据的一致性。