什么是进程?
现代的操作系统需要运行各种各样的程序。为了管理这些程序的运行,操作系统提出了进程(process)的抽象:每一个进程对应一个运行中的程序。
进程
进程的状态
为了对进程进行管理,操作系统定义了进程的状态。
- 新生状态:进程刚刚被创建处理,还未完成初始化。
- 就绪状态:进程可以被调度执行,但是还没有被调度器(管理进程的执行能够切换进程的状态)选择。
- 运行状态:进程正在cpu运行。
- 阻塞状态:该进程需要等待外部事件的完成(例如I/O请求)
- 终止状态:进程完成执行,且不会在被调度。
进程的内存空间
- 用户栈:保存了进程需要使用的各种临时的数据。栈是可以伸缩的数据结构,扩展方向自顶向下,栈底在高地址,栈顶在低地址。临时数据被压入栈时,栈顶向低地址扩展。
- 用户堆:管理进程动态分配的内存,扩展方向自底向上,堆顶在高地址,堆底在低地址。进程需要更多的内存,堆顶向高地址扩展。
- 代码库:进程执行时有时需要依赖共享代码库。
- 数据与代码段:数据段保存的是全局变量的值,代码段保存的是进程需要执行的代码。
- 内核部分:进程在用户态运行时,内核内存对其不可见,当进程进入内核态,才能访问内核内存。
进程控制块和上下文切换
在内核中,每个进程通过一个数据结构保存它的状态,包括进程标识符(PID),进程状态,虚拟内存状态,打开的文件等。这个数据结构称为进程控制块(PCB)。
进程的上下文包括进程运行时寄存器的状态,它能够保存和恢复一个进程在处理器上运行的状态。当操作系统需要切换当前执行的线程的时候,就会使用上下文切换机制。该机制会使前一个进程的寄存器状态保存到PCB中,然后将下一个进程先保存的状态写入寄存器中,切换线程。
进程之间的切换必须先切换到内核态,就是说系统调用相关的数据信息必须存储在内核空间中,然后执行系统调用。
如下图:当进程1由于中断或者系统调用进入内核态,先把上下文保存对应的PCB中,如果进行线程的切换,恢复对应线程的PCB上下文。
并发和并行:
并发是指一个处理器同时处理多个任务,而并行是指多个处理器或多核的处理器同时处理多个不同的任务
线程
在早期的操作系统中,进程是操作系统管理运行程序的最小单位。但是由于一些原因。
- 创建进程的开销比较大,需要完成创建独立的地址空间,载入数据和初始化等步骤。
- 进程之间的数据同步比较麻烦
因此,操作系统的设计者在进程内部添加了可独立执行的单元,它们之间共享内存地址空间,但是有各自保存上下文,这就是线程。之后,线程变成操作系统调度和管理程序的最小单位。
线程的优点:
- 一个进程中可存在多个线程
- 各个线程之间可以并发执行
- 各个线程之间可以共享内存地址空间和文件等资源
进程和线程的比较
- 进程是资源的分配单位,线程是CPU调度的单位。
- 线程同样有就绪,阻塞,执行三中基本状态,也具有它们之间的转换关系。
- 进程是一个完整的资源平台,线程只独享必不可少的资源
- 线程能够减少并发执行的时间和空间开销
- 线程创建比进程块,因为进程创建过程中需要管理资源信息,例如:内存管理,文件管理,但是线程不会涉及到管理这些信息,线程之间共享这些信息。
- 同一个进程之间的线程切换比进程之间快,线程之间具有相同的地址空间,但是进程之间没有相同的地址空间。
- 由于同一个进程的线程之间共享内存和文件资源,那么在线程之间传递数据,不需要内核,实现从之间的交互效率变高
- 线程的终止时间比进程快,因为线程释放的资源较少
线程创建的代码实现
1.继承Thread,重新run方法
package Thread;
// 线程的创建(1)
/**
* 继承Thread类并重写run方法
*/
// 线程2:通过创建 MyThread 类的一个实例并调用其 start 方法,创建了一个新的线程
class MyThread extends Thread {
// 标记为重写方法(Override),覆盖Thread类中的run方法
@Override
// 线程的入口方法,新的线程启动执行这里的代码,不需要手动调用,新的线程创建好后,自动的去执行
// 相当于回调函数,写好的函数自己不去调用,交给别人来调用
public void run() {
// for(int i = 0;i < 5;i++) {
// System.out.println("hello thread");
// }
while(true) {
System.out.println("hello thread");
try {
// 当前线程暂停执行1000ms
Thread.sleep(1000);
// 抛出 InterruptedException ,因为线程在休眠期间可能会被其他线程调用interrupt()方法打断
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class demo1 {
// 线程1:main,JVM启动程序时就创建了
public static void main(String[] args) throws InterruptedException {
// 创建MyThread类的实例,实例是一个线程对象
// Thread是java.lang 包的一部分,Java编译器自动导入java.lang这个包
// 向上转型,MyThread是Thread的一个子类,将子类对象的引用赋值给父类变量,可以用父类的引用变量处理不同的子类对象
Thread t = new MyThread();
// start创建,真正在系统中创建线程(JVM 调用操作系统的API完成线程的创建操作)
t.start();
// t.run()这个操作没有创建线程,只是调用了重写的run,进程中只有一个main线程,这样子还是一个单线程
// t.run();
while(true) {
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
1.sleep是静态方法,是让当前的线程暂时放弃cpu,过一段时间在执行
2.打印出的结果并不是按照“hello main”和“hello thread”顺序执行的,而是两者进行抢占式执行,即多个线程的调度顺序是随机的,线程执行的先后顺序是不确定的
2.实现Runnable,重写run方法
class MyRunnable implements Runnable{
@Override
public void run() {
while(true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class demo2 {
public static void main(String[] args) throws InterruptedException {
// MyRunnable类实现了Runnable接口,MyRunnable的实例可以被赋值给Runnable类型的引用变量runnable。向上转型
Runnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
while(true) {
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
高内聚:一个模块之间,有关联的东西放一起
低耦合:模块之间的依赖关系较少
实现Runnable接口并重写run方法与继承Thread类并重写run方法相比:
-
避免单继承的限制: Java不支持多继承.通过实现Runable接口,可以在继承其他类的同时实现多线程,从而避免了因为继承Thread类而失去继承其他类的能力。
-
更好的解耦: 实现Runnbale接口可以使得类与线程的实现解耦和。提高了代码的可维护性和可扩展性。
-
资源共享:实现一个接口,创建多个实例,将这些实例传给不同的对象,由于这些线程调用相同的方法,因此可以共享相同的资源
-
降低程序复杂度: 更清楚地表明类是为了执行任务而设计的,而不是表示线程本身。
3.其他的创建方式
方式1的匿名内部类以及方式2的匿名内部类和lambda表达式
4.Thread类的其他的属性和方法
方法 | 说明 |
Thread() | 必须重新Thread的run方法创建线程对象 |
Thread(Runnable target) | Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象并命名 |
Thread(Runnable target,String name) | 使用Runnable创建线程对象并命名 |
Thread(ThreadGroup group,Runnable target) | 线程可以被分组管理,分好的就是线程组 |
5.前台线程和后台线程
前台线程(非守护线程):程序的主要执行线程,通常执行重要的任务,不依赖其他线程来执行
特点:
- 通过继承Thread类或实现Runnable接口创建的线程默认是前台线程
- 前台线程不显式的结束,程序不会终止,即使前台线程完成了工作,如果有前台线程正在运行,程序也不会终止
后台线程(守护线程):为前台线程提供服务和支持
特点:
- 所有前台线程结束,即使后台线程仍在运行,jvm也会退出
- 可以通过setDamon(True)的方式将线程设置为后台线程
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
/* 将t设置为守护线程,当main线程结束后,守护线程必须结束
* 由于t是一个守护线程,当主线程的for循环结束后,
* 主线程会打印"main结束",然后主线程结束。因为此时JVM中只剩下守护线程 t,
* 所以JVM将会退出,守护线程 t也将被终止。
*/
t.setDaemon(true);
t.start();
/* 如果守护线程在主线程结束前有足够的时间运行,它可能会打印四次或更多次"hello thread"。
这完全取决于线程调度器的行为和系统的当前状态。*/
for (int i = 0; i < 3; i++) {
System.out.println("hello main");
Thread.sleep(1000);
}
System.out.println("main结束");
}
线程中的一些方法
1.isAlive()
Thread对象的生命周期:
Thread对象的生命周期是指Thread对象的整个生命周期,从创建到垃圾回收。Thread对象的生命周期可以独立于线程的生命周期存在,即使线程已经终止,Thread对象仍然可能存在,直到被垃圾回收器回收。
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
for (int i = 0; i < 3; i++) {
System.out.println("hello thread");
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
// 没有调用start,结果是false
System.out.println(t.isAlive());
t.start();
while(true) {
System.out.println(t.isAlive());
Thread.sleep(1000);
}
// false true true true false false... 线程结束但是Thread的生命周期还没有结束
}
2.start()
对于同一个线程对象,start()只能调用一次,如果多次调用,抛出IllegalThreadStateException异常,调用start()线程由新建变为可运行状态
3.线程终止interrupt()
此处是让线程直接停止,不会再恢复
class Test{
public int val = 0;
}
public class demo10 {
/*isFinished不可以定为局部变量,isFinished应该是final
涉及lambda表达式的变量捕获,lambda表达式是回调函数
执行时机是很久以后,有可能后续线程创建好了,main方法执行完成,对应的isFinished销毁
为了解决这个问题,Java是把捕获的变量拷贝到lambda表达式中,这就意味着这样的变量不适合修改
因此规定不允许修改*/
/*但是如果变成了成员变量 此时不再是变量捕获的语法,而是切换成内部类访问外部类成员的语法
成员变量的生命周期,也是让GC来管理的,在lambda表达式里面不担心变量生命周期失效的问题
也就不必拷贝,也就不必限制final之类的
*/
private static boolean isFinished = false;
public static void main(String[] args) throws InterruptedException {
// boolean isFinished = false;
Test test = new Test();
Thread t = new Thread(()->{
while(!isFinished) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("thread 结束");
System.out.println(test.val);
test.val++; // 修改对象本体可行
// Test test = new Test(); 修改对象引用不可行
});
t.start();
Thread.sleep(3000);
isFinished = true;
}
}
isInterrupted()是Thread的一个实例方法,为了判断当前线程释放被中断,返回一个布尔值,注意,在lambda表达式中不可以直接t.isInterrupted(),因为lamada定义在new Thread之前,也是在Thread t之前,不能够t.isInterrupted(),那么如何解决,利用Thread.currentThread() 是一个静态方法,哪一个线程调用,获取的就是哪个线程的引用,这里是t。
那么就是Thread.currentThread().isInterrupted()用来判断线程是否终止,调用interrupt()主动终止线程
4.等待线程join()
join能够要求多个线程之间的结束先后顺序,例如主线程调用t.join(),表名是主线程等待t线程的结束。当执行到t.join()时,主线程就会“阻塞等待”,一直等到t线程执行完毕,主线程才能执行。如果里面没有传参,那么就是“死等”,主线程的join就一直等下去,直到线程终止;join()里面传参,毫秒为单位,如果这段时间内线程终止,则立即返回,如果大于等待时间,t还没有结束,那么就不会继续等待
线程状态
-
NEW:线程已经创建但尚未启动。即线程对象已经被实例化,但是start()方法还没有被调用。
-
RUNNABLE:线程正在Java虚拟机中执行,但它可能正在等待操作系统的其他资源,如处理器。线程调度器可以随时选择RUNNABLE状态的线程来执行。
-
BLOCKED:线程正在等待监视器锁(monitor lock)来进入一个同步块/方法,或者在调用Object.wait()后重新进入同步块/方法。
-
WAITING:线程正在等待另一个线程执行特定操作。死等。
-
TIMED_WAITING:线程正在等待另一个线程执行特定操作,但有一定的超时时间。
-
TERMINATED:线程已经完成了执行,内核中的线程结束,但是Thread对象还在