JavaSE 多线程概述、线程的创建启动与生命周期
程序员文章站
2022-07-04 19:18:40
...
Java自我学习路线
多线程概述、线程的创建启动与生命周期
一、进程
- 在多道程序环境下,程序的执行属于并发执行,此时它们将失去其封闭性,并具有间断性,以及其运行结果不可再现性的特征,为了能使程序并发执行并且可以对并发执行的程序加以描述和控制,引入了“进程”的概念
- 为了使参与并发执行的每个程序(含数据)都能独立地运行,在操作系统中必须为之配置一个专门的数据结构,称为进程控制块(Process Control Block,PCB),系统利用PCB来描述进程的基本情况和活动过程,进而控制和管理进程。所谓创建进程,实质上是创建进程实体中的PCB,撤消进程,实质上是撤消进程的PCB
- 进程(进程实体)由程序段、相关的数据段、PCB构成
- 在引入了进程实体的概念后,可以把传统OS中进程定义为:进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位
1. 进程的特征
- 进程和程序是两个截然不同的概念,除了进程具有程序所没有的PCB结构外,还具有以下特征:
1.1 动态性
- 进程的实质是进程实体的执行过程,因此动态性是进程的最基本的特征;另外进程有一定的生命周期,由创建而产生,由调度而执行,由撤消而消亡
1.2 并发性
- 多个进程实体同存于内存中,且能在一段时间内同运行
1.3 独立性
- 在传统的OS中,独立性是指进程实体是一个能独立运行、独立获得资源和独立接收调度的基本单位(凡未建立PCB的程序都不能作为一个独立的单位参与运行)
1.4 异步性
- 进程是按异步方式运行的,即按照各自独立的、不可预知的速度向前推进
二、管程
- 管程(Monitors)是进程同步工具,管程包含了面向对象的思想,它将表征共享资源的数据结构及其对数据结构操作的一组过程,包括同步机制,都集中并封装在一个对象内部,隐藏了实现细节
- 管程由四部分组成:管程的名称、局部于管程的共享数据说明、对该数据结构进行操作的一组过程(函数)、对局部于管程的共享数据设置初始化值的语句
1. 管程的特征
1.1 模块化
- 管程是一个基本程序单位,可以单独编译
1.2 抽象数据类型
- 管程中不仅有数据,而且有对数据的操作
1.3 信息掩蔽
- 管程中的数据结构只能被管程中的过程访问,这些过程也是在管程内部定义的,供管程外的进程调用,而管程中的数据结构以及过程(函数)的具体实现外部不可见
2. 管程和进程的区别
- 虽然两者都定义了数据结构,但进程定义的是私有数据结构PCB,管程定义的是公共数据结构,如消息队列等
- 两者都存在对各自数据结构上的操作,但进程是由顺序程序执行有关操作,而管程主要是进行同步操作和初始化操作
- 设置进程的目的在于实现系统的并发性,管程的设置则是解决共享资源的互斥使用问题
- 进程为主动工作方式,管程为被动工作方式,即进程通过调用管程中的过程对共享数据结构实行操作
- 进程之间可以并发执行,而管程则不能与其调用者并发
- 进程具有动态性,管程则是操作系统中的一个资源管理模块,供进程调用
三、线程
- 20世纪80年代中期,提出了比进程更小的基本单位——线程,一个进程至少拥有一个可执行的线程
- 如果说,在OS中引入进程的目的是为了使多个程序能并发执行,以提高资源利用率和系统吞吐量,那么,再引入线程,则是为了减少程序在并发执行时所付出的时空开销,使OS具有更好的并发性
- 由于线程具有许多传统进程所具有的特征,所以又被称为轻型进程或进程元,相应地,把传统进程称为重型进程
1. 线程的特征
1.1 调度的基本单位
- 在传统OS中,进程作为资源独立调度和分配的基本单位,但在每次调度时都需要进程上下文切换,开销大,所以在引入线程的OS中,已经把线程作为调度和分配的基本单位,当线程切换时,仅需保存和设置少量寄存器内容,切换代价远低于进程
- 在同一进程中,线程的切换不会引起进程的切换,但从一个进程中的线程切换到另一个进程中的线程时,必然会引起进程的切换
1.2 并发性
- 进程之间可以并发执行,一个进程中的多个线程之间可以并发执行,不同进程中的线程可以并发执行
1.3 拥有资源
- 进程可以拥有资源,并作为系统中拥有资源的一个基本单位
- 线程本身并不拥有资源,而是仅有一点必不可少的、能保证独立运行的资源,即每个线程中都应具有一个用于控制线程运行的线程控制块TCB、用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈
- 属于同一进程的所有线程都具有相同的地址空间
1.4 独立性
- 在同一进程中的不同线程之间的独立性要比不同进程之间的独立性低得多,因为每个进程为防止彼此干扰和破坏,都拥有一个独立的地址空间和其他资源,除了共享全局变量外,不允许其他进程访问;而同一进程中的不同线程往往是为了提高并发性以及进行相互之间的合作而创建的,它们共享进程的内存地址空间和资源
1.5 系统开销
- 线程的引入是为了减少系统的开销
1.6 支持多处理机系统
- 单线程进程只能运行在一个处理机上,而多线程进程,可以将一个进程中的多个线程分配到多个处理机上,使它们并行执行
四、Java->线程
- 在Java中,每个进程的内存独立不共享;同一个进程中的线程共享其进程中的内存和资源,共享的内存是堆内存和方法区内存,栈内存不共享,即线程和线程之间栈内存独立,堆内存和方法区内存共享,一个线程一个栈
- 多线程并发:每个栈和每个栈之间互不干扰,各自执行各自的,多线程目的就是为了提高程序的处理效率
- Java程序的执行原理
Java命令执行会启动 JVM,JVM 的启动表示启动了一个进程,该进程会自动启动一个“主线程”,然后主线程负责调用某个类的 main 方法,所以 main 方法的执行是在主线程中执行的(主栈栈底),然后通过 main 方法代码的执行可以启动其他的“分支线程”,所以main 方法结束程序不一定结束,只代表主线程(主栈)结束,而其他的分支线程(其他栈)有可能还在执行(压栈、弹栈)
1. 线程的创建与启动
- 创建:使用new语句创建
- 启动:调用线程对象的start()方法
run()与start():
run()方法不会启动线程,不会分配新的分支栈(相当于单线程,不能并发)
start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,只要新的栈空间开辟出来,start()方法结束,线程启动成功,启动成功的线程会自动调用run()方法(由JVM程序调度运作),并且run方法在分支栈的栈底部(压栈),另外main方法在主栈的栈底部,run和main是平级的
1.1 继承Thread类,重写run()方法
- 编写一个类,直接继承java.lang.Thread,重写run方法
public class ThreadTest {
public static void main(String[] args) {
// main方法里的代码属于主线程,在主栈中运行
// 新建一个分支线程对象
MyThread myThread = new MyThread();
// 启动线程
myThread.start(); // 这段代码不结束,下面的代码不能执行,因为方法体当中的代码永远都是自上而下的顺序依次逐行执行的,开辟完栈空间,这段代码结束,以下代码在主线程中执行,同时启动成功的线程会自动调用run()方法,则分支线程也开始执行,所以主线程与分支线程并发执行,互不影响
// 编写程序,这段程序运行在主线程中(主栈)
for (int i = 0; i < 10; i++) {
System.out.println("运行在主线程:" + i);
}
}
}
// 定义线程类
class MyThread extends Thread {
// 重写run()方法
@Override
public void run() {
// 编写程序,这段程序运行在分支线程中(分支栈)
for (int i = 0; i < 10; i++) {
System.out.println("运行在分支线程:" + i);
}
}
}
- 以上结果(即使用myThread.start())有先有后、有多有少是因为线程需要抢夺CPU时间片
- 启动线程时使用myThread.run()的结果(不会启动线程,不会分配新的分支栈,相当于单线程,不能并发)
1.2 实现Runnable接口,重写run()方法
- 编写一个类,实现java.lang.Runnable接口,实现run方法
- 实现接口比较常用,因为一个类实现了接口,它还可以去继承其它的类,扩展性强
- 实现Runnable接口的类是一个可运行的类,但不是一个线程类,所以需要先创建一个可运行对象,再把这个可运行对象封装成一个线程对象
public class ThreadTest {
public static void main(String[] args) {
// 创建一个可运行的对象
MyRunnable myRunnable = new MyRunnable();
// 将可运行的对象封装成一个线程对象
Thread thread = new Thread(myRunnable);
// Thread thread = new Thread(new MyRunnable());
// 启动线程
thread.start();
// 编写程序,这段程序运行在主线程中(主栈)
for(int i = 0; i < 10; i++){
System.out.println("运行在主线程:" + i);
}
}
}
//定义一个可运行的类(不是一个线程类)
class MyRunnable implements Runnable{
// 重写run()方法
@Override
public void run() {
// 编写程序,这段程序运行在分支线程中(分支栈)
for(int i = 0; i < 10; i++){
System.out.println("运行在分支线程:" + i);
}
}
}
- 当然也可以使用匿名内部类的方式
public class ThreadTest03 {
public static void main(String[] args) {
// 创建对象,采用匿名内部类的方式
Thread thread = new Thread(new Runnable() {
// 接口的实现
// 重写run()方法
@Override
public void run() {
// 编写程序,这段程序运行在分支线程中(分支栈)
for (int i = 0; i < 10; i++) {
System.out.println("运行在分支线程:" + i);
}
}
});
// 启动线程
thread.start();
// 编写程序,这段程序运行在主线程中(主栈)
for (int i = 0; i < 10; i++) {
System.out.println("运行在主线程:" + i);
}
}
}
2. 线程的生命周期
- 线程的生命周期存在五个状态:新建、就绪、运行、阻塞、死亡
- 新建:采用 new语句创建完成
- 就绪:执行 start() 后,又叫做可运行状态,表示当前线程具有抢夺CPU时间的权力(CPU时间片就是执行权)
- 运行:占用 CPU 时间,当一个线程抢夺到CPU时间片之后,就开始执行run()方法,run()方法的开始运行,标志线程进入运行态,当之前占有的CPU时间片用完之后,会重新回到就绪态继续抢夺CPU时间片,当再次抢到CPU时间片之后,会重新进入run()方法接着上一次的代码继续执行
- 阻塞:执行了 wait 语句、执行了 sleep 语句和等待某个对象锁,等待输入的场合(sleep、wait、IO),阻塞状态的线程会放弃之前占有的CPU时间片,所以需要再次回到就绪状态抢夺CPU时间片(无法直接转为运行态)
- 死亡:run()方法结束
引用图片: