Java高并发的应用架构所涉及的各种框架、中间件的底层都离不开Java并发编程的核心知识的应用,而这是Java高级开发或架构师面试的一个重头戏,也是没做准备或答不好,容易被面试官吊打的一个环节。
为此,小卷在稀土掘金上开设了《Java并发核心编程理论与实践》这个专栏,和小伙伴们一起应付这块难啃却满是精华的硬骨头。
我们先来聊聊多线程的一些前置知识。
线程的历史
线程的产生,是随着cpu架构的发展、多核心处理器的普及,为了充分利用处理器资源,在不断"压榨"CPU性能的背景下产生的。所以说,多线程的历史是一部对CPU的压榨史。大体上经历了下面几个阶段:
-
纸带机时代
需要人工输入每条纸带来执行计算逻辑,处理器始终需要人工的传送和切换纸带来完成计算,切换的过程中处理器闲着只有等待。
-
多进程批处理
在一个纸带上写入多个计算任务,每个任务串行执行,如果其中一个任务阻塞了,其他任务也只有干等的份儿。
-
多进程并行处理
随着多任务操作系统的诞生,并行处理成为"压榨"CPU性能的通用手段。操作系统调度内存中的不同程序并交给CPU来处理,让CPU始终不会闲着。
-
多线程
对一个程序(进程)来说,内部不同的执行路径,不同任务的切换,就产生了多线程。
程序、进程和线程等概念
引出下面几个概念:
-
程序
扔在硬盘上的一个可执行文件,比如QQ.exe,是一个静态的概念。但我们后文所说的程序,泛指运行中的程序(进程),注意区别。
-
进程
指运行中的程序。双击可执行文件,比如QQ.exe,看到弹出一个可输入账号密码的UI界面,运行起来的程序就是一个进程,进程运行于内存中,相对程序来说是个动态的概念,一个程序可以在内存中开多个进程;进程是os资源分配(内存空间、文件描述符、进程号等等)的基本单位。
-
线程
进程里面最小的调度执行单元,通俗的说,就是一个程序中不同的执行路径,如果程序的执行从头到尾就一条路径就是单线程(主线程),否则,执行路径产生分支就是多线程;线程是属于一个进程内部的不同的执行路径,一个进程内的线程共享进程的资源;多线程的执行可认为是一个程序内部不同任务的来回切换;线程的切换(也叫上下文切换)由操作系统来调度,这个过程需要消耗一定的资源。
线程和cpu的关系,线程的调度由os负责调度,交给cpu,从内存(缓存)中获取cpu执行程序需要的指令和数据,加载到cpu寄存器中,由cpu计算单元执行计算;cpu只负责对获得时间片的线程执行计算,线程调度和切换由os负责,线程在发生切换时上一个线程执行的数据会放到缓存中,在分配到cpu时间片切换回来时再从缓存取了继续执行。
-
纤程/协程
绿色线程,由用户管理的(而不是OS管理的)线程。暂时不在我们要学习掌握的范畴,这里不做过多介绍。
多线程概念面试题
了解了多线程的背景知识后,我们来看几个面试题:
-
单核CPU设定多线程是否有意义?
有的线程趋向于计算(cpu密集型),有的线程趋向于io操作(io密集型),大多数混合这两种操作,总之,一个线程在等待外部输入或者进入休眠后,不会继续空占cpu,退出让其他线程有机会执行,这样单核cpu设定多线程也是有意义的。
-
工作线程数是不是设置的越大越好?
当然也不是设定的工作线程数越大越好,线程越多,上下文切换越频繁,开销越大,cpu消耗的时间都浪费在线程间的切换上了。
-
工作线程数(线程池中线程数量)设多少合适?
这取决于多个因素,期望cpu的利用率(确保cpu还有剩余可利用的)、cpu核心数、等待时间与计算时间的占比(如果一半时间在等待,就可以再加一个线程来执行),公式如下:
Nthreads = Ncpu x Ucpu x (1 + W/C)
W/C可以借助一些Profiler(Java平台本地环境用JProfiler,部署到测试环境用arthas)工具来测算一个方法调用过程中哪些时候在执行计算、调用其他方法或者等待资源,通过各项数据分析得到这个比率,也可以分析性能瓶颈。
对于线程池中核心参数的设置,也可以使用hippo4j对线程池进行监控,用这个工具的好处是,它可以和Spring Boot整合以对应用中使用的线程池进行监控的参数修改。
串行、并行和并发的概念
前面我们提到了程序执行路径,这里不得不提下这几个概念:
-
串行
程序的执行就一条路径,所有任务都是按照先后的次序来执行,讲究一个先来后到。前面的任务执行完,后面才执行。
-
并行
程序会由多个处理器同时处理,真正意义上的多条执行路径同时执行。
-
并发
是在单核cpu的前提下,通过os的调度让多个线程轮流获得cpu的时间片从而被执行,从宏观来说,程序由多条路径在"同时"执行,而微观来说,CPU在极短的时间内,反复切换执行不同的线程,实则为一条迂回的执行路径。
程序的执行我们再从调用者和被调用者的角度来阐述另一对经常被提及的概念:同步与异步、阻塞与非阻塞。
同步、异步与阻塞、非阻塞
举例说明,一个程序中有一个a方法,内部会调用b方法,对于a和b来说有这四种交互情况:
-
a等着b执行完后继续执行
同步阻塞,b执行完后不会主动通知a,a等b执行完再后续执行
-
a先执行不相干的操作,边执行边"瞅"一下,看b有没有执行完
同步非阻塞,b在执行完成之前,a可以先干别的,不用一直等着,但会时不时来看下b执行完没
-
a啥都不干就等着b主动通知
异步阻塞,这种模式几乎很少用
-
a继续执行别的等b主动通知
异步非阻塞,这种模式效率最高,比较符合多线程下高效执行任务的场景