- Day3,第四篇
- 本文章的主题是
【Java实习生面试题系列】-- 多线程篇四
1. 说下对同步器 AQS 的理解?
Java
中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于 AbstractQueuedSynchronizer
(简称为 AQS
)实现的。 AQS
是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。
在 AQS
中的锁类型有两种:分别是 「Exclusive(独占锁)] 和 [Share(共享锁)」。
「独占锁」就是「每次都只有一个线程运行」,例如 ReentrantLock
。
「共享锁」就是「同时可以多个线程运行」,如 Semaphore、CountDownLatch、ReentrantReadWriteLock
。
2. AQS 的原理是什么?
AQS
核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,则调用 LockSupport().park()方法将Node中的线程状态改为WAITING,等待被唤醒或被中断
,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是 CLH
队列的变体实现的,将暂时获取不到锁的线程加入到队列中。
CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO), AQS
是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。
主要原理图如下:
AQS
使用一个Volatile
的int
类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS
完成对State
值的修改。- 在
FIFO
队列中,「头节点占有锁」,也就是头节点才是锁的持有者,尾指针指向队列的最后一个等待线程节点,除了头节点和尾节点,节点之间都有 「前驱指针」 和 「后继指针」 - 在
AQS
中维护了一个 「共享变量state」,标识当前的资源是否被线程持有,多线程竞争的时候,会去判断state
是否为0
,尝试的去把state
修改为1
3. AQS 底层使用了模板方法模式,你能说出几个需要重写的方法吗?
- 独占方式尝试获取资源的方法
tryAcquire
- 独占方式尝试释放资源的方法
tryRelease
- 共享方式尝试获取资源的方法
tryAcquireShared
- 共享方式尝试释放资源的方法
tryReleaseShared
4. 说下对 Semaphore、CountDownLatch 和 CyclicBarrier 的理解?
- Semaphore(信号量)-允许多个线程同时访问:
synchronized
和ReentrantLock
都是一次只允许一个线程访问某个资源,Semaphore
(信号量)可以指定多个线程同时访问某个资源。 - CountDownLatch (倒计时器):
CountDownLatch
是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。 - CyclicBarrier(循环栅栏):
CyclicBarrier
和CountDownLatch
非常类似,它也可以实现线程间的技术等待,但是它的功能比CountDownLatch
更加复杂和强大。主要应用场景和CountDownLatch
类似。CyclicBarrier
的字面意思是可循环使用(Cyclic
)的屏障(Barrier
)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier
默认的构造方法是CyclicBarrier (int parties)
,其参数表示屏障拦截的线程数量,每个线程调用await
方法告诉CyclicBarrier
我已经到达了屏障,然后当前线程被阻塞。
5. 说下对线程池的理解?为什么要使用线程池?
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
使用线程池的好处:
降低资源消耗:
通过重复利用已创建的线程降低线程创建和销毁造成的消耗。提高响应速度:
当任务到达时,任务可以不需要的等到线程创建就能立即执行。提高线程的可管理性:
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
6. 创建线程池的参数有哪些?
- corePoolSize : 核心线程大小。线程池一直运行,核心线程就不会停止。
- maximumPoolSize :线程池最大线程数量。非核心线程数量=maximumPoolSize-corePoolSize
- keepAliveTime :非核心线程的心跳时间。如果非核心线程在keepAliveTime内没有运行任务,非核心线程会消亡。
- workQueue :阻塞队列。ArrayBlockingQueue,LinkedBlockingQueue等,用来存放线程任务。
defaultHandler :饱和策略。ThreadPoolExecutor类中一共有4种饱和策略。通过实现
RejectedExecutionHandler
接口。- AbortPolicy : 线程任务丢弃报错。默认饱和策略。
- DiscardPolicy : 线程任务直接丢弃不报错。
- DiscardOldestPolicy : 将workQueue队首任务丢弃,将最新线程任务重新加入队列执行。
- CallerRunsPolicy :线程池之外的线程直接调用run方法执行。
- ThreadFactory :线程工厂。新建线程工厂。
7. 如何合理配置线程池参数?
自定义线程池就需要我们自己配置最大线程数 maximumPoolSize
,为了高效的并发运行,这时需要看我们的业务是 IO密集型还是CPU密集型
。
- CPU密集型
CPU密集的意思是该任务需要最大的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才能得到加速(通过多线程)。而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那么多。
- IO密集型
IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上这种加速主要就是利用了被浪费掉的阻塞时间。
IO 密集型时,大部分线程都阻塞,故需要多配制线程数。公式为:
CPU核数*2
CPU核数/(1-阻塞系数) 阻塞系数在0.8~0.9之间
查看CPU核数:
System.out.println(Runtime.getRuntime().availableProcessors());
当以上都不适用时,选用动态化线程池,看美团技术团队的实践
8. 执行 execute() 方法和 submit() 方法的区别是什么呢?
execute()
方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;- submit()方法用于提交需要返回值的任务。线程池会返回一个
future
类型的对象,通过这个future
对象可以判断任务是否执行成功,并且可以通过future的get()
方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)
方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
9. 说下对 Fork和Join 并行计算框架的理解?
Fork/Join
框架是 Java7 提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
Fork/Join
框架需要理解两个点,「分而治之」和「工作窃取算法」。
「分而治之」
以上 Fork/Join
框架的定义,就是分而治之思想的体现啦
「工作窃取算法」
把大任务拆分成小任务,放到不同队列执行,交由不同的线程分别执行时。有的线程优先把自己负责的任务执行完了,其他线程还在慢慢悠悠处理自己的任务,这时候为了充分提高效率,就需要工作盗窃算法啦~
今天的面试题就总结这么一些吧,总结面试题也花费了我不少时间,所以说总结不易,如果你感觉对你有帮助的话,请你三连支持,后面的文章会一点点更新。祝大家 offer 连连!!!