进程是系统资源管理最小单位,线程是CPU调度的最小单位。一个进程中应至少有一个线程,否则该进程无法被CPU调度执行,一个进程中可以有多个线程。进程更偏向于内存管理,每个进程的内存都是相互隔离的,无法互相访问。线程作为CPU调度的最小单位,
有些程序是基于进程调度的,一个进程中由一个或多个线程。但进程粒度过高,CPU都是基于时间片转轮法切换进程的,每次切换进程都需要保存程序运行的上下文,如临时变量、程序运行位置PC等需要保存在主内存中,而后复制到用户态JMM中的虚拟机栈里,每次切换成本过高,性能不高,因此产生了线程,即粒度较低的进程,由于线程都位于进程中,一个进程可以有多条线程,线程之间共享同一进程的内存,相比进程间切换,线程的上下文切换成本较低。但大量线程同样会产生上下文切换的性能问题,因此应根据CPU核数尽量控制线程数量。
目前web开发大多是基于线程池模型,小部分基于一部非阻塞模型。基于线程池模型的服务器难以负载所谓的大并发,主要原因有以下几点:
1.大量并发请求导致的线程上下文切换成本过高
2.web开发是IO密集型任务,IO等待如DB连接、RPC调用不可避免导致CPU空闲,无法完全利用CPU性能
3.线程数过多,锁竞争激烈
基于以上的原因,服务器端的线程模型从bio进化到nio,解决了一部分的性能问题,bio的线程模型是1:1,nio则是n:m,基于selector(select、poll、epoll)减少了大量无用线程,因为虽然socket层被改成了异步,但业务代码仍然是同步调用的。并不能从根本上解决问题。这时那种单线程异步非阻塞的优势就体现出来了,如nodejs,对应到java技术栈是netty,不过传统的servlet规范在3.0里也提供了异步servlet,将IO等待的任务交给一条新线程处理,并不阻塞当前线程,虽然如此,在性能上肯定还是不如异步回调模型的,只是目前支持异步的第三方库还比较少,像mysql驱动只有同步的,而一些nosql对异步的支持还算比较好,如redis、mongodb、elasticSearch等等。
异步非阻塞的网络通信框架如vert.x相比线程池模型的同步阻塞式的服务器如bio的tomcat,其优点是更能吃满CPU,而不是把CPU事件浪费在无意义的IO等待上,同时线程安全,不需要考虑锁和并发问题,缺点也有,比如是单线程的,没法根据CPU核数做优化,对应的解决方法也很简单,部署多个实例即可。vert.x由于是纯异步框架,所以大量利用callback的写法,内置了消息总线eventbus,基于topic/direct队列方便传输数据,也解耦了不同模块之间的关联。但callback的写法对程序员来说并不是很友好,很容易导致大量的回调嵌套,即所谓的callback hell。
这时候协程的优势就体现出来了,协程可以理解为一种超轻量级的用户态线程,由于线程是抢占式的,且基于linux内核调度,因此每次线程上下文切换都需要用户态与内核交互,而协程则不一样,协程是非抢占式的,需要程序员手动控制休眠sleep和唤醒awake,相比线程间的上下文切换,协程对性能的影响极低,毕竟协程的调度不依赖于linux底层,而是在应用层上做。除此之外,协程对内存的占用极低,一条新的线程栈至少要吃掉1M内存,而协程可能只需要1K,这样就可以随心所欲的开协程而不必考虑内存占用的问题。虽然协程的调度是在应用层做,但本质上还是基于selector,比如server向client发送一个请求,client做出响应,需要selector获取到read的事件,然后程序员通知协程该做什么事了,这样可以消灭掉回调的写法,相当于用同步的代码风格写异步的程序,对程序员更加友好,同时性能也相当不错,这个应该就是go语言吹嘘的轻松写出高并发的代码的原因了。至于java,java的生态环境和技术栈已经太大太广了,历史包袱也太重了。不过不用担心,只是时间问题,况且已经有了,如Quasar。