JavaEE初阶——多线程(1)初识线程与创建线程

目录

一、进程

[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();
    }
相关推荐
勤奋菲菲2 小时前
Egg.js 完全指南:企业级 Node.js 应用框架
开发语言·javascript·node.js
长安城没有风3 小时前
从入门到精通【Redis】初识Redis哨兵机制(Sentinel)
java·数据库·redis·后端
蒂法就是我3 小时前
java集合类的底层类是哪个
java·开发语言
Hoking3 小时前
LangChain4j集成SpringBoot接入百炼大模型(Qwen)
java·人工智能·spring boot·llm
浪里小白龙593 小时前
零信任平台接入芋道框架
java
代码匠心4 小时前
从零开始学Flink:流批一体的执行模式
java·大数据·后端·flink·大数据处理
一只程序烽.4 小时前
java项目使用宝塔面板部署服务器nginx不能反向代理找到图片资源
java·服务器·nginx
小黄人软件4 小时前
用AI写的【实时文件搜索引擎】python源码【找资源】
开发语言·python·搜索引擎
Deryck_德瑞克4 小时前
IDEA编译时报错OOM的解决方案
java·ide·intellij-idea