并发面试题总结(续juc+并发)

面试题总结

线程池(七大参数、怎么用、为什么用)
进程线程(区别、联系)
线程通信方式
信号量(概念、使用)
反射(作用)

AQS 了解吗

AQS 队列同步器是用来构建锁或其他同步组件的基础框架,它使用一个 volatile int state 变量作为共享资源,如果线程获取资源失败,则进入同步队列等待;如果获取成功就执行临界区代码,释放资源时会通知同步队列中的等待线程。

同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,对同步状态进行更改需要使用同步器提供的 3个方法 getState、setState 和 compareAndSetState ,它们保证状态改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅定义若干同步状态获取和释放的方法,同步器既支持独占式也支持共享式。

同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。锁面向使用者,定义了使用者与锁交互的接口,隐藏实现细节;同步器面向锁的实现者,简化了锁的实现方式,屏蔽了同步状态管理、线程排队、等待与唤醒等底层操作。

每当有新线程请求资源时都会进入一个等待队列,只有当持有锁的线程释放锁资源后该线程才能持有资源。等待队列通过双向链表实现,线程被封装在链表的 Node 节点中,Node 的等待状态包括:CANCELLED(线程已取消)、SIGNAL(线程需要唤醒)、CONDITION (线程正在等待)、PROPAGATE(后继节点会传播唤醒操作,只在共享模式下起作用)。

AQS 有哪两种模式?

独占模式exclusive表示锁只会被一个线程占用,其他线程必须等到持有锁的线程释放锁后才能获取锁,同一时间只能有一个线程获取到锁。

共享模式shared表示多个线程获取同一个锁有可能成功,ReadLock 就采用共享模式。

独占模式通过 acquire 和 release 方法获取和释放锁,共享模式通过 acquireShared 和 releaseShared 方法获取和释放锁。

AQS 独占式获取/释放锁的原理

获取同步状态时,调用 acquire 方法,维护一个同步队列,使用 tryAcquire 方法安全地获取线程同步状态,获取失败的线程会被构造同步节点并通过 addWaiter 方法加入到同步队列的尾部,在队列中自旋。之后调用 acquireQueued 方法使得该节点以死循环的方式获取同步状态,如果获取不到则阻塞,被阻塞线程的唤醒主要依靠前驱节点的出队或被中断实现,移出队列或停止自旋的条件是前驱节点是头结点且成功获取了同步状态。

释放同步状态时,同步器调用 tryRelease 方法释放同步状态,然后调用 unparkSuccessor 方法唤醒头节点的后继节点,使后继节点重新尝试获取同步状态。

为什么只有前驱节点是头节点时才能尝试获取同步状态?

头节点是成功获取到同步状态的节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。

目的是维护同步队列的 FIFO 原则,节点和节点在循环检查的过程中基本不通信,而是简单判断自己的前驱是否为头节点,这样就使节点的释放规则符合 FIFO,并且也便于对过早通知的处理,过早通知指前驱节点不是头节点的线程由于中断被唤醒。

AQS 共享式式获取/释放锁的原理?

获取同步状态时,调用 acquireShared 方法,该方法调用 tryAcquireShared 方法尝试获取同步状态,返回值为 int 类型,返回值不小于于 0 表示能获取同步状态。因此在共享式获取锁的自旋过程中,成功获取同步状态并退出自旋的条件就是该方法的返回值不小于0。

释放同步状态时,调用 releaseShared 方法,释放后会唤醒后续处于等待状态的节点。它和独占式的区别在于 tryReleaseShared 方法必须确保同步状态安全释放,通过循环 CAS 保证,因为释放同步状态的操作会同时来自多个线程。

线程的生命周期有哪些状态?

NEW:新建状态,线程被创建且未启动,此时还未调用 start 方法。

RUNNABLE:Java 将操作系统中的就绪和运行两种状态统称为 RUNNABLE,此时线程有可能在等待时间片,也有可能在执行。

BLOCKED:阻塞状态,可能由于锁被其他线程占用、调用了 sleep 或 join 方法、执行了 wait方法等。

WAITING:等待状态,该状态线程不会被分配 CPU 时间片,需要其他线程通知或中断。可能由于调用了无参的 wait 和 join 方法。

TIME_WAITING:限期等待状态,可以在指定时间内自行返回。导可能由于调用了带参的 wait 和 join 方法。

TERMINATED:终止状态,表示当前线程已执行完毕或异常退出。

线程的创建方式有哪些

① 继承 Thread 类并重写 run 方法。实现简单,但不符合里氏替换原则,不可以继承其他类。

② 实现 Runnable 接口并重写 run 方法。避免了单继承局限性,编程更加灵活,实现解耦。

③实现 Callable 接口并重写 call 方法。可以获取线程执行结果的返回值,并且可以抛出异常。

线程有哪些方法

① sleep 方***导致当前线程进入休眠状态,与 wait 不同的是该方法不会释放锁资源,进入的是 TIMED-WAITING 状态。

② yiled 方法使当前线程让出 CPU 时间片给优先级相同或更高的线程,回到 RUNNABLE 状态,与其他线程一起重新竞争CPU时间片。

③ join 方法用于等待其他线程运行终止,如果当前线程调用了另一个线程的 join 方法,则当前线程进入阻塞状态,当另一个线程结束时当前线程才能从阻塞状态转为就绪态,等待获取CPU时间片。底层使用的是wait,也会释放锁。
sleep不会释放锁资源,wait会释放锁资源,sleep是thread里面的方法,wait是object里面的方法

什么是守护线程?

守护线程是一种支持型线程,可以通过 setDaemon(true) 将线程设置为守护线程,但必须在线程启动前设置。

守护线程被用于完成支持性工作,但在 JVM 退出时守护线程中的 finally 块不一定执行,因为 JVM 中没有非守护线程时需要立即退出,所有守护线程都将立即终止,不能靠在守护线程使用 finally 确保关闭资源。

线程通信的方式有哪些?

命令式编程中线程的通信机制有两种,共享内存和消息传递。在共享内存的并发模型里线程间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。在消息传递的并发模型里线程间没有公共状态,必须通过发送消息来显式通信。Java 并发采用共享内存模型,线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

volatile 告知程序任何对变量的读需要从主内存中获取,写必须同步刷新回主内存,保证所有线程对变量访问的可见性。

synchronized 确保多个线程在同一时刻只能有一个处于方法或同步块中,保证线程对变量访问的原子性、可见性和有序性。

等待通知机制指一个线程 A 调用了对象的 wait 方法进入等待状态,另一线程 B 调用了对象的 notify/notifyAll 方法,线程 A 收到通知后结束阻塞并执行后序操作。对象上的 wait 和 notify/notifyAll 如同开关信号,完成等待方和通知方的交互。

如果一个线程执行了某个线程的 join 方法,这个线程就会阻塞等待执行了 join 方法的线程终止,这里涉及等待/通知机制。join 底层通过 wait 实现,线程终止时会调用自身的 notifyAll 方法,通知所有等待在该线程对象上的线程。

ThreadLocal 是线程共享变量,但它可以为每个线程创建单独的副本,副本值是线程私有的,互相之间不影响。

线程池有什么好处?

降低资源消耗,复用已创建的线程,降低开销、控制最大并发数。

隔离线程环境,可以配置独立线程池,将较慢的线程与较快的隔离开,避免相互影响。

实现任务线程队列缓冲策略和拒绝机制。

实现某些与时间相关的功能,如定时执行、周期执行等。

线程池处理任务的流程?

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出在务来执行。

① 核心线程池未满,创建一个新的线程执行任务,此时 workCount < corePoolSize。

② 如果核心线程池已满,工作队列未满,将线程存储在工作队列,此时 workCount >= corePoolSize。

③ 如果工作队列已满,线程数小于最大线程数就创建一个新线程处理任务,此时 workCount < maximumPoolSize,这一步也需要获取全局锁。

④ 如果超过大小线程数,按照拒绝策略来处理任务,此时 workCount > maximumPoolSize。

线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执行完任务后还会循环获取工作队列中的任务来执行。

有哪些创建线程池的方法?

可以通过 Executors 的静态工厂方法创建线程池:

newFixedThreadPool,固定大小的线程池,核心线程数也是最大线程数,不存在空闲线程,keep AliveTime = 0。该线程池使用的工作队列是无界阻塞队列 LinkedBlockingQueue,适用于负载较重的服务器。

newSingleThreadExecutor,使用单线程,相当于单线程串行执行所有任务,适用于需要保证顺序执行任务的场景。

newCachedThreadPool,maximumPoolSize 设置为 Integer 最大值,是高度可伸缩的线程池。该线程池使用的工作队列是没有容量的 SynchronousQueue,如果主线程提交任务的速度高于线程处理的速度,线程池会不断创建新线程,极端情况下会创建过多线程而耗尽CPU 和内存资源。适用于执行很多短期异步任务的小程序或负载较轻的服务器。

④ newScheduledThreadPool:线程数最大为 Integer 最大值,存在 OOM 风险。支持定期及周期性任务执行,适用需要多个后台线程执行周期任务,同时需要限制线程数量的场景。相比 Timer 更安全,功能更强,与 newCachedThreadPool 的区别是不回收工作线程。

⑤ newWorkStealingPool:JDK8 引入,创建持有足够线程的线程池支持给定的并行度,通过多个队列减少竞争。

创建线程池有哪些参数?

① corePoolSize:常驻核心线程数,如果为 0,当执行完任务没有任何请求时会消耗线程池;如果大于 0,即使本地任务执行完,核心线程也不会被销毁。该值设置过大会浪费资源,过小会导致线程的频繁创建与销毁。

② maximumPoolSize:线程池能够容纳同时执行的线程最大数,必须大于等于 1,如果与核心线程数设置相同代表固定大小线程池。

③ keep AliveTime:线程空闲时间,线程空闲时间达到该值后会被销毁,直到只剩下 corePoolSize 个线程为止,避免浪费内存资源。

④ unit:keep AliveTime 的时间单位。

⑤ workQueue:工作队列,当线程请求数大于等于 corePoolSize 时线程会进入阻塞队列。

⑥ threadFactory:线程工厂,用来生产一组相同任务的线程。可以给线程命名,有利于分析错误。

⑦ handler:拒绝策略,默认使用 AbortPolicy 丢弃任务并抛出异常,CallerRunsPolicy 表示重新尝试提交该任务,DiscardOldestPolicy 表示抛弃队列里等待最久的任务并把当前任务加入队列,DiscardPolicy 表示直接抛弃当前任务但不抛出异常。

如何关闭线程池

可以调用 shutdown 或 shutdownNow 方法关闭线程池,原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法中断线程,无法响应中断的任务可能永远无法终止。

区别是 shutdownNow 首先将线程池的状态设为 STOP,然后尝试停止正在执行或暂停任务的线程,并返回等待执行任务的列表。而 shutdown 只是将线程池的状态设为 SHUTDOWN,然后中断没有正在执行任务的线程。

通常调用 shutdown 来关闭线程池,如果任务不一定要执行完可调用 shutdownNow。

线程池的选择策略有什么?

可以从以下角度分析:①任务性质:CPU 密集型、IO 密集型和混合型。②任务优先级。③任务执行时间。④任务依赖性:是否依赖其他资源,如数据库连接。

性质不同的任务可用不同规模的线程池处理,CPU 密集型任务应配置尽可能小的线程,如配置 Ncpu+1 个线程的线程池。由于 IO 密集型任务线程并不是一直在执行任务,应配置尽可能多的线程,如 2*Ncpu。混合型的任务,如果可以拆分,将其拆分为一个 CPU 密集型任务和一个 IO 密集型任务,只要两个任务执行的时间相差不大那么分解后的吞吐量将高于串行执行的吞吐量,如果相差太大则没必要分解。

优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 处理。

执行时间不同的任务可以交给不同规模的线程池处理,或者使用优先级队列让执行时间短的任务先执行。

依赖数据库连接池的任务,由于线程提交 SQL 后需要等待数据库返回的结果,等待的时间越长 CPU 空闲的时间就越长,因此线程数应该尽可能地设置大一些,提高 CPU 的利用率。

建议使用有界队列,能增加系统的稳定性和预警能力,可以根据需要设置的稍微大一些。

阻塞队列有哪些选择?

阻塞队列支持阻塞插入和移除,当队列满时,阻塞插入元素的线程直到队列不满。当队列为空时,获取元素的线程会被阻塞直到队列非空。阻塞队列常用于生产者和消费者的场景,阻塞队列就是生产者用来存放元素,消费者用来获取元素的容器。

Java 中的阻塞队列

ArrayBlockingQueue,由数组组成的有界阻塞队列,默认情况下不保证线程公平,有可能先阻塞的线程最后才访问队列。

LinkedBlockingQueue,由链表结构组成的有界阻塞队列,队列的默认和最大长度为 Integer 最大值。

PriorityBlockingQueue,支持优先级的无界阻塞队列,默认情况下元素按照升序排序。可自定义 compareTo 方法指定排序规则,或者初始化时指定 Comparator 排序,不能保证同优先级元素的顺序。

DelayQueue,支持延时获取元素的无界阻塞队列,使用优先级队列实现。创建元素时可以指定多久才能从队列中获取当前元素,只有延迟期满时才能从队列中获取元素,适用于缓存和定时调度。

实现原理

使用通知模式实现,生产者往满的队列里添加元素时会阻塞,当消费者消费后,会通知生产者当前队列可用。当往队列里插入一个元素,如果队列不可用,阻塞生产者主要通过 LockSupport 的 park 方法实现,不同操作系统中实现方式不同,在 Linux 下使用的是系统方法 pthread_cond_wait 实现。

谈一谈 ThreadLocal

ThreadLoacl 是线程共享变量,主要用于一个线程内跨类、方法传递数据。ThreadLoacl 有一个静态内部类 ThreadLocalMap,其 Key 是 ThreadLocal 对象,值是 Entry 对象,Entry 中只有一个 Object 类的 vaule 值。ThreadLocal 是线程共享的,但 ThreadLocalMap 是每个线程私有的。ThreadLocal 主要有 set、get 和 remove 三个方法。

set 方法

首先获取当前线程,然后再获取当前线程对应的 ThreadLocalMap 类型的对象 map。如果 map 存在就直接设置值,key 是当前的 ThreadLocal 对象,value 是传入的参数。

如果 map 不存在就通过 createMap 方法为当前线程创建一个 ThreadLocalMap 对象再设置值。

get 方法

首先获取当前线程,然后再获取当前线程对应的 ThreadLocalMap 类型的对象 map。如果 map 存在就以当前 ThreadLocal 对象作为 key 获取 Entry 类型的对象 e,如果 e 存在就返回它的 value 属性。

如果 e 不存在或者 map 不存在,就调用 setInitialValue 方法先为当前线程创建一个 ThreadLocalMap 对象然后返回默认的初始值 null。

remove 方法

首先通过当前线程获取其对应的 ThreadLocalMap 类型的对象 m,如果 m 不为空,就解除 ThreadLocal 这个 key 及其对应的 value 值的联系。

存在的问题

线程复用会产生脏数据,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被重用。如果没有调用 remove 清理与线程相关的 ThreadLocal 信息,那么假如下一个线程没有调用 set 设置初始值就可能 get 到重用的线程信息。

ThreadLocal 还存在内存泄漏的问题,由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此当 ThreadLocal 被垃圾回收后,value 依旧不会被释放。因此需要及时调用 remove 方法进行清理操作。

ArrayBlockingQueue:是一个基于数组结构的有界阻塞风列,此队列按FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列接FIFO(先进先出)排序元素,吞吐量通常要高于ArrayBlockingQueue。
synchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值