多线程
一、进程与线程
进程:
-
资源调度的最小单元;
-
一个内存中的运行的应用程序,每个进程都有独立的内存空间;
线程:
-
CPU调度的最小单元;
-
线程是进程的一个执行路径,共享一个内存空间,线程之间可以自由切换;
-
线程是在进程的基础上的进一步划分,一个线程启动后,里面的若干执行路径可以划分为若干线程;
二、进程的生命周期
进程可以分为以下三种状态:
-
运行(running):进程正在处理器上运行;
-
就绪(ready):进程已经准备好运行,但操作系统选择不在此时刻运行;
-
阻塞(blocked)
实际系统中,为了管理的需要,还存在着另外两种状态:
- 创建:系统已为其分配了PCB,但进程所需资源尚未分配,进程还未进入主存,即创建工作尚未完成,进程还不能被调度运行。
- 僵死:进程到达自然结束点或者因意外被终结,进入僵死状态的进程不能再执行,但在操作系统中仍然保留着一个记录,其中保存状态码和一些计时统计数据,供其它进程收集。
三、线程调度
3.1 调度指标
- 周转时间 = 完成时间 - 到达时间
- 响应时间 = 首次运行时间 - 到达时间
3.2 Java线程调度的方式
3.2.1 抢占式调度
抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制。
系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。
3.2.2 协同式调度
协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行。
这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。
3.3 线程调度算法
3.3.1 批处理系统常用调度算法
-
先来先服务 (FCFS) / 先进先出 (FIFO):(非抢占)
-
最短作业优先 (Shortest Job Fisrt,SJF):(非抢占)
-
最短剩余时间优先 (Shortest Time-to-Completion First,STCF):(抢占)
- 向 SJF 添加抢占,每当新工作进入系统时,它就会确定剩余工作和新工作中,谁的剩余时间最少,然后调度该工作。
-
高响应比优先调度算法
- 在利用该算法时,每要进行调度之前,都须先做响应比的计算,这会增加系统开销。
3.3.2 分时系统调度算法
- 轮转调度
- RR 在一个时间片内运行一个工作,然后切换到运行队列中的下一个任务,而不是运行一个任务直到结束。它反复执行,直到所有任务完成。
- 因此,RR 有时被称为时间切片,时间片长度必须是时钟中断周期的倍数。
- 优先级调度
- 此算法常被用于批处理系统中,作为作业调度算法,也作为多种操作系统中的进程调度算法,还可用于实时系统中。当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程,这时,又可进一步把该算法分成如下两种。
- 非抢占式优先权算法:在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时,系统方可再将处理机重新分配给另一优先权最高的进程。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。
- 抢占式优先权调度算法:在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。因此,在采用这种调度算法时,是每当系统中出现一个新的就绪进程i 时,就将其优先权Pi与正在执行的进程j 的优先权Pj进行比较。如果Pi≤Pj,原进程Pj便继续执行;但如果是Pi>Pj,则立即停止Pj的执行,做进程切换,使i 进程投入执行。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。
多级反馈队列调度
不必事先知道各种进程所需的执行时间,而且还可以满足各种类型进程的需要。
-
应设置多个就绪队列,并为各个队列赋予不同的优先级;该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。
-
规则
彩票调度
3.3.3 实时系统调度算法
- 单比率调度
- 限期调度
- 最少裕度法
3.4 JVM线程调度实现(抢占式)
Java 使用的线程调使用抢占式调度,Java 中线程会按优先级分配 CPU 时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。
线程让出 cpu 的情况:
- 当前运行线程主动放弃 CPU,JVM 暂时放弃 CPU 操作(基于时间片轮转调度的 JVM 操作系统不会让线程永久放弃 CPU,或者说放弃本次时间片的执行权),例如调用 yield()方法。
- 当前运行线程因为某些原因进入阻塞状态,例如阻塞在 I/O 上。
- 当前运行线程结束,即运行完 run()方法里面的任务。
四、单核和多核、超线程
4.1 一个单核CPU如何执行多线程和原理
(1)单核cpu同一时间,cpu只能处理1个线程,只有1个线程在执行;
(2)多线程同时执行,是cpu快速的在多个线程之间的切换;
(3)cpu调度线程的时间足够快,就造成了多线程的“同时”执行;
(4)如果线程数非常多,cpu会在多个线程之间切换,也会消耗大量的cpu资源;
(5)如果线程数非常多,每个线程被调度的次数会降低,线程的执行效率也会降低。
单核CPU执行多线程原理:宏观并行,微观串行。
单核CPU只能做一个任务,而多核CPU可以做多个任务。任务做的越多,任务的进度就越快速,系统完成任务的效率就高了。
4.2 超线程
超线程技术就是通过采用特殊的硬件指令,可以把两个逻辑内核模拟成两个物理超线程芯片,在单处理器中实现线程级的并行计算,同时在相应的软硬件的支持下大幅度提高运行效能,从而实现在单处理器上模拟双处理器的效能。其实,从实质上说,超线程是一种可以将CPU内部暂时闲置处理资源充分“调动”起来的技术。
虽然采用超线程技术能够同时执行两个线程(如果每个进程同一时刻只能运行它的一个子线程的话,那就等同个能够同时执行两个进程),但它并不像两个真正的CPU那样,每个CPU都具有独立的资源。当两个线程都同时需要某一个资源时,其中一个要暂时停止,并让出资源,直到这些资源闲置后才能继续。因此超线程的性能并不等于两颗CPU的性能。
超线程与多核的区别主要取决于资源的独立性。当运行的这两个线程属于同一进程,那么对于超线程技术,就会遇到两个线程的资源发生冲突的情况;对于多核,就不会发生这种情况,因此两个线程在两个不同的CPU之中运行,纵使它们属于同一进程。
五、同步和异步
5.1 同步
- 排队执行,效率低但安全
5.2 异步
- 同时执行,效率高但数据不安全
5.3 实现异步的方法
创建线程可以通过调用API来实现,那么怎么知晓旧任务已经运行结束了呢?
状态,即设一共享变量(FLAG),旧任务结束时,变量置有效值,之后旧任务结束,新任务循环检测变量是否有效;
通知,即像下载软件一样,下载完,系统会通知你,即旧任务向新任务发通知或消息,发完之后,旧任务结束;
回调,就是把旧任务做完后要做的收尾工作交给旧任务本身,这样,旧任务做完收尾工作后便结束。
上述三种方法当中,“回调”,旧任务与新任务之间没有关系;“通知”,旧任务和新任务有直接联系;“状态”,旧任务和新任务有间接联系,通过状态变量。
回调时,新任务可以不管旧任务的事情;
通知时,新任务必须间断地等待是否有通知,如果有通知,就处理它。等待的时候,新任务一般处于阻塞状态;
状态时,新任务不必进行等待操作,它只需要适时检测(判断)这个状态变量是否有效(即是否为有效值)就可以了,这种方法也就是轮询。
六、并发和并行
6.1 并发的含义
指两个或多个事件在同一时间段执行。
当有多个进程在运行时,如果系统是单核的CPU,那它根本不可能真正地同时运行一个以上的进程。系统只能把CPU运行时间划分成若干个时间段(在每个时刻段的起始时刻使用调度算法分配任务),再将每个时间段分配给每个进程执行。在一个时间段内,某进程在运行时,其它进程处于挂起状态(就绪状态)。这种方式我们称之为并发(Concurrent)。
并发的精髓就是分配时间片,微观上是间断的,宏观上是连续的。
6.2 并行的含义
指两个或多个事件在同一时刻执行。
当系统有一个以上CPU(即多核)时,则线程的操作有可能不是并发的。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
七、线程的实现
7.1 继承Thread
Demo
package com.test0806;
public class Demo {
public static void main(String[] args) {
MyThread m = new MyThread();
m.start();
for(int i=0;i<10;i++){
System.out.println("哈哈"+i);
}
}
}
package com.test0806;
public class MyThread extends Thread{
/**
* run:线程要执行的任务
*/
@Override
public void run() {
// 这里的方法,相当于一条新的执行路径
// 通过调用start()方法启动任务
for(int i=0;i<10;i++){
System.out.println("哼哼"+i);
}
}
}
匿名内部类
package com.test0806;
public class Demo2 {
public static void main(String[] args) {
/**
* 匿名内部类
*/
new Thread(){
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("hehehe"+i);
}
}
}.start();
for(int i=0;i<10;i++){
System.out.println("呵呵"+i);
}
}
}
构造函数
构造器 | 描述 |
---|---|
Thread() | 分配新的 Thread 对象。 |
Thread(Runnable target) | 分配新的 Thread 对象,并指定任务。 |
Thread(Runnable target, String name) | 分配新的 Thread 对象,命名并指定任务。 |
Thread(String name) | 分配新的 Thread 对象,命名。 |
常用方法
long | getId() | 返回此Thread的标识符。 |
---|---|---|
String | getName() | 返回此线程的名称。 |
int | getPriority() | 返回此线程的优先级。 |
StackTraceElement[] | getStackTrace() | 返回表示此线程的堆栈转储的堆栈跟踪元素数组。 |
Thread.State | getState() | 返回此线程的状态。 |
void | setDaemon(boolean on) | 将此线程标记为 daemon线程或用户线程。 |
void | setName(String name) | 将此线程的名称更改为等于参数 name 。 |
void | setPriority(int newPriority) | 更改此线程的优先级。 |
static void | sleep(long millis) | 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性。 |
static void | sleep(long millis, int nanos) | 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数加上指定的纳秒数,具体取决于系统定时器和调度程序的精度和准确性。 |
void | start() | 导致此线程开始执行; Java虚拟机调用此线程的run 方法。 |
设置优先级
-
变量和类型 字段 描述 static int
MAX_PRIORITY
线程可以拥有的最大优先级。 static int
MIN_PRIORITY
线程可以拥有的最低优先级。 static int
NORM_PRIORITY
分配给线程的默认优先级。
设置和获取线程名称
package com.test0806;
public class Demo {
public static void main(String[] args) {
// 如何获取线程的名称
System.out.println(Thread.currentThread().getName());
new Thread(new MyRunnable()).start();
new Thread(new MyRunnable()).start();
Thread t = new Thread(new MyRunnable());
t.setName("弟弟");
t.start();
}
static class MyRunnable implements Runnable{
/**
* 线程执行的任务
*/
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
}
如何停止线程?
注意不能使用已过时的stop()方法,因为线程可能还有资源未释放;
通过设置状态,即设一共享变量(FLAG),旧任务结束时,变量置有效值,之后旧任务结束,新任务循环检测变量是否有效;
新任务不必进行等待操作,它只需要适时检测(判断)这个状态变量是否有效(即是否为有效值)就可以了,这种方法也就是轮询。如果有效,就停止线程。
t1.interrupt();
线程的中断
package com.test0806;
public class Demo {
public static void main(String[] args) {
// 线程的中断
// 一个线程是一个独立的执行路径,它是否结束,应该由它自己决定
Thread t1 = new Thread(new MyRunnable());
t1.start();
for(int i=1;i<=5;i++ ){
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 主线程打印完后,给线程t1添加中断标记
t1.interrupt();
}
static class MyRunnable implements Runnable{
/**
* 线程执行的任务
*/
@Override
public void run() {
for(int i=1;i<=10;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 线程捕获到中断标记的操作
return;// 自杀
}
}
}
}
}
守护线程
package com.test0806;
public class Demo {
public static void main(String[] args) {
// 线程的中断
// 一个线程是一个独立的执行路径,它是否结束,应该由它自己决定
// 线程可以分为用户线程和守护线程
// 用户线程:当一个线程不包含任何用户线程时,所有守护线程自动死亡
// 守护线程:守护用户线程的,当最后一个用户线程结束时,所有守护线程自动死亡
Thread t1 = new Thread(new MyRunnable());
// 在线程t1启动之前设置为守护线程
t1.setDaemon(true);
t1.start();
for(int i=1;i<=5;i++ ){
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class MyRunnable implements Runnable{
/**
* 线程执行的任务
*/
@Override
public void run() {
for(int i=1;i<=10;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 线程捕获到中断标记的操作
e.printStackTrace();
}
}
}
}
}
7.2 实现Runnable
package com.test0806;
public class Demo {
public static void main(String[] args) {
// 1. 创建一个任务对象
MyRunnable myRunnable = new MyRunnable();
// 2. 创建一个线程,并为其分配一个任务
Thread t = new Thread(myRunnable);
t.start();
for(int i=0;i<10;i++){
System.out.println("哈哈"+i);
}
}
}
package com.test0806;
public class MyRunnable implements Runnable{
/**
* 线程执行的任务
*/
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("hhhhhhhh"+i);
}
}
}
7.3 实现Runnable比继承Thread相比的优势
- 通过创建任务,然后给线程分配的方式来实现多线程,更适合多个线程同时执行相同任务的情况;
- 可以避免单继承带来的局限性;
- 任务和线程本身是分离的,提高了程序的健壮性;
- 线程池,接受Runnable类型的任务,而不接受Thread类型的线程。