目录
[1.1 什么是进程](#1.1 什么是进程)
[1.2 PCB(进程控制块)](#1.2 PCB(进程控制块))
[1.3 进程的组织方式](#1.3 进程的组织方式)
[2.1 认识线程](#2.1 认识线程)
[2.1.1 线程是什么](#2.1.1 线程是什么)
[2.1.2 为什么要有线程](#2.1.2 为什么要有线程)
[2.1.3 进程和线程的区别](#2.1.3 进程和线程的区别)
[2.1.4 Java线程和操作系统线程的关系](#2.1.4 Java线程和操作系统线程的关系)
[2.2 创建线程](#2.2 创建线程)
[2.2.1 第一个线程对象](#2.2.1 第一个线程对象)
[2.2.2 创建两个线程同时运行](#2.2.2 创建两个线程同时运行)
[2.2.3 使用jconsole观察线程的状态](#2.2.3 使用jconsole观察线程的状态)
[2.3 创建线程的方法](#2.3 创建线程的方法)
[2.3.1 继承Thread类](#2.3.1 继承Thread类)
[2.3.2 实现Runnable接口](#2.3.2 实现Runnable接口)
[2.3.3 其他变形](#2.3.3 其他变形)
[2.3.3.1 匿名内部类创建Thread子类对象](#2.3.3.1 匿名内部类创建Thread子类对象)
[2.3.3.2 匿名内部类创建Runnable子类对象](#2.3.3.2 匿名内部类创建Runnable子类对象)
[2.3.3.3 lambda表达式创建Runnable子类对象](#2.3.3.3 lambda表达式创建Runnable子类对象)
一、进程
1.1 什么是进程
在学习线程之前,我们先来了解一下进程。我们的计算机都会有操作系统,他对下(硬件)管理各种计算机设备,对上(软件)为各种软件提供一个稳定的运行环境。那么进程其实是操作系统对一个正在运行的程序的一种抽象,换言之,我们可以把**进程看作程序的一次运行过程,**也就是运行的程序在操作系统以进程的形式存在。
1.2 PCB(进程控制块)
我们知道在Java语言中,我们通过类来描述一个特征,这个类就是PCB:进程控制块,我们来了解一下这个类中有什么属性
java
class PCB{
PID //PID ------------ 进程的唯一标识
内存指针 //程序在运行之前操作系统会为它分配一片有效空间,内存指针指向这片空间
文件描述符表(集合)//程序运行起来后需要访问一些文件资源,操作系统负责位程序分配这些资源
进程的状态 //描述当前进程的运行状态:就绪,堵塞......
进程的优先级
进程的上下文 //保存上下文(读档),恢复上下文(存档)
进程的统计信息 //统计进程在CPU上的运行时间和次数
}
这段代码只是伪代码,不能真的运行,只是用来描述PCB里面的内容。这样一个PCB对象,就代表一个实实在在运行着的程序,也就是进程
根据了解PCB之后我们就知道每创建一个PCB后,系统就会集中分配很多空间和资源。
进程是系统分配资源的最小单位
1.3 进程的组织方式
我们通过一个双向链表来组织PCB
- 创建一个进程就是把PCB加入到链表中
- 销毁一个进程就是把PCB从链表中删除
- 查看所有进程就是遍历双向链表
二、线程
2.1 认识线程
2.1.1 线程是什么
一个线程就是一个执行流,每个线程都可以按照顺序执行自己的代码,多个线程之前是可以同时执行多份代码。
2.1.2 为什么要有线程
我们之前提到了进程是一个运行的程序,申请了很多空间和资源。如果有新的代码要运行,再创建一个进程不就行了,为什么要去引入线程呢?
多进程确实可以利用CPU资源去处理复杂业务,可以提高业务的处理效率。但是我们再去回想一个进程的创建需要什么,每创建一个进程,我们就需要分配资源分配空间,销毁进程又需要释放这些资源,而这些操作对系统的性能影响是比较大的。
有时候我们处理一件事情,申请一份资源就够了,就算有新的任务下达,我们也使用刚刚的资源就好了,不要再另外申请,所以我们引入了线程的概念。
线程共用进程启动时申请的资源,线程也可以叫做轻量级的进程。我们现在用图来更好的理解一下

我们现在假设张三要开设工厂
- 1.申请一片地皮
- 2.建厂
- 3.建生产线
- 4.招人,买材料
- 5.开工生产
我们把他开设工厂的步骤简单分为上面五步,很明显开设工厂是花费了很多钱申请了很多资源(地皮,建厂),现在厂里只有一条生产线。那现在生产线1已经不够处理订单的需求了,我们要新建生产线2,我们此时是重新建一个厂,再申请资源,还是在原来的厂里新建一个生产线?
很明显我们没必要再建一个厂,直接新建一个生产线2即可。两个生产线都是共用了这个厂的资源,新建生产线也比新建一个厂要效率高的多。
|-------|---------|
| 工业园 | 操作系统 |
| 张三的工厂 | 一个进程 |
| 生产线 | 一个线程 |
| 开工生产 | 开始线程的任务 |
相信我们现在可以理解,为什么要引入线程了,线程不需要申请资源浪费时间,可以提高效率

我们可以从任务管理器的CPU性能看到图片中的信息,图片中显示逻辑处理器为20,这个数字决定了创建线程的个数
我们现在假设一个场景,有一个任务是需要吃100只鸡,那如果只有一个人也就是一个线程去吃效率肯定是比较低的,那我们可以喊多几个人来吃也就是多创建几个线程来完成这个任务,这样效率肯定会提高。
那我喊十个人来吃鸡,和喊一百个人 来吃鸡,这两种情况也就对应着10个线程完成一个任务和100个线程来完成一个任务到底谁的效率会更高。
此时其实是10个线程效率会更高,因为我们这个CPU显示有20个逻辑处理器,当我们申请的线程个数大于这个数,会有很多线程出现堵塞的情况,反而会造成拥堵,浪费资源,降低效率。
- 当线程数小于逻辑处理器数时,效率会提升
- 当线程数大于逻辑处理器数是,由于过多的线程在阻塞等待状态,并没有真正发挥并行的效果, 反而因为创建线程消耗了系统资源
2.1.3 进程和线程的区别
进程是包含线程的,每个进程至少有一个线程存在,即主线程
进程和进程直接是不共享内存空间的,同一个进程的线程之间是共享同一个内存空间
还是用我们刚刚建厂的例子,张三和李四的工厂不是用的同一个空间和资源,也就是两个进程不共享内存空间,但是张三的生产线都是在张三的厂里,也就是一个进程的线程共享空间
进程是系统分配资源的最小单位,线程是系统调度的最小单位
一个进程崩溃一般不会影响到其他进程,一个线程崩溃往往可能影响同进程的其他线程一起崩溃
2.1.4 Java线程和操作系统线程的关系
线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并且对用户层提供了一些API工用户使用
Java标准库中的额Thread类可以是做是对操作系统提供的API进行了进一步的抽象和封装
2.2 创建线程
2.2.1 第一个线程对象
java
class Mythread1 extends Thread{
@Override
public void run() {
while(true){
System.out.println("hello my thread...");
}
}
}
Mythread1是我们自定义的一个线程类,这个类需要集成Java中的Thread类,在这个类中我们可以重写run方法,在run方法中我们可以自定义任务,只要在run方法中写我们的代码逻辑即可。
java
public class Demo1 {
public static void main(String[] args) {
Mythread1 mythread1=new Mythread1();
mythread1.start();
}
}
定义好我们的线程类后,我们可以实例化一个对象,调用start方法,即可开始运行线程

我们可以去查看start方法的源码,发现他其实是调用了本地方法(native)
这里我们重点区分两个概念
- 操作系统中的线程PCB
- Java中的线程,Thread类的一个对象,是对PCB的一个抽象
所以PCB和Thread类不是一个概念
线程启动的过程是
Java中创建一个线程对象------>start方法被调用------>启动线程------>JVM调用系统的API------>申请操作系统的PCB------>CPU调度执行
2.2.2 创建两个线程同时运行
java
class Mythread1 extends Thread{
@Override
public void run() {
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("hello my thread...");
}
}
}
java
public static void main(String[] args) {
Mythread1 mythread1=new Mythread1();
mythread1.start();//Mythread1线程
//主线程
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("hello main thread");
}
}
为了方便观察现象,我们调用了Thread类中的sleep方法,让线程休眠1秒再进行
我们可以看到,我们自定义的线程是一个死循环,我们main方法中的主线程也是一个死循环,如果按照我们以前的单线程想法,程序应该会在mythread1的run方法死循环打印,但是我们观察下面的运行结果,可以看到主线程和自定义线程是在同时进行的,互不打扰。

同时不要认为他是按照顺序进行,线程的执行顺序并没有什么规律,我们能看到图中红色区域,有可能先打印mythread1的内容,有可能先打印主线程的内容,这是**因为CPU是抢占式调度线程,这是随机不确定的,**所以我们能观察的不同的执行顺序。
2.2.3 使用jconsole观察线程的状态
jconsole在jdk-版本号(jdk-17)\bin目录下

我们选择Demo1进程

这里的Thread-0就是线程在操作系统里的名字,这是默认的线程名Thread-N,N从0开始

main就是主线程
2.3 创建线程的方法
2.3.1 继承Thread类
我们刚刚创建线程使用的是下面的代码
java
class Mythread1 extends Thread{
@Override
public void run() {
while(true){
System.out.println("hello my thread...");
}
}
}
这样创建线程的方法就是自定义一个线程类继承Thread类
2.3.2 实现Runnable接口
java
class MyRunnable implements Runnable{
@Override
public void run() {
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("实现Runnable接口...");
}
}
}
我们创建一个MyRunnable类实现Runnable接口
java
public static void main(String[] args) {
Thread t=new Thread(new MyRunnable());
t.start();
}
通过Thread类调用构造方法是将MyRunnable对象作为参数传入,再调用start方法,显示正常运行

通过实现Runnable接口这样的方法,可以单独定义线程的任务 ,我们是推荐这样的做法的 。因为这样可以让线程类和我们的业务解耦,让代码尽量分离,以便后面修改代码让互相的影响最小化 ,如果任务有变动,只需要修改一份代码即可。我们需要什么任务就传入对应的Runnable对象即可
2.3.3 其他变形
2.3.3.1 匿名内部类创建Thread子类对象
java
public static void main(String[] args) {
Thread t1=new Thread(){
@Override
public void run() {
System.out.println("匿名内部类实现Thread子类...");
}
};
t1.start();
}
2.3.3.2 匿名内部类创建Runnable子类对象
java
public static void main(String[] args) {
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类实现Runnable子类");
}
});
t1.start();
}
2.3.3.3 lambda表达式创建Runnable子类对象

我们来看一下Runnable接口的源代码,我们可以发现这个接口中只有一个方法。
那么像这种只有一个方法的接口,我们称为函数式接口 。对于函数式接口,我们可以使用lambda表达式来简化
java
public static void main(String[] args) {
Thread t1=new Thread(()->{
System.out.println("lambda表达式实现Runnable子类");
});
t1.start();
}