面试官:看你简历上写,最近正在写并发编程方面的博客,是吧?
安琪拉:闲来无事,看看闲书,写写段子,承蒙读者厚爱,有此打算。
面试官:少跟我这拽文,“闲来无事”?阿里不用996吗?
安琪拉:修福报,你知道吗?..... 技术人的日常,能算996吗?
面试官:算了算了,还是聊正题,你先跟我讲讲什么是并发?
安琪拉:并发就是存在两个或多个线程,这些线程同时操作相同的物理机中的资源。
面试官:那并发跟并行有什么区别呢?
安琪拉:举个生活中的例子就懂了:
- 你在打王者荣耀,这个时候女朋友找你视频,你一直打完王者荣耀才接,说明你不支持并发(也不支持并行);
- 你在打王者荣耀,这个时候女朋友给你发了微信,你退出王者荣耀,回完微信再回到王者,微信和王者间来回切换,说明你支持并发,但不支持并行;
- 你在打王者荣耀,这个时候女朋友给你打电话,你边打荣耀边接电话,说明你支持并行。
并行的关键点是物理的“同时”,我们在单核CPU的时候,既能写代码也能听歌,这个多线程实际是基于操作系统根据CPU时间片做的任务轮转,是伪“同时”,只能说是并发,不能算并行,但是多核CPU可以支持每个核同时运行任务,是真实的“同时”,是并行。
Erlang 之父 Joe Armstrong 画了一张图解释了并发与并行的区别,Concurrent (并发),Parallel (并行)。
并发允许二队小孩轮流使用咖啡机,并行是同时存在二台咖啡机,二队小孩同时使用,不冲突。
面试官:那高并发呢?你了解高并发吗?
安琪拉:【心里想,该来的还是来了,要造火箭了】
你说High Concurrency(高并发)是吧(先拽句英文)。
通常我们谈论并发的时候,更多的关注点在于线程安全,但是讨论高并发时,关注点不仅仅是线程安全问题,而是如何在短时间内处理大量请求,保证系统响应时间和吞吐量的可靠,更多关注的是稳定性问题(SRE),高并发涉及的是完整的系统知识,线程安全只是其中一小部分。
高并发是现在互联网设计系统中需要考虑的一个重要因素之一,通常来说,就是通过严谨的设计来保证系统能够同时并行处理很多的请求。这就是大家常说的「 高并发 」。也就是说系统能够在某一时间段内提供很多请求,但是不会影响系统的性能。如果想设计出高可用和高性能的系统,就应该从很多的方面来考虑,例如应该从硬件、软件、编程语言的选择、网络方面的考虑、系统的整体架构、数据结构、算法的优化、数据库的优化等等多方面。这其中的每一点展开来说都要说很多的知识,安琪拉会在后续课程更新这部分内容。
面试官:那你跟我讲讲你们系统的QPS有多少?
安琪拉:大促场景能有个10W+的QPS,日常业务高峰期也有2W+,其他时间几千。
其实对于大部分的系统,几十、几百很正常,QPS能过千的就已经不低了,有的业务会有峰值,QPS稳定过万的系统实际中不多,所以大家日常可以关注一下自己系统的QPS,这个问题面试经常会问。
面试官:一般我们有什么工具可以模拟并发请求呢?
安琪拉:PostMan、Apache Bench(AB)、Jmeter,推荐使用Jmeter。
面试官:那你能写段代码,演示一下并发安全的问题吗?
安琪拉:可以啊。笔递给我一下,顺便帮我拿下A4纸。
public class ConcurrencySafeTest {
private static int counter = 0;
public static void main(String[] args) {
//使用线程池
ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newCachedThreadPool();
//提交2000个任务
for(int i = 0; i < 2000; i++) {
threadPool.submit(new Add());
}
threadPool.shutdown();
System.out.println(counter);
}
static class Add implements Runnable {
@Override
public void run() {
counter++;
}
}
}
我们进行计数操作,执行2000次,预期的执行结果应该是2000,但是实际执行结果如下:
1971
1987
因为《并发》系列是从基础开始讲的,上面的代码部分内容涉及到后面的一些内容,比如线程池和线程的使用,这里只要大致了解并发的安全问题,后面会有详细说明,后面面试官的问题作为扩展阅读。
面试官:看到你代码中用了CachedThreadPool,那2000次任务执行,CachedThreadPool 线程池创建了多少个线程?
安琪拉:答案是不确定,CachedThreadPool 缓存了线程(复用线程),没有让任务排队,来一个任务,要么复用已有线程处理,要么新建一个线程处理。那我们怎么确定线程池创建过多少个线程呢?可以加一段代码打印出来。
如下:
private static int counter = 0;
public static void main(String[] args) {
//使用线程池
ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newCachedThreadPool();
//提交2000个任务
for(int i = 0; i < 2000; i++) {
threadPool.submit(new Add());
}
threadPool.shutdown();
//打印最多使用线程数
System.out.println("largestPoolSize:" + threadPool.getLargestPoolSize());
System.out.println(counter);
}
输出结果如下:
第一次:
largestPoolSize:11
1937
第一次:
largestPoolSize:14
1956
第一次:
largestPoolSize:31
1970
可以看到每次都不一样,线程池之前有文章讲过,这个系列后面还会深入讲解。
关于 largestPoolSize
, 注释说明了,记录线程池中最大的线程数。
/**
* Tracks largest attained pool size. Accessed only under
* mainLock.
*/
private int largestPoolSize;
面试官:看你代码中写了调用线程池的shutdown,那shutdown 和 shutdownNow 方法什么区别?
安琪拉:shutdown 是将线程池的状态设置为SHUTWDOWN状态,正在执行的任务会继续执行下去,没有被执行的则中断。而shutdownNow则是将线程池的状态设置为STOP,正在执行的任务则被停止,没被执行任务的则返回。
源码对比:
//shutdown
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
//设置线程池状态为SHUTDOWN
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
//设置线程池状态为STOP
advanceRunState(STOP);
interruptWorkers();
//把队列剩余等待执行任务取出,返回
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
面试官:线程池有哪几种状态?
安琪拉:5种,注意这里说的是线程池的状态,不是线程的状态。下面是线程池的状态流转图:
本文是《并发》系列第一集,主要介绍了一些并发、并行、高并发的一些基础概念,以及并发安全问题的案例,下一集讲并发的风险与优势和CPU多级缓存,以及一些内存操作的指令,然后说Java内存模型。
完整大纲参考: