一、简述进程
认识线程之前我们应该去学习一下“进程" 的概念,我们可以把一个运行起来的程序称之为进程,进程的调度,进程的管理是由我们的操作系统来管理的,创建一个进程,操作系统会为每一个进程创建一个 PCB,并为进程在内存中开辟一块运行空间 ,然后把这个 PCB 加入到链表中。
进程的调度是为了解决,处理多进程运行的机制,CPU 按照并发的方式执行进程,在进程之间高速切换,看起来就是多进程同时运行。为了描述控制进程的运行,系统中存放进程的管理和控制信息的数据结构称为进程控制块(PCB Process Control Block),它是进程实体的一部分,是操作系统中最重要的记录性数据结构。它是进程管理和控制的最重要的数据结构,每一个进程均有一个PCB,PCB 记载了进程的优先级,进程的状态,进程的上下文,进程的记账信息等等。
进程的概念就是为了能够实现,多任务,并发执行(”同时执行“)的机制。
能否实现多进程并发执行,需要看操作系统是否能够支持操作,执行的效率这个是考验CPU的性能。
二、什么是线程
进程是操作系统分配资源的基本单位,无论是创建,还是调度都是非常麻烦的,进程与进程之间的独立性是较高的,多进程协同维护同一个程序,对资源的消耗是非常大的。
举个例子:我们的QQ 当我们与别人打qq电话的时候,还可以接收qq 消息,并且qq空间同一时刻也接收了你铁哥们发的动态,做个假设,这三个功能是不是可以看作是三个进程,他们可以相互独立的运行,不受其他进程的影响,进程之间又有着某种关联,可以相互通信,他们共同维护了 qq 这个应用。
那么这个假设的例子:我们运行qq , 操作系统此时要给三个进程分配系统资源,开辟内存空间,而且还要花费精力保证进程之间的通信、关联性,是不是很复杂~此时的复杂主要体现在系统资源的分配上。
2.1 线程的概念
线程是建立在进程的基础之上的,可以看作是轻量级的进程,一个进程可以包含一个或者多个线程,同一个进程下的线程之间都是独立且可以调度执行的“执行流”,也是并发执行的,这些线程之间共用父进程的系统资源。
举个例子:
根据以上实例:
- 线程是建立在进程的基础上的,进程包含线程。
- 同一个进程内部,多个线程之间,共用一份系统提供给进程的资源(内存空间,资源共享)。
- 启动进程后(一般是会创建一个线程,例如 JAVA ,C/C++ 中的main()函数,程序从main()函数开始执行,操作系统为main() 函数开辟栈帧,然后CPU 的寄存器处理、维护栈帧),需要系统花时间,花精力去分配系统资源,进程创建完毕后,无论当中有多少个线程,站在进程的角度上都不需要再去申请系统资源了,线程之间共用一份进程资源。
- 进程是系统分配资源的基本单位。
- 操作系统真正调度的是线程,线程是操作系统调度运行的基本单位。
- 进程之间相互独立,同一个进程之下,线程之间共享进程资源,此时如果其中一个线程崩溃有可能会对其他线程造成影响,甚至是崩溃。
- 一个进程中的线程数应当设计合理,线程之间也是并发执行,而CPU 的核心处理器是有限的,如果同一个进程下的线程过多,虽然在系统资源的分配上只需要执行一次,但是 CPU 需要并发处理的数量过多的话,反而会使得线程的执行的效率变低(CPU高速来回切换的处理线程)。
上文博主假设的qq 例子,应该是一个进程,然后不同的功能交由多线程来执行,功能之间可以独立并发执行,共同维护我们的qq。
三、 Java创建线程
Java 学习过程中主要是偏向于多线程开发,那么接下来学习的是如何用Java 代码来创建一个线程。
3.1 继承Thread 类
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.
API : Application Programing linerface
给你一个软件,你能对他干什么,基于它提供的这些功能,就可以写一些代码,然后封装在一起,方便别人使用。
举个例子:
张三肚子饿了,想吃猪脚饭,自己做吧,首先得买猪脚,买调料,洗猪脚,切割,起锅烧油……想想都麻烦,于是张三打消了自己做饭的念头,于是前往楼下小吃店购买猪脚饭,张三到店之后,老板娘给张三一张纸,让他写一写自己想吃啥。
什么意思?我们想吃猪脚饭,不需要知道猪脚饭是怎么做的,我们只需要知道哪里有卖猪脚饭得地方即可
我们想创建一个线程,首先得找到 ”饭店“,这个饭店就是操作系统对创造线程操作封装的API ,然后JAVA 把 操作系统提供的 API 进一步的处理,封装成 Thread 类,我们不需要知道 系统是怎么创建一个线程的,只需要知道 Thread 类,可以吃到 "猪脚饭”。
代码实现:
class MyThread extends Thread { // MyThread 类继承 Thread 类,创建一个线程类 // 并重写 Thread 类的 run() 方法 // 此时MyThread 线程类的 run() 方法相当于主线程中的 main() 方法 @Override public voidrun() { while(true) { try { Thread.sleep(1000); //线程休眠,1000 = 1秒 System.out.println("t 线程 执行"); } catch(InterruptedException e) { e.printStackTrace(); } } } }
public class Demo1 { public staticvoid main(String[] args) { // 创建 MyThread 线程类实例化对象 Thread t =new MyThread(); // start 创建是新的线程并启动执行,调用run() 方法 // 创建线程默认就会执行线程的 run 方法 // 不调用 start() 方法线程不会启动 t.start(); while(true) { try { Thread.sleep(1000); //线程休眠,1000 = 1秒 System.out.println("Main主线程 执行"); } catch(InterruptedException e) { e.printStackTrace(); } } } }
线程不调用 start() 方法线程不会启动
调用start() 方法会从系统中创建一个线程,新的线程会执行 run 方法,run 方法式线程的入口方法,类型于主线程的 main() 方法。
启动线程之后,线程就会进入就绪状态,随时准备被系统调度,CPU 执行。
两个线程中分别设置了死循环,打印线程执行,可以出看出控制台两个线程都可以打印数据,也就是说两个线程之间宏观上是并发执行的,线程执行的顺序是无序的。
MyThread 线程类的 run() 方法相当于主线程中的 main() 方法,都是描述线程的入口。
疑问点:为什么调用 start() 方法会自动执行 run() 方法?
因为类Thread中的start方法中,调用了Thread中的run方法。
MyThread继承了Tread类,在MyThread 中重写run方法,就会覆盖掉Thread中的run方法,子类重写父类方法,优先调用子类重写后的方法,如果子类没有重写父类方法,默认执行的是父类的 run 方法,所以子类调用start方法后,实现的是自己的run方法体里面的代码。
3.2 实现 Runnable 接口
- 自定义一个类使其实现Runnable 接口
class MyThread2 implements Runnable { //实现 Runnable 接口 @Override public voidrun() { //重写接口里面的 run 方法 //线程运行的代码 while (true){ try { Thread.sleep(1000); } catch(InterruptedException e) { e.printStackTrace(); } System.out.println("线程t执行"); } } }
- 创建 Thread 类实例, 调用 Thread 的构造方法时将Runnable 对象作为 target 参数.
public class Demo2 { public staticvoid main(String[] args) { //创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数. Thread t =new Thread(new MyThread2()); t.start();// 启动线程 while(true) { try { Thread.sleep(1000); } catch(InterruptedException e) { e.printStackTrace(); } System.out.println("主线程 main 执行"); } } }
这是 Thread 线程类提供的有参构造方法,可以看出里面是 Runnable 接口的引用来接收,我们自定义的线程类由于实现了 Runnable 接口,此时发生向上转型,父接口引用接收子类对象,由于子类重写了Runnable 接口的 run 方法,所以接口引用可以直接调用子类重写后的 run 方法,如果在 父类想调用子类其他独有的成员变量或者是方法,就需要 向下转型(强制类型转换)。
然后线程对象 t 调用 start() 方法启动线程然后调用 run() 方法。
采用实现接口方式创建一个线程 与 继承 Threard 类 创建一个线程 最终的结果是没什么区别的。
3.3 lambda 表达式创建线程
Thread t = new Thread(() -> {
System.out.println("使用匿名类创建 Thread 子类对象");
});
t.start();
我们常常在通过创建 Thread 对象的时候,通过创建匿名内部类重写 run() 方法。
这里创建的匿名内部类
lambda 表达式的本质就匿名函数。
语法形式为 () -> {},其中 () 用来描述参数列表,{} 用来描述方法体,-> 为lambda运算符 ,读作(goes to)。
有时候我们不是必须要自己重写某个匿名内部类的方法,我们可以利用 lambda表达式的接口快速指向一个已经被实现的方法。
public class Demo3 { public staticvoid main(String[] args) throws InterruptedException { Thread t =new Thread( () -> { while(true) { try{ Thread.sleep(1000); }catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程 T执行"); } }); t.start();//启动 while(true) { Thread.sleep(1000); System.out.println("主线程Main执行"); } } }