面试官:看你简历上写,最近正在写并发编程方面的博客,是吧?

安琪拉:闲来无事,看看闲书,写写段子,承蒙读者厚爱,有此打算。

面试官:少跟我这拽文,“闲来无事”?阿里不用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种,注意这里说的是线程池的状态,不是线程的状态。下面是线程池的状态流转图:

《并发与高并发系列第一集-基础与概念》_复用_02

本文是《并发》系列第一集,主要介绍了一些并发、并行、高并发的一些基础概念,以及并发安全问题的案例,下一集讲并发的风险与优势和CPU多级缓存,以及一些内存操作的指令,然后说Java内存模型。

完整大纲参考:

《并发与高并发系列第一集-基础与概念》_高并发_03