多线程---认识线程
文章目录
- 什么是进程?
- 如何管理进程?
- 认识PCB
- 了解进程调度的过程
- 虚拟地址空间
- 什么是线程?
- 进程 VS 线程
- Thread类的属性和方法
- Thread类的属性
- Thread类的方法
- 构造方法
- 普通方法
- 线程的状态
什么是进程?
进程,也叫做“任务”,一个跑起来的程序就是进程。也就是说进程是运行起来的程序。在同一时刻,操作系统中的进程有很多,他们是如何管理的呢?
如何管理进程?
管理进程实际上就是做两件事儿:
- 描述进程:详细的表示一个进程有哪些属性、哪些信息?这是通过一个结构体来实现的,这个结构体里面包含了进程的各种信息,这个结构体叫做PCB(进程控制块);
- 组织进程:通过一个数据结构把若干个PCB联系起来,使其可以进行增查改删。这个数据结构一般使用双向链表。
注:
- 创建进程实际上就是创建一个PCB,然后把它放到双向链表中。
- 结束进程实际上就是在双向链表中找到这个节点,然后把这个节点删除。
- 查看进程列表实际上是遍历整个双向链表。
- 一个进程可能是一个PCB,也可能是多个。
认识PCB
认识PCB,就是了解PCB里面到底包含了哪些信息:
-
pid:是一个进程的身份标识
在同一台主机,同一时刻,这些进程的pid是唯一的。通过pid来区分进程。
-
内存指针:描述进程持有的内存资源
当我们双击一个可执行文件时,操作系统就要把这个文件的核心数据加载到内存中,同时会在内存中创建进程PCB。这就会给进程分配一定的内存空间,这个内存空间会被分为不同的区域,内存指针就是来描述每个区域是干嘛的。
-
文件描述符表:描述进程持有的文件资源
每个进程都可以打开一些文件(存储在硬盘上的数据),文件描述符表里就记录了当前这个进程打开了哪些文件。
-
进程状态:描述进程当前能否被调用
就绪状态:进程可以被调度到CPU上执行。
阻塞状态:进程不能被调度到CPU上执行。 -
进程优先级:描述进程调用的先后顺序
在创建进程时,可以通过一些系统调用来干预优先级。
-
进程上下文:保存当前进程执行过程中产生的中间结果。
一个进程在CPU执行一会儿之后,会切换到另一个进程执行,在过一段时间之后可能会再次切换回来继续执行。那么此时就需要知道上次执行到哪儿了。进程上下文就是用来保存中间结果的。
-
进程记账信息:统计一个进程在CPU上执行了多久
进程在执行时由进程优先级控制执行哪个进程,但是这样就有可能导致某个进程一直执行不到。通过统计进程记账信息,能让进程调度更均衡,避免执行不到某个进程。
了解进程调度的过程
其中,进程状态、进程优先级、进程上下文和进程记账信息都是和进程调度相关的信息,那么什么是进程调度呢?
我们先要明白: 进程是操作系统进行资源分配的基本单位。
进程调度其实是由“并行” + “并发”的方式执行的。
并行,即:在每个CPU核心上都可以独立的运行一个进程,多个CPU核心就可以同时运行多个进程。
并发,即:在一个CPU核心上,先运行一下进程1,再运行一下进程2,再运行一下进程3,再运行一下进程1…这样循环执行。只要切换的速度足够快,宏观上看起来三个进程就是在同时运行。
进程状态、进程优先级、进程上下文和进程记账信息存在的意义,就是支撑“进程调度”
虚拟地址空间
虚拟地址空间也是进程中非常关键的概念。
我们知道在创建进程时,都会给每个进程分配一定的内存空间,用来完成进程的工作。即:
在正常情况下,进程各自使用各自的内存,不会有任何问题。但是如果某个进程使用了野指针,不小心访问到了别的进程的内存且进行了修改。这就是个大问题:它这样做不仅仅影响到了自己的执行,而且还影响到了别人的执行。我们就通过虚拟地址空间来避免这个问题。
我们通过“虚拟地址空间”让每个进程都拥有自己的内存空间,并且和其他进程的内存空间隔离开。当进程要访问内存时通过MMU设备进行虚拟内存空间到真正内存空间的映射,访问真正的内存。如果发现有进程访问的内存越界,MMU设备就会进行拦截,关闭此进程,不让它影响到其他进程。
面对有些需要让多个进程配合的场景,又引入了进程间通信机制。它的原理就是:找到一块所有进程都能访问的公共资源,然后基于公共资源来交换数据。
什么是线程?
虽然多进程已经实现了并发编程,但是有一个巨大的问题:如果频繁的创建进程、销毁进程,那么这个操作就比较低效。
创建进程的过程:1. 创建PCB 2.给进程分配资源并赋值到PCB中 3. 把PCB插入链表
销毁进程的过程:1. 把PCB从链表上删除 2. 把PCB持有的资源释放 3. 销毁PCB
其中,分配资源和释放资源对操作系统来说要做的工作非常多,需要花费大量的时间。
因此,程序员就发明了“线程”。一个进程默认至少有一个线程,也可能有多个线程。这些线程都可以单独的在CPU上进行调度。最重要的是:同一个进程中的这些线程共用同一份系统资源(内存+文件),创建线程和销毁线程的开销远小于进程。所以,也把线程称为“轻量级进程”。
前面提到操作系统是通过PCB来描述进程的,更准确的说法是通过一组PCB来描述进程的。
每一个PCB对应一个线程,而一个进程可能包含多个线程。
使用多线程有一些优势:
- 能够充分利用多核CPU,提高效率。
- 只有创建第一个线程的时候需要申请资源,后续再创建新的线程都是共用同一份资源,节省了申请资源的开销;销毁线程的时候,也只有销毁到最后一个线程的时候才释放资源,节省了释放资源的开销。
使用多线程也有一些问题:
- 线程数目不是越多越好。当CPU核心已经饱和时,继续增加线程不会提高效率。反而会因为线程太多,线程的调度开销太大,影响了效率。
- 线程之间可能会相互影响到,造成线程安全问题
- 如果某个线程发生了意外就可能让整个进程奔溃
进程 VS 线程
- 进程包含线程
- 线程比进程更轻量,创建更快,销毁也更快
- 同一个进程的多个线程共用同一份系统资源(内存+文件),进程和进程之间则是有各自的系统资源(内存+文件)
- 进程是资源分配的基本单位,线程是调度执行的基本单位
Thread类的属性和方法
Thread类的属性
public class Test {
public static void main(String[] args) {
Thread thread = new Thread();
System.out.println(thread.getId());
System.out.println(thread.getName());
System.out.println(thread.getState());
System.out.println(thread.getPriority());
System.out.println(thread.isDaemon());
System.out.println(thread.isAlive());
System.out.println(thread.isInterrupted());
// 20
// Thread-0
// NEW
// 5
// false
// false
// false
}
}
- id是线程的唯一标识,不同的线程id不会重复。
- name在自己调试的时候会用到,可以自己在线程的构造方法里定义。
- state表示线程现在的状态(在下面介绍)
- priority在线程调度的时候会使用
- daemon:守护线程,也叫后台线程。前台进程:会阻止进程的退出,如果main线程执行完后,前台线程还没执行完,会等待前台线程执行完再退出进程;后台进程:不会组织进程的退出,当main线程执行完就退出进程。 我们创建的线程默认是前台线程。
- alive: 判断内核线程在不在。当new 出Thread对象但没有使用start方法启动时,不会把线程放入内核,使用start方法后才会把线程放入内核执行;当线程在内核执行完任务后,就会退出内核,清除内核线程,但是Thread对象还在。
- interrupt:线程中断,让线程提前结束,本质是让run方法尽快结束,不是让run方法执行到一半就退出。interrupt有两种情况:1. 如果线程正在执行 则设置标记位为true中断线程 2. 如果线程被阻塞 则唤醒sleep抛出异常 被catch捕获后在catch里处理,有两种中断的方式。
- 使用线程库里面自带的标记位
//通过使用标准库里自带的标记位
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// e.printStackTrace();
// [方式一] 立即结束线程
break;
// [方式二] 啥都不做, 不做理会. 线程继续执行
// [方式三] 线程稍后处理
// Thread.sleep(1000);
// break;
}
}
System.out.println("t 线程执行完了");
});
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();
System.out.println("设置让 t 线程结束!");
}
- 自定义一个标记位
//自己设置一个标记位 用来中断退出
public static boolean isQuit = false;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (!isQuit){
System.out.println("执行线程");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
while (true){
System.out.println("执行main");
isQuit = true;
System.out.println("手动中断线程");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Thread类的方法
构造方法
构造方法主要是用来创建线程的,现在共有七种创建线程的方式,点击查看
在构造方法里可以添加一个String类型的参数,用来命名线程。
普通方法
-
start():启动线程。把线程放到内核中执行。
-
interrupt():中断线程。让线程提前退出。
-
join():线程等待。在main中调用join(),就是等待该线程执行完了再执行main线程。
-
sleep():线程休眠。让线程阻塞一段时间。
PCB在管理线程时有俩个队列:一个就绪队列、一个阻塞队列。调用sleep()就是把线程放到阻塞队列里,等阻塞时间结束再放回就绪队列参与调度。
线程的状态
线程一共有六大状态,我们可以这样理解: