文章目录
一、AQS高频问题
1.1 AQS是什么?
AQS是JUC下大量工具的基础类,很多工具都基于AQS实现的,比如lock锁,CountDownLatch,Semaphore,线程池等等都用到了AQS。
AQS中有一个核心属性state,还有一个双向链表以及一个单向链表。其中state是基于volatile修饰,再基于CAS修改,可以保证原子,可见,有序三大特性。单向链表是内部类ConditionObject对标synchronized中的等待池,当lock在线程持有锁时,执行await方法,会将线程封装为Node对象,扔到Condition单向链表中,等待唤醒。如果线程唤醒了,就将Condition中的Node扔到AQS的双向链表等待获取锁。
1.2 唤醒线程时,AQS为什么从后往前遍历?
当持有资源的线程执行完成后,需要在AQS的双向链表拿出来一个,如果head的next节点取消了
如果在唤醒线程时,head节点的next是第一个要被唤醒的,如果head的next节点取消了,会出现节点丢失问题。
如下图,当一个新的Node添加到链表时有3个步骤,当第三个步骤还未完成时,如果从head开始就找不到需要被唤醒的节点了。
1.3 AQS为什么用双向链表,(为啥不用单向链表)?
因为AQS中,存在取消节点的操作,如果使用双向链表只需要两步
- 需要将prev节点的next指针,指向next节点。
- 需要将next节点的prev指针,指向prev节点。
但是如果是单向链表,需要遍历整个单向链表才能完成的上述的操作。比较浪费资源。
1.4 AQS为什么要有一个虚拟的head节点
每个Node都会有一些状态,这个状态不单单针对自己,还针对后续节点
- 1:当前节点取消了。
- 0:默认状态,啥事没有。
- -1:当前节点的后继节点,挂起了。
- -2:代表当前节点在Condition队列中(await将线程挂起了)
- -3:代表当前是共享锁,唤醒时,后续节点依然需要被唤醒。
但是一个节点无法同时保存当前节点状态和后继节点状态,有一个哨兵节点,更方便操作。
1.5 ReentrantLock的底层实现原理
ReentrantLock是基于AQS实现的。
- 在线程基于ReentrantLock加锁时,需要基于CAS去修改state属性,如果能从0改为1,代表获取锁资源成功
- 如果CAS失败了,添加到AQS的双向链表中排队(可能会挂起线程),等待获取锁。
- 持有锁的线程,如果执行了condition的await方法,线程会封装为Node添加到Condition的单向链表中,等待被唤醒并且重新竞争锁资源
1.6 ReentrantLock的公平锁和非公平锁的区别
公平锁和非公平中的lock方法和tryAcquire方法的实现有点不同,其他都一样
- 非公平锁
- lock:直接尝试将state从 0 改为 1,如果成功,拿锁直接走,如果失败了,执行tryAcquire。
- tryAcquire:如果当前没有线程持有锁资源,直接再次尝试将state从0 改为 1 如果成功,拿锁直接走。
- 公平锁
- lock:直接执行tryAcquire。
- tryAcquire:如果当前没有线程持有锁资源,先看一下,有排队的么。如果没有排队的,直接尝试将state从 0 改为 1。如果有排队的并且第一名,直接尝试将state从 0 改为 1。
如果都没拿到锁,公平锁和非公平锁的后续逻辑是一样的,加入到AQS双向链表中排队。
1.7 ReentrantReadWriteLock如何实现的读写锁
如果一个操作写少读多,还用互斥锁的话,性能太低,因为读读不存在并发问题。读写锁可以解决该问题。
ReentrantReadWriteLock也是基于AQS实现的一个读写锁,但是锁资源用state标识。如何基于一个int来标识两个锁信息,有写锁,有读锁,怎么做的?
一个int,占了32个bit位。在写锁获取锁时,基于CAS修改state的低16位的值。在读锁获取锁时,基于CAS修改state的高16位的值。
写锁的重入,基于state低16直接标识,因为写锁是互斥的。读锁的重入,无法基于state的高16位去标识,因为读锁是共享的,可以多个线程同时持有。所以读锁的重入用的是ThreadLocal来表示,同时也会对state的高16为进行追加。
二、阻塞队列高频问题
2.1 说下你熟悉的阻塞队列?
ArrayBlockingQueue:底层基于数组实现,记得new的时候设置好边界。
LinkedBlockingQueue:底层基于链表实现的,可以认为是无界队列,但是可以设置长度。
PriorityBlockingQueue:底层是基于数组实现的二叉堆,可以认为是无界队列,因为数组会扩容。
ArrayBlockingQueue,LinkedBlockingQueue是ThreadPoolExecutor线程池最常用的两个阻塞队列。
2.2 虚假唤醒是什么?
虚假唤醒在阻塞队列的源码中就有体现。
比如消费者1在消费数据时,会先判断队列是否有元素,如果元素个数为0,消费者1会await挂起。此处判断元素为0的位置,如果用if循环会导致出现一个问题。
- 如果生产者添加了一个数据,会唤醒消费者1并去拿锁资源。
- 此时如果来了消费者2抢到了锁资源并带走了数据的话,消费者1再次拿到锁资源时,无法从队列获取到任何元素,出现虚假唤醒问题。
解决方案,将判断元素个数的位置,设置为while判断。
三、线程池高频问题
3.1 线程池的7个参数
核心线程数,最大线程数,最大空闲时间,时间单位,阻塞队列,线程工厂,拒绝策略
3.2 线程池的状态有什么,如何记录的?
线程池有5个状态:RUNINING、SHUTDOWN、STOP、TIDYING、TERMINATED
线程池的状态是在ctl属性中记录的。本质就是int类型
3.3 线程池常见的拒绝策略
-
AbortPolicy:丢弃任务并抛异常(默认)
-
CallerRunsPolicy:当前线程执行
-
DiscardPolicy:丢弃任务直接不要
-
DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执行当前任务
一般情况下,线程池自带的无法满足业务时,自定义一个线程池的拒绝策略,实现下面的接口即可。
3.4 线程池执行流程
核心线程不是new完就构建的,是懒加载的机制,添加任务才会构建核心线程
3.5 线程池为什么添加空任务的非核心线程
避免线程池出现队列有任务,但是没