怎么套模板 网站,直播型网站开发,使网站有流量,wordpress内插件翻译并发编程面试题2
一、AQS高频问题#xff1a;
1.1 AQS是什么#xff1f;
AQS就是一个抽象队列同步器#xff0c;abstract queued sychronizer#xff0c;本质就是一个抽象类。
AQS中有一个核心属性state#xff0c;其次还有一个双向链表以及一个单项链表。
首先state…并发编程面试题2
一、AQS高频问题
1.1 AQS是什么
AQS就是一个抽象队列同步器abstract queued sychronizer本质就是一个抽象类。
AQS中有一个核心属性state其次还有一个双向链表以及一个单项链表。
首先state是基于volatile修饰再基于CAS修改同时可以保证三大特性。原子可见有序
其次还提供了一个双向链表。有Node对象组成的双向链表。
最后在Condition内部类中还提供了一个由Node对象组成的单向链表。
AQS是JUC下大量工具的基础类很多工具都基于AQS实现的比如lock锁CountDownLatchSemaphore线程池等等都用到了AQS。 state是啥state就是一个int类型的数值同步状态至于到底是什么状态看子类实现。
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的双向链表等待获取锁。
1.2 唤醒线程时AQS为什么从后往前遍历
如果线程没有获取到资源就需要将线程封装为Node对象安排到AQS的双向链表中排队并且可能会挂起线程
如果在唤醒线程时head节点的next是第一个要被唤醒的如果head的next节点取消了AQS的逻辑是从tail节点往前遍历找到离head最近的有效节点
想解释清楚这个问题需要先了解一个Node对象是如何添加到双向链表中的。
基于addWaiter方法中是先将当前Node的prev指向tail的节点再将tail指向我自己再让prev节点指向我
如下图如果只执行到了2步骤此时Node加入到了AQS队列中但是从prev节点往后会找不到当前节点。 1.3 AQS为什么用双向链表为啥不用单向链表
因为AQS中存在取消节点的操作节点被取消后需要从AQS的双向链表中断开连接。
还需要保证双向链表的完整性
需要将prev节点的next指针指向next节点。需要将next节点的prev指针指向prev节点。
如果正常的双向链表直接操作就可以了。
但是如果是单向链表需要遍历整个单向链表才能完成的上述的操作。比较浪费资源。
1.4 AQS为什么要有一个虚拟的head节点
有一个哨兵节点更方便操作。
另一个是因为AQS内部每个Node都会有一些状态这个状态不单单针对自己还针对后续节点
1当前节点取消了。0默认状态啥事没有。-1当前节点的后继节点挂起了。-2代表当前节点在Condition队列中await将线程挂起了-3代表当前是共享锁唤醒时后续节点依然需要被唤醒。
Node节点的ws表示很多信息除了当前节点的状态还会维护后继节点状态。
如果取消虚拟的head节点一个节点无法同时保存当前阶段状态和后继节点状态。
同时在释放锁资源时就要基于head节点的状态是否是-1。来决定是否唤醒后继节点。
如果为-1正常唤醒
如果不为-1不需要唤醒吗减少了一次可能发生的遍历操作提升性能。
1.5 ReentrantLock的底层实现原理
ReentrantLock是基于AQS实现的。
在线程基于ReentrantLock加锁时需要基于CAS去修改state属性如果能从0改为1代表获取锁资源成功
如果CAS失败了添加到AQS的双向链表中排队可能会挂起线程等待获取锁。
持有锁的线程如果执行了condition的await方法线程会封装为Node添加到Condition的单向链表中等待被唤醒并且重新竞争锁资源
Java中除了一会讲到的线程池中Worker的锁之外都是可重入锁。
在基础面试题2中波波老师说到了Synchronizer和ReentrantLock的区别
1.6 ReentrantLock的公平锁和非公平锁的区别
注意想回答的准确些就别举生活中的列子用源码的角度去说。
如果用生活中的例子你就就要用有些人没素质但是又有素质。
源码
公平锁和非公平中的lock方法和tryAcquire方法的实现有一内内不同其他都一样 非公平锁lock直接尝试将state从 0 ~ 1如果成功拿锁直接走如果失败了执行tryAcquire公平锁lock直接执行tryAcquire非公平锁tryAcquire如果当前没有线程持有锁资源直接再次尝试将state从 0 ~ 1如果成功拿锁直接走公平锁tryAcquire如果当前没有线程持有锁资源先看一下有排队的么。 如果没有排队的直接尝试将state从 0 ~ 1如果有排队的第一名不是我不抢继续等待。如果有排队的我是第一名直接尝试将state从 0 ~ 1 如果都没拿到锁公平锁和非公平锁的后续逻辑是一样的排队后就不存在所谓的插队。
生活的例子非公平锁会有机会尝试强行获取锁资源两次成功开开心心走人失败消消停停去排队。
有个人前来做核酸 公平锁先看眼有排队的么有就去排队非公平锁不管什么情况先尝试做凳子上。如果坐上了直接被扣扣完走人如果没做到凳子上 有人正在扣嗓子眼么 没人正在被扣上去尝试做凳子上成功了扣完走人。如果有人正在扣消停去排队。
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 说下你熟悉的阻塞队列
ArrayBlockingQueueLinkedBlockingQueuePriorityBlockingQueue
ArrayBlockingQueue底层基于数组实现记得new的时候设置好边界。
LinkedBlockingQueue底层基于链表实现的可以认为是无界队列但是可以设置长度。
PriorityBlockingQueue底层是基于数组实现的二叉堆可以认为是无界队列因为数组会扩容。
ArrayBlockingQueueLinkedBlockingQueue是ThreadPoolExecutor线程池最常用的两个阻塞队列。
PriorityBlockingQueue是ScheduleThreadPoolExecutor定时任务线程池用的阻塞队列跟PriorityBlockingQueue的底层实现是一样的。其实本质用的是DelayWorkQueue
2.2 虚假唤醒是什么
虚假唤醒在阻塞队列的源码中就有体现。
比如消费者1在消费数据时会先判断队列是否有元素如果元素个数为0消费者1会挂起。
此处判断元素为0的位置如果用if循环会导致出现一个问题。
如果生产者添加了一个数据会唤醒消费者1。
但是如果消费者1没拿到锁资源消费者2拿到了锁资源并带走了数据的话。
消费者1再次拿到锁资源时无法从队列获取到任何元素。导致出现逻辑问题。
解决方案将判断元素个数的位置设置为while判断。
三、线程池高频问题最重要的点
3.1 线程池的7个参数不会就回家等通知
核心线程数最大线程数最大空闲时间时间单位阻塞队列线程工厂拒绝策略
3.2 线程池的状态有什么如何记录的
线程池不是什么时候都接活的
线程池有5个状态。、 线程池的状态是在ctl属性中记录的。本质就是int类型 ctl的高三位记录线程池状态
低29位记录工作线程个数。即便你指定的线程最大数量是Integer.MAX_VALUE他也到不了
3.3 线程池常见的拒绝策略不会就回家等通知
AbortPolicy抛异常默认 CallerRunsPolicy谁提交的任务谁执行。异步变同步 DiscardPolicy任务直接不要 DiscardOldestPolicy把最早放过来的任务丢失再次尝试将当前任务交给线程池处理 一般情况下线程池自带的无法满足业务时自定义一个线程池的拒绝策略。
实现下面的接口即可。 3.4 线程池执行流程不会就回家等通知
核心线程不是new完就构建的是懒加载的机制添加任务才会构建核心线程
2个核心线程 5个最大线程 阻塞队列长度为2 3.5 线程池为什么添加空任务的非核心线程 避免线程池出现工作队列有任务但是没有工作线程处理。
线程池可以设置核心线程数是0个。这样任务扔到阻塞队列但是没有工作线程这不凉凉了么~~
线程池中的核心线程不是一定不会被回收线程池中有一个属性如果设置为true核心线程也会被干掉 3.6 在没任务时线程池中的工作线程在干嘛
线程会挂起默认核心线程是WAITING状态非核心是TIMED_WAITING
如果是核心线程默认情况下会在阻塞队列的位置执行take方法直到拿到任务为止。
如果是非核心线程默认情况下会在阻塞队列的位置执行poll方法等待最大空闲时间如果没任务直接拉走咔嚓掉如果有活那就正常干。
3.7 工作线程出现异常会导致什么问题
是否抛出异常、影响其他线程吗、工作线程会嘎嘛
如果任务是execute方法执行的工作线程会将异常抛出。
如果任务是submit方法执行的futureTask工作线程会将异常捕获并保存到FutureTask里可以基于futureTask的get得到异常信息
出现异常的工作线程不会影响到其他的工作线程。
runWorker中的异常会被抛到run方法中run方法会异常结束run方法结束线程就嘎了
如果是submit异常没抛出来那就不嘎~
3.8 工作线程继承AQS的目的是什么
工作线程的本质就是Worker对象
继承AQS跟shutdown和shutdownNow有关系。
如果是shutdown会中断空闲的工作线程基于Worker实现的AQS中的state的值来判断能否中断工作线程。
如果工作线程的state是0代表空闲可以中断如果是1代表正在干活。
如果是shutdownNow直接强制中断所有工作线程
3.9 核心参数怎么设置
如果面试问到你项目中的线程池参数设置的是多少你先给个准确的数字和配置。别上来就说怎么设置
线程池的目的是为了充分发挥CPU的资源。提升整个系统的性能。
系统内部不同业务的线程池参考的方式也不一样。
如果是CPU密集的任务一般也就是CPU内核数 1的核心线程数。这样足以充分发挥CPU性能。
如果是IO密集的任务因为IO的程度不一样的啊有的是1s有的是1ms有的是1分钟所以IO密集的任务在用线程池处理时一定要通过压测的方式观察CPU资源的占用情况来决定核心线程数。一般发挥CPU性能到70~80足矣。所以线程池的参数设置需要通过压测以及多次调整才能得出具体的。
比如一个业务要查询三个服务
并发编程面试题3
一、CountDownLatchSemaphore的高频问题
1.1 CountDownLatch是啥有啥用底层咋实现的可以融入到你的项目业务中。
CountDownLatch本质其实就是一个计数器。
在多线程并形处理业务时需要等待其他线程处理完再做后续的合并等操作再响应用户时可以使用CountDownLatch做计数等到其他线程出现完之后主线程就会被唤醒。
CountDownLatch本身就是基于AQS实现的。
new CountDownLatch时直接指定好具体的数值。这个数值会复制给state属性。
当子线程处理完任务后执行countDown方法内部就是直接给state - 1而已。
当state减为0之后执行await挂起的线程就会被唤醒。
CountDownLatch不能重复使用用完就凉凉。
1.2 Semaphore是啥有啥用底层咋实现的
信号量就是一个可以用于做限流功能的工具类。
比如Hystrix中涉及到了信号量隔离要求并发的线程数有限制就可以使用信号量实现。
比如要求当前服务最多10个线程同时干活将信号量设置为10。没一个任务提交都需要获取一个信号量就去干活干完了归还信号量。
信号量也是基于AQS实现的。
构建信号量时指定信号量资源数获取时指定获取几个信号量也是CAS保证原子性归还也是类似的。
1.3 main线程结束程序会停止嘛
如果main线程结束但是还有用户线程在执行不会结束
如果main线程结束剩下的都是守护线程结束
二、CopyOnWriteArrayList的高频问题
2.1 CopyOnWriteArrayList是如何保证线程安全的有什么缺点吗
CopyOnWriteArrayList写数据时是基于ReentrantLock保证原子性的。
其次写数据时会复制一个副本写入写入成功后才会写入到CopyOnWriteArrayList中的数组。
保证读数据时不要出现数据不一致的问题。
如果数据量比较大时每次写入数据都需要复制一个副本对空间的占用太大了。如果数据量比较大不推荐使用CopyOnWriteArrayList。
写操作要求保证原子性读操作保证并发并且数据量不大 ~
三、ConcurrentHashMapJDK1.8的高频问题
3.1 HashMap为啥线程不安全
问题1JDK1.7里有环扩容时。
问题2数据会覆盖数据可能丢失。
问题3其次计数器也是传统的在记录元素个数和HashMap写的次数时记录不准确。
问题4数据迁移扩容也可能会丢失数据。
3.2 ConcurrentHashMap如何保证线程安全的
回答1尾插其次扩容有CAS保证线程安全
回答2写入数组时基于CAS保证安全挂入链表或插入红黑树时基于synchronized保证安全。
回答3这里ConcurrentHashMap是采用LongAdder实现的技术底层还是CAS。AtomicLong
回答4ConcurrentHashMap扩容时一点基于CAS保证数据迁移不出现并发问题 其次ConcurrentHashMap还提供了并发扩容的操作。举个例子数组长度64扩容为128两个线程同时扩容的话
线程A领取64-48索引的数据迁移任务线程B领取47-32的索引数据迁移任务。关键是领取任务时是基于CAS保证线程安全的。
3.3 ConcurrentHashMap构建好数组就创建出来了吗如果不是如何保证初始化数组的线程安全
ConcurrentHashMap是懒加载的机制而且大多数的框架组件都是懒加载的~
基于CAS来保证初始化线程安全的这里不但涉及到了CAS去修改sizeCtl的变量去控制线程初始化数据的原子性同时还使用了DCL外层判断数组未初始化中间基于CAS修改sizeCtl内层再做数组未初始化判断。 3.4 为什么负载因子是0.75为什么链表长度到8转为红黑树
而且ConcurrentHashMap的负载因子不允许修改
负载因子是0.75从两个方面去解释。
为啥不是0.5为啥不是1
0.5如果负载因子是0.5数据添加一半就开始扩容了
优点hash碰撞少查询效率高。缺点扩容太频繁而且空间利用率低。
1如果负载因子是1数据添加到数组长度才开始扩容
优点扩容不频繁空间利用率可以的。缺点hash冲突会特别频繁数据挂到链表上影响查询效率甚至链表过长生成红黑树导致写入的效率也收到影响。
0.75就可以说是一个居中的选择两个方面都兼顾了。
再聊就是泊松分布在负载因子是0.75时根据泊松分布得出链表长度达到8的概率是非常低的源码中的标识是0.00000006生成红黑树的概率特别低。
虽然ConcurrentHashMap引入了红黑树但是红黑树对于写入的维护成本更高能不用就不用HashMap源码的注释也描述了要尽可能规避红黑树。
至于6退化为链表是因为满树是7个值7不退化是为了防止频繁的链表和红黑树转换这里6退化留下了一个中间值避免频繁的转换。
put操作太频繁的场景会造成扩容时期put的阻塞
一般情况下不会造成阻塞。
因为如果在put操作时发现当前索引位置并没有数据时正常把数据落到老数组上。
如果put操作时发现当前位置数据已经被迁移到了新数组这时无法正常插入去帮助扩容快速结束扩容操作并且重新选择索引位置查询
3.5 ConcurrentHashMap何时扩容扩容的流程是什么
ConcurrentHashMap中的元素个数达到了负载因子计算的阈值那么直接扩容当调用putAll方法查询大量数据时也可能会造成直接扩容的操作大量数据是如果插入的数据大于下次扩容的阈值直接扩容然后再插入数组长度小于64并且链表长度大于等于8时会触发扩容
扩容的流程sizeCtl是一个int类型变量用于控制初始化和扩容
每个扩容的线程都需要基于oldTable的长度计算一个扩容标识戳避免出现两个扩容线程的数组长度不一致。其次保证扩容标识戳的16位是1这样左移16位会得到一个负数第一个扩容的线程会对sizeCtl 2代表当前有1个线程来扩容除去第一个扩容的线程其他线程会对sizeCtl 1代表现在又来了一个线程帮助扩容第一个线程会初始化新数组。每个线程会领取迁移数据的任务将oldTable中的数据迁移到newTable。默认情况下每个线程每次领取长度为16的迁移数据任务当数据迁移完毕时每个线程再去领取任务时发现没任务可领了退出扩容对sizeCtl - 1。最后一个退出扩容的线程发现-1之后还剩1最后一个退出扩容的线程会从头到尾再检查一次有没有遗留的数据没有迁移走这种情况基本不发生检查完之后再-1这样sizeCtl扣除完扩容结束。
3.6 ConcurrentHashMap得计数器如何实现的
这里是基于LongAdder的机制实现但是并没有直接用LongAdder的引用而是根据LongAdder的原理写了一个相似度在80%以上的代码直接使用。
LongAdder使用CAS添加保证原子性其次基于分段锁保证并发性。
3.7 ConcurrentHashMap的读操作会阻塞嘛
无论查哪都不阻塞。
查询数组 第一块就是查看元素是否在数组在就直接返回。
查询链表第二块如果没特殊情况就在链表nextnext查询即可。
扩容时第三块如果当前索引位置是-1代表当前位置数据全部都迁移到了新数组直接去新数组查询不管有没有扩容完。
查询红黑树如果有一个线程正在写入红黑树此时读线程还能去红黑树查询吗因为红黑树为了保证平衡可能会旋转旋转会换指针可能会出现问题。所以在转换红黑树时不但有一个红黑树还会保留一个双向链表此时会查询双向链表不让读线程阻塞。至于如何判断是否有线程在写和等待写或者是读红黑树根据TreeBin的lockState来判断如果是1代表有线程正在写如果为2代表有写线程等待写如果是4n代表有多个线程在做读操作。