【Java实习生面试题系列】-- 多线程篇四
Java
中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于 AbstractQueuedSynchronizer
(简称为 AQS
)实现的。 AQS
是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。
在 AQS
中的锁类型有两种:分别是 「Exclusive(独占锁)] 和 [Share(共享锁)」。
「独占锁」就是「每次都只有一个线程运行」,例如 ReentrantLock
。
「共享锁」就是「同时可以多个线程运行」,如 Semaphore、CountDownLatch、ReentrantReadWriteLock
。
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
tryAcquire
tryRelease
tryAcquireShared
tryReleaseShared
synchronized
和 ReentrantLock
都是一次只允许一个线程访问某个资源,Semaphore
(信号量)可以指定多个线程同时访问某个资源。CountDownLatch
是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。CyclicBarrier
和 CountDownLatch
非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch
更加复杂和强大。主要应用场景和 CountDownLatch
类似。 CyclicBarrier
的字面意思是可循环使用(Cyclic
)的屏障(Barrier
)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。 CyclicBarrier
默认的构造方法是 CyclicBarrier (int parties)
,其参数表示屏障拦截的线程数量,每个线程调用 await
方法告诉 CyclicBarrier
我已经到达了屏障,然后当前线程被阻塞。线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
使用线程池的好处:
降低资源消耗:
通过重复利用已创建的线程降低线程创建和销毁造成的消耗。提高响应速度:
当任务到达时,任务可以不需要的等到线程创建就能立即执行。提高线程的可管理性:
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。RejectedExecutionHandler
接口。
自定义线程池就需要我们自己配置最大线程数 maximumPoolSize
,为了高效的并发运行,这时需要看我们的业务是 IO密集型还是CPU密集型
。
CPU密集的意思是该任务需要最大的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才能得到加速(通过多线程)。而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那么多。
IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上这种加速主要就是利用了被浪费掉的阻塞时间。
IO 密集型时,大部分线程都阻塞,故需要多配制线程数。公式为:
CPU核数*2
CPU核数/(1-阻塞系数) 阻塞系数在0.8~0.9之间
查看CPU核数:
System.out.println(Runtime.getRuntime().availableProcessors());
当以上都不适用时,选用动态化线程池,看美团技术团队的实践
execute()
方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
submit()方法用于提交需要返回值的任务。线程池会返回一个future
类型的对象,通过这个future
对象可以判断任务是否执行成功,并且可以通过future的 get()
方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)
方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
Fork/Join
框架是 Java7 提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
Fork/Join
框架需要理解两个点,「分而治之」和「工作窃取算法」。
「分而治之」
以上 Fork/Join
框架的定义,就是分而治之思想的体现啦
「工作窃取算法」
把大任务拆分成小任务,放到不同队列执行,交由不同的线程分别执行时。有的线程优先把自己负责的任务执行完了,其他线程还在慢慢悠悠处理自己的任务,这时候为了充分提高效率,就需要工作盗窃算法啦~
今天的面试题就总结这么一些吧,总结面试题也花费了我不少时间,所以说总结不易,如果你感觉对你有帮助的话,请你三连支持,后面的文章会一点点更新。祝大家 offer 连连!!!