并发编程常见面试题汇总(二)

一、AQS高频问题:

1、AQS是什么?

AQS就是一个抽象队列同步器,abstract queued sychronizer,本质就是一个抽象类。

AQS中有一个核心属性state,其次还有一个双向链表以及一个单项链表。

  • state是基于volatile修饰,再基于CAS修改,同时可以保证三大特性。(原子,可见,有序);同时,state就是一个int类型的数值,同步状态,至于到底是什么状态,看子类实现。

  • 一个双向链表。有Node对象组成的双向链表。

  • 在Condition内部类中,还提供了一个由Node对象组成的单向链表。

AQS是JUC下大量工具的基础类,很多工具都基于AQS实现的,比如lock锁,CountDownLatch,Semaphore,线程池等等都用到了AQS。


condition和单向链表是啥:都知道sync内部提供了wait方法和notify方法的使用,lock锁也需要实现这种机制,lock锁就基于AQS内部的Condition实现了await和signal方法。(对标sync的wait和notify)


sync在线程持有锁时,执行wait方法,会将线程扔到WaitSet等待池中排队,等待唤醒

lcok在线程持有锁时,执行await方法,会将线程封装为Node对象,扔到Condition单向链表中,等待唤醒


Condition在做了什么:将持有锁的线程封装为Node扔到Condition单向链表,同时挂起线程。如果线程唤醒了,就将Condition中的Node扔到AQS的双向链表等待获取锁。

2 、唤醒线程时,AQS为什么从后往前遍历?

如果线程没有获取到资源,就需要将线程封装为Node对象,安排到AQS的双向链表中排队,并且可能会挂起线程

如果在唤醒线程时,head节点的next是第一个要被唤醒的,如果head的next节点取消了,AQS的逻辑是从tail节点往前遍历,找到离head最近的有效节点。

一个Node对象,是如何添加到双向链表中的?

基于addWaiter方法中,是先将当前Node的prev指向tail的节点,再将tail指向当前Node自己,再让prev节点指向Node自己。
如下图,如果只执行到了2步骤,此时,Node加入到了AQS队列中,但是从prev节点往后,会找不到当前节点。

image.png

3 、AQS为什么用双向链表,为啥不用单向链表?

因为AQS中,存在取消节点的操作,节点被取消后,需要从AQS的双向链表中断开连接。

还需要保证双向链表的完整性,

  • 需要将prev节点的next指针,指向next节点。
  • 需要将next节点的prev指针,指向prev节点。

如果正常的双向链表,直接操作就可以了。

但是如果是单向链表,需要遍历整个单向链表才能完成的上述的操作。比较浪费资源。

4 、AQS为什么要有一个虚拟的head节点

因为AQS内部,Node节点的wait status,表示很多信息,除了当前节点的状态,还会维护后继节点状态。状态如下:

  • 1:当前节点取消了。
  • 0:默认状态,啥事没有。
  • -1:当前节点的后继节点,挂起了。
  • -2:代表当前节点在Condition队列中(await将线程挂起了)
  • -3:代表当前是共享锁,唤醒时,后续节点依然需要被唤醒。

如果取消虚拟的head节点,一个节点无法同时保存当前阶段状态和后继节点状态。

同时,在释放锁资源时,就要基于head节点的状态是否是-1。来决定是否唤醒后继节点:

  • 如果为-1,正常唤醒

  • 如果不为-1,不需要唤醒吗,减少了一次可能发生的遍历操作,提升性能。

5 、ReentrantLock的底层实现原理

ReentrantLock是基于AQS实现的。

在线程基于ReentrantLock加锁时,需要基于CAS去修改state属性,如果能从0改为1,代表获取锁资源成功

如果CAS失败了,添加到AQS的双向链表中排队(可能会挂起线程),等待获取锁。

持有锁的线程,如果执行了condition的await方法,线程会封装为Node添加到Condition的单向链表中,等待被唤醒并且重新竞争锁资源

Java中除了线程池中Worker的锁之外,都是可重入锁。

6、 ReentrantLock的公平锁和非公平锁的区别

公平锁和非公平中的lock方法和tryAcquire方法的实现有一点不同,其他都一样

  • 非公平锁lock:直接尝试将state从 0 ~ 1,如果成功,拿锁直接走,如果失败了,执行tryAcquire
  • 公平锁lock:直接执行tryAcquire
  • 非公平锁tryAcquire:如果当前没有线程持有锁资源,直接再次尝试将state从 0 ~ 1如果成功,拿锁直接走
  • 公平锁tryAcquire:如果当前没有线程持有锁资源,先看一下,有排队的么。
    • 如果没有排队的,直接尝试将state从 0 ~ 1
    • 如果有排队的,第一名不是我,不抢,继续等待。
    • 如果有排队的,我是第一名,直接尝试将state从 0 ~ 1
  • 如果都没拿到锁,公平锁和非公平锁的后续逻辑是一样的,排队后,就不存在所谓的插队。

7、 ReentrantReadWriteLock如何实现的读写锁

如果一个操作写少读多,还用互斥锁的话,性能太低,因为读读不存在并发问题。

怎么解决 => 有读写锁的出现。

ReentrantReadWriteLock也是基于AQS实现的一个读写锁,但是锁资源用state标识,一个int,占了32个bit位。

在写锁获取锁时,基于CAS修改state的低16位的值。

在读锁获取锁时,基于CAS修改state的高16位的值。

写锁的重入,基于state低16直接标识,因为写锁是互斥的。

读锁的重入,无法基于state的高16位去标识,因为读锁是共享的,可以多个线程同时持有。

所以读锁的重入次数用的是ThreadLocal来表示,同时也会对state的高16为进行追加。

二、阻塞队列高频问题:

1 、常见阻塞队列有哪些?

ArrayBlockingQueue:底层基于数组实现,记得new的时候设置好边界。

LinkedBlockingQueue:底层基于链表实现的,可以认为是无界队列,但是可以设置长度。

PriorityBlockingQueue:底层是基于数组实现的二叉堆,可以认为是无界队列,因为数组会扩容。

ArrayBlockingQueue,LinkedBlockingQueue是ThreadPoolExecutor线程池最常用的两个阻塞队列。

PriorityBlockingQueue:是ScheduleThreadPoolExecutor定时任务线程池用的阻塞队列跟PriorityBlockingQueue的底层实现是一样的。(其实本质用的是DelayWorkQueue,也是基于数组实现的二叉堆)

2、 虚假唤醒是什么?

虚假唤醒在阻塞队列的源码中就有体现,如下:

例如:ArrayBlockingQueue的take()

    public E take() throws InterruptedException {
        ReentrantLock lock = this.lock;
        lock.lockInterruptibly();

        Object var2;
        try {
           // while判断 + await(),是虚假唤醒
            while(this.count == 0) {
                this.notEmpty.await();
            }

            var2 = this.dequeue();
        } finally {
            lock.unlock();
        }

        return var2;
    }

比如消费者1在消费数据时,会先判断队列是否有元素,如果元素个数为0,消费者1会挂起。

此处判断元素为0的位置,如果用if循环会导致出现一个问题:

如果生产者添加了一个数据,会唤醒消费者1。

但是如果消费者1没拿到锁资源,消费者2拿到了锁资源并带走了数据的话。

消费者1再次拿到锁资源时,无法从队列获取到任何元素。导致出现逻辑问题。

解决方案,将判断元素个数的位置,设置为while判断。

三、线程池高频问题

1 、线程池的7个参数

核心线程数,最大线程数,最大空闲时间,时间单位,阻塞队列,线程工厂,拒绝策略

2 、线程池的状态有什么,如何记录的?

线程池有5个状态:

image.png

线程池的状态是在ctl属性中记录的。本质就是int类型

image.png

ctl的高3位记录线程池状态

低29位,记录工作线程个数。即便指定的线程最大数量是Integer.MAX_VALUE也到不了。

最大工作线程数:2^29-1

3、 线程池常见的拒绝策略

AbortPolicy:抛异常(默认)

image.png

CallerRunsPolicy,谁提交的任务,谁执行。异步变同步(尽量不要用,会让主线程阻塞,异步变同步)

image.png

DiscardPolicy:任务直接不要

image.png

DiscardOldestPolicy:把最早放过来的任务丢失,再次尝试将当前任务交给线程池处理

image.png

一般情况下,线程池自带的无法满足业务时,自定义一个线程池的拒绝策略。实现下面的接口即可。r:任务;executor:线程池

image.png

4、 线程池执行流程

2个核心线程 5个最大线程 阻塞队列长度为2

image.png

注:核心线程不是new完就构建的,是懒加载的机制,添加任务才会构建核心线程

5、 线程池为什么添加空任务的非核心线程

image.png

场景1:线程池可以设置核心线程数是0个。这样,任务扔到阻塞队列,但是没有工作线程

场景2:线程池中的核心线程不是一定不会被回收,线程池中有一个属性,如果设置为true,核心线程也会被干掉

image.png

为解决以上场景兜底:避免线程池出现工作队列有任务,但是没有工作线程处理。

6 、在没任务时,线程池中的工作线程在干嘛?

线程会挂起,默认核心线程是WAITING状态,非核心是TIMED_WAITING。

(注:线程挂起是不占CPU,但占内存)

如果是核心线程,默认情况下,会在阻塞队列的位置执行take方法,直到拿到任务为止。

如果是非核心线程,默认情况下,会在阻塞队列的位置执行poll方法,等待最大空闲时间,如果没任务,直接退出,如果有活,那就正常干。

7、 工作线程出现异常会导致什么问题?

导致的问题及回答:

问题1、是否抛出异常?

回答:

如果任务是execute方法执行的,工作线程会将异常抛出。

如果任务是submit方法执行的futureTask,工作线程会将异常捕获并保存到FutureTask里,可以基于futureTask的get得到异常信息

问题2、影响其他线程吗?

回答:出现异常的工作线程不会影响到其他的工作线程。

问题3、工作线程会挂吗?

回答:runWorker中的异常会被抛到run方法中,run方法会异常结束,run方法结束,线程就结束了

如果是submit,异常没抛出来,那就不挂

拓展:线程在执行任务的时候,只根据数量,如果核心线程挂了,那非核心线程就成为核心线程。(凑核心线程的数量)

8 、工作线程继承AQS的目的是什么?

工作线程的本质,就是Worker对象

继承AQS跟shutdown和shutdownNow有关系。

如果是shutdown,会中断空闲的工作线程,基于Worker实现的AQS中的state的值来判断线程是否在干活,从而知道能否中断工作线程。

runWorker(Worker w)里,线程先w.lock(),将当前线程状态设置为1,结束任务后再w.unlock(),将当前线程状态设置为0。

如果工作线程的state是0,代表空闲,可以中断,如果是1,代表正在干活。

扩展:interruptIdleWorkers(boolean onlyOne) 里有w.tryLock(),尝试将状态变为0。

w.tryLock()用的是tryAcquire()

如果是shutdownNow,直接强制中断所有工作线程

9 、核心参数怎么设置?

线程池的目的是为了充分发挥CPU的资源。提升整个系统的性能

系统内部不同业务的线程池参考的方式也不一样。

如果是CPU密集的任务(计算,判断,循环多),一般也就是CPU内核数 + 1的核心线程数。这样足以充分发挥CPU性能。

如果是IO密集的任务(crud多或一个业务要查询三个服务),因为IO的程度不一样,有的是1s,有的是1ms,有的是1分钟,所以IO密集的任务在用线程池处理时,一定要通过压测的方式,观察CPU资源的占用情况,来决定核心线程数。一般发挥CPU性能到70~80足矣。所以线程池的参数设置需要通过压测以及多次调整才能得出具体的。

IO密集一般是n倍CPU数

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值