java并发编程之进程和线程调度基础详解
作者:千月落
1.什么是进程
进程(process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。
——摘自百度百科
该怎么理解这句话呢,首先进程和线程都是操作系统层面的概念,不是某种编程语言提出的。进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、磁盘IO等。进程是程序在计算机上的一次执行活动。显然,程序是死的、静态的,进程是活的、动态的。
进程可以分为系统进程和用户进程,凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身,用户进程就是所有由你启动的进程。
当你运行一个程序,你就启动了一个进程。例如我们使用java语言在windows系统中启动一个main方法,就会系统就会创建一个名为java.exe的进程。
windows系统中使用直接使用任务管理器查看进程
windows系统中dos窗口键入 tasklist 命令查看进程
linux系统中可以使用ps命令查看进程
2.什么是线程
线程(thread)是操作系统能够进行运算调度的最小单位。
它被包含在进程之中,是进程中的实际运作单位。
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
3.进程和线程的区别与联系
联系:
- 进程是线程的容器,一个进程可以创建多个线程分支,两者之间存在包含关系。
- 两者都是多任务编程方式,都能使用计算机的多核资源。
- 线程具有进程的许多特征,故又称轻型进程,传统进程称重型进程。
- 一个程序至少有一个进程,一个进程至少有一个线程。
- 线程是CPU调度的最小单位,必须依赖于进程而存在
区别:
- 其实从概念上看的出,进程主要用于操作系统资源分配,线程主要在于CPU运算调度
- 进程和进程之间是相互独立的,每个进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响。而同一个进程下的多个线程共享资源。
- 线程的划分尺度小于进程,使得多线程程序的并发性高。
- 在执行过程中,每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
4.CPU内核数和线程数的关系
通常我们在选购电脑的时候,CPU是一个需要考虑到核心因素,因为它决定了电脑的性能等级。
CPU从早期的单核,发展到现在的双核,多核。在介绍CPU属性时经常会说“几核几线程”。
那么这个参数到底代表什么意思呢?
“几核”,就是指CPU核心数,“几线程”,就是线程数,也称逻辑CPU个数。
CPU的核心数 是指物理上,也就是硬件上存在着几个核心。比如,双核就是包括2个相对独立的CPU核心单元组,四核就包含4个相对独立的CPU核心单元组。
线程数 表示该CPU能同一时刻能够执行的最大线程数量 也称逻辑CPU数 是一种逻辑的概念,简单地说,就是模拟出的CPU核心数。一个核心最少对应一个线程,但引入超线程技术后一个核心可以对应两个线程,也就是说它可以同时运行两个线程。
对于win7操作系统来说
我们从任务管理器的性能标签页,以及设备管理器中处理器看到的就是线程数。
DOS窗口中输入“wmic”
然后在出现的新窗口中输入“cpu get * ”也可查看物理CPU数、CPU核心数、线程数。
- Name:物理CPU名称
- NumberOfCores:表示CPU核心数
- NumberOfLogicalProcessors:表示CPU线程数
所以我这台win7主机CPU配置的就是 2核心 4线程
而对于win10系统来说,任务管理器中已经标明了线程数。
在Linux系统中,可以根据下面命令查看
查询CPU个数:
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l
查询核心数:
cat /proc/cpuinfo| grep "cpu cores"| uniq
查询逻辑CPU总数(线程数):
cat /proc/cpuinfo| grep "processor"| wc -l
当然我们java也可以查看CPU线程数
public static void main(String[] args) { int i = Runtime.getRuntime().availableProcessors(); System.out.println("当前主机CPU线程数:"+i); }
5.CPU时间片轮转调度机制
不知道大家有没有疑问,一般高配置服务器的CPU 核心数也仅仅有32核,使用超线程技术同一时刻能执行的最大线程数也就是64,而服务器却能承载一秒10w多的访问量。而且我们平时在开发的时候,感觉并没有受 CPU 核心数的限制,想启动线程就启动线程,哪怕是在单核 CPU 上,为什么?
其实首先我们得明确两个概念,一个是并行 一个是并发
并行 是指两个或者多个事件在同一时刻发生。
并发 是指两个或多个事件在同一时间间隔发生,当谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少?离开了单位时间其实是没有意义的。
所以可以说,上面那个服务器 并行数是64,而每秒能承载的并发量是10w
那么CPU怎么做到每秒执行10w线程数的呢,这是由于操作系统提供了一种CPU时间片轮转调度机制。
操作系统一般是按照一定策略,定期给每个活动的进程执行其内部程序的机会,并且每次只执行一小段时间,然后操作系统利用中断强行退出执行,将当前程序信息压栈,然后开始执行下一个进程的一小段程序。通过这样不断快速的循环切换,每个程序都获得执行,在用户看来,感觉到很多程序都在平行的执行。
当然在自己的程序运行时不是独一无二的,我们看似很顺畅的工作,其实是由一个个的执行片段构成的,我们眼中相邻的两条语句甚至同一个语句中两个不同的运算符之间,都有可能插入其他线程或进程的动作。
时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称RR(Round-Robin)调度。
每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
百度百科对CPU时间片轮转机制原理解释如下:
如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。
如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。
调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。
时间片轮转算法的基本思想是,系统将所有的就绪进程按先来先服务算法的原则,排成一个队列,每次调度时,系统把处理机分配给队列首进程,并让其执行一个时间片。
当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序根据这个请求停止该进程的运行,将它送到就绪队列的末尾,再把处理机分给就绪队列中新的队列首进程,同时让它也执行一个时间片。
6.上下文切换
而这种时间片轮转是有代价的,往往还会伴随着上下文切换。
任何对进程或者线程的调度,都会引入额外的开销,这个开销中就包括上下文切换(Context Switch),有时也称做进程切换或任务切换,是指CPU 从一个进程或线程切换到另一个进程或线程。
在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。
例如我们经常用做缓存的redis,采用的就是单线程的方式,大大减少上下文切换产生的额外开销。
上下文切换通常是计算密集型的。也就是说上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
根据Tsuna的测试报告,每次上下文切换都需要几十纳秒到数微妙的CPU时间。这个时间还是相当可观的,特别是上下文切换次数较多的情况下,很容易导致CPU将大量的时间耗费在寄存器、内核栈、以及虚拟内存等资源的保存和恢复上,进而大大缩短了真正运行的时间。
引起线程上下文切换的原因大概有以下几种:
- 当前执行任务的时间片用完之后,系统CPU正常调度下一个任务。
- 当前执行任务碰到IO阻塞,调度器将此任务挂起,继续下一任务。
- 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务。
- 用户代码挂起当前任务,让出CPU时间。
7.并发编程的意义、好处
由于多核多线程的CPU的诞生,多线程、高并发的编程越来越受重视和关注。多线程可以给程序带来如下好处。
(1)充分利用CPU的资源
从上面的CPU的介绍,可以看的出来,现在市面上没有CPU的内核不使用多线程并发机制的,特别是服务器还不止一个CPU,如果还是使用单线程的技术做思路,明显就out了。因为程序的基本调度单元是线程,并且一个线程也只能在一个CPU的一个核的一个线程跑,如果你是个i3的CPU的话,最差也是双核心4线程的运算能力:如果是一个线程的程序的话,那是要浪费3/4的CPU性能:如果设计一个多线程的程序的话,那它就可以同时在多个CPU的多个核的多个线程上跑,可以充分地利用CPU,减少CPU的空闲时间,发挥它的运算能力,提高并发量。
就像我们平时坐地铁一样,很多人坐长线地铁的时候都在认真看书,而不是到家了再去看书,这样你的时间就相当于有了两倍。这就是为什么有些人时间很充裕,而有些人老是说没时间的一个原因,工作也是这样,有的时候可以并发地去做几件事情,充分利用我们的时间,CPU也是一样,也要充分利用。
(2)加快响应用户的时间
比如我们经常用的迅雷下载,都喜欢多开几个线程去下载,谁都不愿意用一个线程去下载,为什么呢?答案很简单,就是多个线程下载快啊。
(3)可以使你的代码模块化,异步化,简单化
例如我们实现电商系统,下订单和给用户发送短信、邮件就可以进行拆分,将给用户发送短信、邮件这两个步骤独立为单独的模块,并交给其他线程去执行。这样既增加了异步的操作,提升了系统性能,又使程序模块化,清晰化和简单化。
多线程应用开发的好处还有很多,大家在日后的代码编写过程中可以慢慢体会它的魅力。
8.并发编程需要注意事项
1.线程之间的安全性
从前面的章节中我们都知道,在同一个进程里面的多线程是资源共享的,也就是都可以访问同一个内存地址当中的一个变量。
例如:若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的:若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
2.线程之间的死锁
为了解决线程之间的安全性引入了Java的锁机制,而一不小心就会产生Java线程死锁的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。假设有两个线程,分别代表两个饥饿的人,他们必须共享刀叉并轮流吃饭。他们都需要获得两个锁:共享刀和共享叉的锁。
假如线程A获得了刀,而线程B获得了叉。线程A就会进入阻塞状态来等待获得叉,而线程B则阻塞来等待线程A所拥有的刀。这只是人为设计的例子,但尽管在运行时很难探测到,这类情况却时常发生
3.线程太多了会将服务器资源耗尽形成死机宕机
线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及CPU的“过渡切换”,造成系统的死机,那么我们该如何解决这类问题呢?
某些系统资源是有限的,如文件描述符。多线程程序可能耗尽资源,因为每个线程都可能希望有一个这样的资源。如果线程数相当大,或者某个资源的侯选线程数远远超过了可用的资源数则最好使用资源池。一个最好的示例是数据库连接池。只要线程需要使用一个数据库连接,它就从池中取出一个,使用以后再将它返回池中。资源池也称为资源库。
多线程应用开发的注意事项很多,希望大家在日后的工作中可以慢慢体会它的危险所在。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。