目录
在了解多线程之前先来看看进程是什么。
🥬什么是进程
进程就是计算机完成一个工作的过程。
进程是如何被管理的呢?
管理=描述(PCB)+组织
⭐描述:进程控制块,这是一个C语言的结构体,一个结构体对象代表一个进程。
PCB属性:
1、pid:一个进程的标识符,同一个机器同一时刻不可能有两个进程的pid相同。
2、内存指针:表示进程使用的内存空间的范围。
3、文件描述符表:表示这个进程都打开了哪些文件,
⭐组织:使用一定的数据结构来组织,比较常见的就是用双向链表。查看进程列表,本质上就是遍历操作系统内核中的这个链表,并显示其中的属性;创建一个进程,本质上就是创建一个PCB对象,加入到内核的链表中。销毁一个进程,本质上就是把这个PCB对象从内核链表中删除掉。
(要想让进程跑起来,就得给进程分配一定的系统硬件资源,例如:CPU、内存、磁盘、网络带宽……)
🥬进程调度
一台电脑可能有上百个进程正在运行,但是一台电脑上的CPU只有一个(我的是8核CPU--多核CPU 表示把多个CPU绑在一起,相当于CPU有了8个分身,能同时干活),这时就需要进程调度。
怎么个调度法:
1、并发式执行:CPU进行切换,先运行A进程,然后立马切换去运行B进程……由于CPU运行速度极快,我们坐在电脑前,是感知不到这个过程的。(宏观上是同时执行,微观上其实是顺序执行)
2、并行式执行:由于我们现在的CPU基本上都是多核的,就可以同时运行多个进程。(宏观和微观上都是同时执行)
进程的调度离不开进程的状态、进程优先级、进程的上下文、进程的记账信息。
🌵进程的状态
进程的三个基本状态:R(就绪)、S(休眠)、X(结束)。
R:进程已经准备好了,随时可以运行。
S:某些进程由于某些原因不能立马运行,需要等待。
X:进程正常结束或者由于某些原因中断退出运行。
🌵进程的优先级
优先级决定进程何时运行和接收多少 CPU 时间。
🌵进程的上下文
主要是存储调度出CPU之前,寄存器中的信息(把寄存器信息保存到内存中)等到这个进程下次恢复到CPU上次执行的时候,就把内存保存好的数据恢复到寄存器中。(需要记住上次运行到哪个指令了,方便下次调度的时候继续从这个位置开始)
🌵进程的记账信息
记录一个进程在CPU上运行的时间,用来辅助决定这个进程是继续在CPU上执行,还是要调度出去了。
总结:其实何时进行进程调度,这个事情对于进程本身来说,是感知不到的,执行到进程中的任意指令,都可能会产生这样的调度。此时,这样的调度其实给我们的代码引入了一定的随机性,对于我们代码的正确性来说,就产生了新的挑战。
🥬进程的虚拟地址空间
一个进程要想运行,就需要给它分配一些系统资源,其中内存就是一个最核心的资源。
我们刚开始想的地址分配:直接就依据真实的内存地址划分出空间,分配给每个进程
比如:0x100-0x200 分配给进程1
0x300-0x400 分配给进程2
0x500-0x600 分配给进程2
………………
但其实不是这样,这些地址都是操作系统抽象出来的虚拟地址,系统会自动的把这个虚拟地址转换成真实的物理地址。如下所示:
0x100-0x200 分配给进程1
0x100-0x200 分配给进程2
0x100-0x600 分配给进程3
那为什么要有虚拟空间地址呢?
为了一定程度的减少内存访问越界,带来的后果。这样做,也让进程与进程之间相互影响的可能性减少了,隔离性增加了,进程也就更稳定了。
假设现在进程1的内存范围0x100 - 0x200,如果我的代码尝试修改0x201的地址的数据,这个操作就是越界访问(错误的操作),如果这是一个真实的物理地址,这个修改就真的把0x201给改了,如果这个0x201正好是进程2要使用的内存,此时进程2可能就出错误了,就直接崩溃了。
如果进程访问的是虚拟地址,也尝试修改0x201,此时系统就要针对0x201来查询页表,找到对应的物理地址。由于0x201已经是非法地址,在页表中查不到,系统就明白了,你这是在越界访问,于是就直接让这个进程出现崩溃(系统会给进程发送一个SEGMENT_ FAULT信号,这个信号通常会导致进程崩溃),防止影响到其他的进程。
虽然虚拟空间地址让进程与进程之间的相互独立性提高了,整个系统也更加稳定了,但是这也导致进程与进程之前很难相互访问对方的内存,如果需要相互沟通交流就需要借助一定的特殊手段(借助某个双方都能访问的中间量来进行访问),例如,文件,管道(是内核中提供的一个队列),消息队列,信号量,信号, socket。
🥬线程
进程是可以"并发"编程,提高效率,但是创建进程需要分配资源,销毁进程需要释放资源,如果频繁创建、销毁进程,开销属实有点大。于是就有了线程,线程也称"轻量级进程"。
轻量体现在:
创建线程比创建进程更高效;
销毁线程比销毁进程更高效;
线程调度比进程调度更高效。
为什么更高效呢?
因为创建线程不用去申请资源,销毁线程也不需要去释放资源。让线程产生于进程内部,同一个进程可以产生一个或多个线程,这些线程可以和进程共用一样的资源。进程和线程是包含关系。这样子就让线程比进程更高效了。
这里共用一样的资源指的是:
1、内存,线程1与线程共用同一份内存(同一个变量,相当于说两个线程可以访问同一个变量)。
2、文件,线程1打开的文件,线程2也能使用。
上述就是对线程的描述,从操作系统内核角度而言,线程就是用PCB来描述的,不是说有几个PCB就有几个进程,如果几个PCB中的pid相同,说明他们是属于同一个进程。
虽然说线程的出现解决了资源开销大的问题,但也不是说同一个进程中线程越多越好,如果线程过多,可能会造成线程不安全。
线程过多可能出现的问题:
1、线程过多,线程之间频繁的调度,调度的开销就无法忽略。
2、线程过多,如果两个线程修改同一份内存的数据,就会产生冲突。
3、线程过多,如果某个线程抛出异常,没有catch住,可能会导致整个进程都异常退出。
🌵线程和进程的不同
进程和线程之间的区别和联系: (经典面试题)
1、进程是包含线程的,一个进程里可以有一个线程,也可以有多个线程。
2、每个进程都有独立的内存空间(虚拟地址空间),同一个进程的多个线程之间,共用这个虚拟地址空间。
3、进程是操作系统资源分配的基本单位,线程是操作系统调度执行的基本单位。
🥬创建线程
在Java中,使用Thread这个类的对象来表示一个操作系统中的线程。PCB是在操作系统内核中,描述线程的。Thread类是在Java代码中描述线程的。
使用java如何创建线程?
a)继承Thread重写run方法
//继承Thread重写run
class MyThread extends Thread {
@Override
public void run() {
System.out.println("hello Thread");
}
}
public class ThredDemo1 {
public static void main(String[] args) {
Thread t = new MyThread();
// start 方法,就会在操作系统中真的创建一个线程出来。(内核中创建一个PCB, 加入到双线链表中)
// 这个新的线程, 就会执行 run 中所描述的代码。
t.start();
//t.run();
}
}
start 方法会在操作系统中真的创建一个线程出来。(内核中创建一个PCB, 加入到双线链表中)这个新的线程, 就会执行 run 中所描述的代码。
其实我们直接引用run方法也能执行run中的代码
那为什么我们要使用start呢?接下来再来看一个代码:
class MyThread2 extends Thread{
@Override
public void run() {
while(true){
System.out.println("hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Thread t = new MyThread2();
//t.start();
t.run();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
b)实现Runnable重写run
//实现Runnable重写run
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("hello Thread");
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
Thread t=new Thread(new MyRunnable());
t.start();
}
}
c)继承Thread,重写run,使用匿名内部类
public class ThreadDemo4 {
public static void main(String[] args) {
// 创建了一个匿名的类, 这个类继承了 Thread,
// 此处new 的实例, 其实是 new 了这个新的子类的实例
Thread t=new Thread(){
@Override
public void run() {
System.out.println("hello Thread");
}
};
t.start();
}
}
d)实现Runnable重写run使用匿名内部类
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello Thread");
}
});
t.start();
}
}
e) lambda
public class ThreadDemo6 {
public static void main(String[] args) {
Thread t=new Thread(()->{
System.out.println("hello Thread");
});
t.start();
}
}
🥬线程的常用方法
1、线程ID
getId();
2、线程名字
getName()
3、线程当前状态