概述
进程:操作系统对进程进行调度,使其在不同的核上轮番运行。 进程是系统中运行的一个程序,程序一旦运行就是进程。进程是系统分配资源的实体,每个进程都有独立的地址空间。一个进程无法访问另一个进程的变量和数据机构如果想让一个进程访问另一个进程的资源,需要使用进程间通信(英语:Inter-Process Communication,简称IPC),比如管道,文件,套接字。
线程:线程是进程的进一步划分,是CPU调度和分派的最小单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,栈),但是它可与同属一个进程的其他的线程共享进程所拥有的
全部资源。
协程:协程是一种程序组件,是一种比线程更加轻量级的存在。正如一个进程可以有多个线程,一个线程可以有多个协程。协程是用户视角的一种抽象,操作系统并没有协程的概念。协程运行在线程之上,协程的主要思想是在用户态实现调度算法,用少量线程完成大量任务的调度。多进程、多线程已经提高了系统的并发能力,但是在当今互联网高并发场景下,为每个任务都创建一个线程是不现实的,因为会消耗大量的内存 (进程虚拟内存会占用 4GB [32 位操作系统],而线程也要大约 4MB)。
【内存空间共享问题】
多线程进程内部的所有内存都由所有线程”共享”。这通常是线程的定义,因为它们都在同一内存空间中运行。
当从第一个进程派生出另一个进程时,它们将共享相同的内存空间。如果某个进程尚未从另一个进程派生,则它们通常不共享任何内存(通信除外)。
【协程的优势】
内存占用要小,且创建开销要小:用户态的协程,可以设计的很小,可以达到 KB 级别。是线程的千分之一。线程栈空间通常是MB级别, 协程栈空间最小KB级别。
减少上下文切换的开销:让可执行的线程尽量少,这样切换次数必然会少;让线程尽可能的处于运行状态,而不是阻塞让出时间片,当一个协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上(分时复用)。即使有协程阻塞,该线程的其他协程也可以被 runtime 调度,转移到其他可运行的线程上。
守护线程是一种在后台运行的线程,它不会阻止程序的结束,即使所有的非守护线程都已经结束,它也会自动退出。与之相反,非守护线程在主线程结束之前必须完成它们的任务,否则程序将一直等待它们结束。
Java 线程调度
进程与线程
进程是指程序的一次动态执行过程,通常我们说计算机中正在执行的程序就是进程,每个程序都会对应着一个进程。一个进程包含了从代码加载到执行完成的一个完整过程,它是操作系统资源分配最小单元。
而线程则是比进程更小的执行单位,是CPU调度和分派的基本单位。每个进程至少有一个线程,反过来一个线程只能属于一个进程,线程可以对进程所有的资源进行调度和运算。线程既可以由操作系统内核来控制调度,也可以由用户程序进行控制调度。
根据下图可以看到,多个CPU会去执行这三个进程。其中每个进程都包含着至少一个线程,比如进程1包含了四个线程,进程2和进程3包含一个线程。此外,每个进程都有自己的资源,而进程内的所有线程都共享进程包含的资源。
线程调度器
在Java多线程环境中,为保证所有线程的执行能按照一定的规则执行,JVM需要实现一个线程调度器。这个调度器定义了线程调度的策略,同时对CPU运算的分配都进行了约定,通过特定的机制为多个线程分配CPU的使用权。
线程调度器里面包含了多种调度策略算法,由这些算法来决定CPU的任务执行。此外,每个线程还有自己的优先级,比如有高、中、低,调度算法会通过这些优先级来实现优先机制。
抢占式调度
常见的线程调度模式有两种:抢占式调度和协同式调度。
抢占式调度指的是每条线程的执行时间、线程的切换都由调度系统控制,调度系统拥有某种运行机制。调度系统可能为每条线程都分配相同的执行时间片,也可能为某些特定线程分配较长的执行时间,甚至在极端条件下还可能不给某些线程分配执行时间片,从而导致某些线程得不到执行。在抢占式调度机制下,一个线程的堵塞不会导致整个进程堵塞。
协同式调度
协同式调度指某一线程执行完后主动通知调度系统切换到另一线程上执行。这种调度模式就像接力赛一样,一个人跑完自己的路程后就把接力棒交接给下一个人,下个人继续往下跑。在这种模型下,线程的执行时间由线程本身控制,也就是说线程的切换点是可以预先知道的。所以这种模式不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写得有问题,则可能导致系统运行到一半就一直阻塞了,最终将可能导致整个系统崩溃。
Java的调度
在了解了两种线程调度模式后,现在我们来看Java使用的是哪种线程调度模式。实际上,Java的线程调度涉及到了JVM的实现,JVM规范中规定每个线程都有优先级,且优先级越高越优先执行。但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片。反之,优先级低的分到的执行时间少但不会分配不到执行时间。JVM的规范并没有严格地给调度策略定义,可能因为面对众多不同调度策略,JVM要封装所有细节提供一个统一的策略不太现实,于是给了一个不严格但足够统一的定义。
定义 | 描述 | 表示的常量 |
---|---|---|
public static final int MIN_PRIORITY | 最低优先级 | 1 |
public static final int NORM_PRIORITY | 中等优先级,是线程的默认状态 | 5 |
public static final int MAX_PRIORITY | 最高优先级 | 10 |
那么Java线程使用的是什么调度呢?其实它使用的是抢占式调度。在JVM中体现为让可运行池中优先级高的线程拥有CPU使用权,如果可运行池中线程优先级一样则随机选择线程。但我们要注意的是实际上一个绝对时间点只有一个线程在运行,这里是相对于一个CPU来说,如果你的机器是多核的还是可能多个线程同时运行的。只有线程进入非可运行状态或另一个具有更高优先级的线程进入到可运行线程池时,才会使之让出CPU的使用权,从而更高优先级的线程会抢占优先级低的线程的CPU执行。
Java 线程状态
状态转移图
-
要明白线程转移的详细过程,可以先通过一张图片,了解一个线程的生命周期中,该线程会处在何种状态:
注意:单向箭头表示不可逆
1.0 新建态到就绪态
- 概念:1. 新建态:一个线程被创建出来时候所处的状态 ;2. 就绪态:线程调用start()方法后,便处于可以被操作系统调度的状态,即就绪态。该状态可以由三处转化而来,新建态执行了start、线程阻塞结束、锁池等待队列中的线程获得了锁
- 该状态对应状态图中的第0步,比较简单,不再赘述
1.1 就绪态到运行态
- 概念:运行态:表示当前线程被操作系统调度,分配了时间片,执行线程中的run方法时的状态。运行态只可以由就绪态的线程转化而来,如果多个线程都处在就绪态,就等待操作系统分配
- 注:可以看到t1和t2两个线程都运行start()方法后,控制台会随机交叉打印两个线程的输出信息,这种随机,是操作系统随机分配时间片的调度决定的
1.2 运行态到就绪态
1.2.1 时间片用完
- 我们知道,操作系统为了公平,不可能从就绪态里面选择一个,一直执行完,而是随机切换到另外的线程去执行,每个线程分配的执行时间结束,操作系统去调用别的线程,当前刚执行结束的线程便由运行态重新回到就绪态,等待操作系统的再次分配。参考上一个代码例子,t1的线程执行体方法中循环打印100次,t2也是,但是会看到控制台是交叉打印的,说明了这一点
1.2.2 t1.yield() 、Thread.yield();
- 概念:在t1线程体中调用t1.yield(),和Thread.yield();本质上一样,Thread.yield()表示当前线程让渡。线程调用yield()方法,会让该线程重新回到就绪队列,但是yield()让当前线程回到就绪队列后,并不能保证操作系统再次调用不会选择该线程,所以yield()方法不能用来控制线程的执行顺序
- 注意:这个程序我故意把线程让步yield()方法写在线程体刚运行的时候,也就是说,每次操作系统分配给t1线程时间片时候,t1都会让步。但这次的让步不代表t1接下来的方法不会执行,也就是我让步之后,大家再一起抢,t1又抢到了时间片,那么t1本次时间片内便执行接下来的方法,等时间片结束,再次分配t1时间片,t1还会让,再接着抢,抢到和抢不到都有可能。
1.3 运行态到阻塞态
- 概念:阻塞态表示当前线程被由于某种原因,被挂起,也就是被阻塞,正在运行的线程被阻塞后,即使结束阻塞状态也回不去运行态,只能回到就绪态,等待os分配cpu资源去调度
1.3.1 Thread.sleep()
- 注意:让当前线程睡眠,该线程被阻塞,睡眠时间结束,该线程接着运行
1.3.2 t2.join()
- 当在t1中调用t2.join()。那么t1会阻塞,一直等待t2执行完毕,才结束阻塞回到就绪态
- 直接看代码:这里我把t1和t2抽出来当做全局静态变量
- 解释:这个程序的运行结果是,首先t1,t2挣抢时间片,按系统调度,首先控制台t1和t2都有打印自身的输出信息,当t1执行到i=50的时候,调用了t2.join()。此时控制台会全部打印t2的信息,一直等待t2的循环结束,执行体的run方法结束,再去打印t1剩下的没运行完的循环
- 所以join的流程可以抽象为下面这张图片
1.3.3 t1等待用户输入,等待键盘响应
这个很好理解,比如你就执行一个main函数的主线程,等待输入时,该线程是不会结束的,就是处于阻塞状态。
1.4 阻塞态到就绪态
- 1.3中所有阻塞态结束,比如sleep结束,join后t2执行结束,用户输入了信息回车等。t1会结束阻塞态,但是都是回到就绪态,无法再立即回到运行态
1.5 运行态到等待队列
这里牵扯到对象锁的概念
- 两个线程竞争锁,其中t1释放锁,也就是把所占有的对象锁让出。那么如果不主动唤醒,该线程一直处在等待队列中,得不到操作系统OS的调度
- 概念:等待队列,就是当前线程占有锁之后,主动把锁让出,使自身进入等待队列。此种wait加notify可以保证线程执行的先后顺序。notify()是通知一个等待队列的线程回到锁池队列。notifyAll()是通知所有处在等待队列的线程,都回到锁池队列。
- show me code:
1.6 运行态到锁池队列
- 参考1.5的程序,在i=5之前,t1占有该对象锁,t2即使start()也得不到运行,原因是该对象锁被t1占有,t2拿不到,所以就进入锁池队列
1.7 等待队列到锁池队列
- 参考1.5的程序,当t1wait之后,让出对象锁,t1进入了等待队列,t2拿到锁,运行完之后,调用notify()让等待队列中的t1进入锁池队列。
1.8 锁池队列到就绪态
- 参考1.5的程序,当t2结束后,通知t1进入锁池队列,t2由于运行结束,处在锁池队列中的t1可以拿到对象锁,进入就绪态,等待操作系统的调度,从而进入运行态
1.9 运行态到死亡态
死亡态不可逆,一旦线程进入死亡态,就再也回不到其他状态
- 死亡态只能由运行态进入,运行态中的线程。例如通过操作系统的不停调度,t1直到把整个run方法中的循环体执行完毕,该线程完成了它的使命,便进入死亡态
Java 线程的 cpu 使用率
cpu usage
获取进程使用率
获取线程使用率
统计原理
CPU time is allocated in discrete time slices (ticks). For a certain number of time slices, the CPU is busy, other times it is not (which is represented by the idle process). In the picture below the CPU is busy for 6 of the 10 CPU slices. 6/10 = .60 = 60% of busy time (and there would therefore be 40% idle time).
CPU 时间按离散时间片(滴答)分配。对于一定数量的时间片,CPU 很忙,其他时候则不忙(用空闲进程来表示)。在下图中,10 个 CPU 片中的 6 个 CPU 处于繁忙状态。 6/10 = .60 = 60% 的繁忙时间(因此会有 40% 的空闲时间)。
A percentage is defined as “a number or rate that is expressed as a certain number of parts of something divided into 100 parts”. So in this case, those parts are discrete slices of time and the something is busy time slices vs idle time slices — the rate of busy to idle time slices.
百分比被定义为“以某物的一定数量部分除以 100 份来表示的数字或比率”。因此,在这种情况下,这些部分是离散的时间片,而忙碌时间片与空闲时间片——忙碌时间片与空闲时间片的比率。
举个例子
The CPU time is the time that the process is using the CPU - converting it to a percentage is done by dividing by the amount of real time that’s passed.
CPU 时间是进程使用 CPU 的时间 - 通过除以所经过的实际时间量来将其转换为百分比。
So, if I have a process that uses 1 second of CPU time over a period of 2 seconds, it’s using 50% of a CPU.
因此,如果我有一个进程在 2 秒内使用 1 秒的 CPU 时间,则它使用了 50% 的 CPU。