Java【多线程基础1】线程概念 + 进程线程的区别 + 创建线程

文章介绍了线程的基本概念,强调线程是进程中的执行流,比进程更轻量且能提高执行效率。多线程编程中,可以通过继承Thread类或实现Runnable接口创建线程,推荐使用Runnable接口结合lambda表达式。文章还讨论了进程与线程的区别,包括资源分配和调度执行的基本单位,并通过例子展示了如何感受多线程的并发执行。
摘要由CSDN通过智能技术生成


前言

📕各位读者好, 我是小陈, 这是我的个人主页
📗小陈还在持续努力学习编程, 努力通过博客输出所学知识
📘如果本篇对你有帮助, 烦请点赞关注支持一波, 感激不尽
📙 希望我的专栏能够帮助到你:
JavaSE基础: 基础语法, 类和对象, 封装继承多态, 接口, 综合小练习图书管理系统等
Java数据结构: 顺序表, 链表, 堆, 二叉树, 二叉搜索树, 哈希表等
JavaEE初阶: 多线程, 网络编程, TCP/IP协议, HTTP协议, Tomcat, Servlet, Linux, JVM等(正在持续更新)

上篇分享了进程 相关的知识, 认识了什么是进程, 什么是并行, 什么是并发
但 Java 并不鼓励多进程编程, 但是十分鼓励多线程编程, 线程也是Java中很重要的知识点, 从本篇开始介绍多线程相关的基础内容, 内容较多, 分为若干篇持续分享


提示:是正在努力进步的小菜鸟一只,如有大佬发现文章欠佳之处欢迎批评指点~ 废话不多说,直接上干货!

一、初识线程

1, 线程的概念

📌一个线程就是一个 “执行流”, 每个线程之间都可以按照顺序执行自己的代码, 多个线程之间 “同时” 执行着多份代码

上篇说到, 进程其实就是运行起来的程序 , 而所谓的 “执行流” 如何理解呢 ?

👉比如你打开QQ, 这是一个程序, 运行后就生成了一个进程, 你在QQ里发信息, 这是一个执行流, 生成了一个线程
你和好友打视频电话, 这也是一个执行流, 也就是一个线程
同理, 打语音电话, 逛QQ空间…都会生成一个线程

👉比如你打开微信, 生成一个进程, 在微信打视频电话, 会生成一个线程, 逛朋友圈会生成一个线程, 打视频电话会生成一个线程, 微信摇一摇也会生成一个线程…

🔎如果把进程比作工厂(程序), 那么线程就是工厂里的一条流水线(执行流)

🔎那不难理解什么是多线程了, 工厂里有多条流水线嘛
👉比如你在微信, 一边和女朋友打视频电话, 一边和备胎聊天(手动狗头保命) , 视频通话和发消息这两个线程同时运行, 就是多线程并发运行

上一篇介绍进程时提到, 并行和并发是由操作系统控制的, 程序员感知不到, 所以一般把并行和并发都 统称为并发


2, 为什么要有线程

Java中并不鼓励多进程编程, 而十分鼓励多线程编程, 为啥?

🔎🔎🔎
多进程编程 和 多线程编程 都能满足并发需求的场景, 而线程比进程更轻量, 并且某些场景下, 多线程编程可以提高代码的整体执行效率


如何理解线程更轻量?
👉首先, 线程是被包含于进程中的, 这不难理解, 毕竟线程是程序中的执行流

例如打微信电话和运行微信的关系, 打微信电话的前提一定是先运行微信

👉其次, 在上篇介绍进程时引出了一个重要概念: 进程是操作系统分配资源的基本单位

进程的创建, 销毁, 调度, 都是比较耗时的操作, 主要就体现在资源分配这种耗时操作上(因为系统需要通过遍历找到空闲资源, 每次申请都需要遍历)

👉于是, 操作系统就允许一个进程中可以同时存在一个或多个线程, 多个线程之间 相互独立, 并发运行

这就意味着, 多个线程共用一份进程申请出来的系统资源(内存资源和硬盘资源), 创建进程时, 系统资源分配完, 进程中再创建线程时, 就使用已经申请好的资源就行了, 这样线程就省去了资源分配这种耗时操作

但是创建线程本身还是需要一些时间消耗的

综上所述, 线程比进程更轻量 , 所以 :
1️⃣创建线程 比 创建进程更快.
2️⃣销毁线程 比 销毁进程更快.
3️⃣调度线程 比 调度进程更快.

进程和线程图示 :
在这里插入图片描述


🚦🚦🚦
所以现在应该明白:
上篇讲到了 进程的调度, 而进程中的线程往往是 “并发” 执行的, 其实 操作系统真正调度的是线程, 当进程中只有一个线程时, 调度这个线程时就是在调度(只包含一个线程的)进程
所以这就引出另一个重要概念: 线程是操作系统调度执行的基本单位

🚦🚦🚦
多个线程的执行可以 :
在多个 CPU 核心上同时运行(并行) 或者 在一个 CPU 核心上快速调度(并发)

线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池” (ThreadPool) 和 “协程” , 这个以后会发文介绍


3, 进程和线程的区别(重点)

1️⃣ 进程是包含线程的
2️⃣ 每个进程都有自己的独立的内存空间地址和文件描述符表, 而同一个进程中的线程(们)共用一份内存空间地址(内存资源) 和 文件描述符表(硬盘资源)
3️⃣ 进程是操作系统 分配资源的基本单位, 线程是操作系统 调度执行的基本单位
4️⃣ 多个进程之间互不影响, 具有独立性, 同一个进程中的线程(们)虽然也独立运行,但一旦有某个线程崩了, 整个进程都会崩, 会影响进程中的其他线程


4, 一些额外解释

🔎 问 : 计算机什么时候会创建一个进程或线程?
答 : 对敲代码写程序的程序员来说 : 取决于如何编写代码, 可以用代码创建进程或线程, 对使用软件的用户来说 : 打开(运行)程序就会创建一个进程, 使用程序中的功能就会创建一个线程

🔎 问 : 进程不会公用系统资源, 但线程会共用系统资源, 那线程之间会不会因为错误的内存操作产生bug?
答 : 取决于程序员如何编写代码

🔎 问 : 创建进程时申请到的资源有多少, 是程序员控制的吗?
答 : 有一部分是程序员控制的, 比如创建多少变量, 申请多少内存空间, 打开多少文件…还有一部分是操作系统运行进程时的额外资源

🔎 问 : 为什么一个线程崩了会影响进程中的其他线程?
答 : 由于线程之间是透明的, 共用一份资源, 如果一个线程崩了, 那么这个线程极有可能(即将)破坏了这个进程中的内存地址空间, 所以如果继续执行其他线程, 是由风险的, 说白了就是: 你好我好大家好, 我不好谁都别想好(一颗老鼠屎坏了一锅粥)

🔎 问 : 一个进程中可以存在一个或多个线程, 那线程的个数有没有上限?
答 : 理论上, 只要资源无限, 线程就可以有无限个, 可惜系统资源是有限的, CPU核心数也是有限的, 如果线程个数太多, 反而不会体现出 “并发编程” 的好处, 甚至效率还会下降

🔎 问 : 那如果进程 A 中的线程太多怎么办? 能不能再创建一个进程 B , 把进程 A 中的线程分给进程 B ?
答 : 进程 A 中线程太多导致运行效率下降, 只能说明系统资源不够了, 即便是再创建一个进程, 也是在同一个 CPU 上运行, 资源总是没有变化, 所以根本上解决问题的方法只有 : 再搞一个 CPU 这就涉及到分布式了, 我暂时还不太了解, 就不瞎BB了


二、初识多线程编程

在学习多线程之前, 我们写过的所有 Java 代码都是单线程的 , 当运行 idea 时, 就创建出了一个 idea 进程, 当运行 main 方法时, 就创建出了一个Java 子进程, 这个 Java 子进程里暂时只有一个 main 方法所在的线程, 称之为主线程

👉这个状态下, 我们的代码暂时是单线程执行的


1, 如何创建线程

👉如何编写多线程程序呢? 首先要学会如何创建其他的线程 : 使用 Thread 类

Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装, Thread 类是 JVM 用来管理线程的一个类,每个线程都有一个唯一的 Thread 对象与之关联

下面介绍使用 Thread 类创建线程的两种方法


1.1, 方法1: 继承 Thread 类

1️⃣ 首先自定义一个类 MyThread, 继承 Thread 类
2️⃣ 重写父类 Thread 类中的 run 方法, run 方法的方法体描述了线程执行什么操作
3️⃣ 实例化 MyThread
4️⃣ 调用 myThread 对象的 start 方法启动一个线程 , start负责启动一个线程

class MyThread extends Thread {
    @Override 
    public void run() {
        System.out.println("一个新的thread线程");
    }
}

public class Test1 {
    public static void main(String[] args) {
        // 向上转型
        Thread thread = new MyThread();
        // 启动线程
        thread.start();
        System.out.println("主线程");
    }
}

在这里插入图片描述

run 方法是一个特殊的 “入口方法”, 通过 start 方法启动 thread 线程后, 会自动调用 run 方法, 执行 run 方法里的代码, 注意, run 方法不是我们自己调用的, 是被自动调用的❗️❗️

调用 start 方法之后会创建出另一个 thread 线程
此时, java 子进程中就有了一个主线程, 和一个 thread 线程, 两个线程开始"并发"执行


🚗🚗🚗
也可以使用匿名内部类的方式 :

运行效果和上面一致, 只是写法不同

public class Test2 {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                System.out.println("一个新的thread线程");
            }
        };
        // 启动一个线程
        thread.start();

        System.out.println("主线程");
        }
    }
}

1.2, 方法2: 实现 Runnable 接口

1️⃣ Runnable 接口 中仅提供了一个 run 方法, 我们定义一个 MyRunnable 类, 实现 Runnable 接口
2️⃣ 重写接口中的 run 方法, run 方法的方法体描述了线程执行什么操作
3️⃣ 实例化 MyRunnable
4️⃣ 把 myRunnable 对象作为参数传给 Thread 类 的构造方法
5️⃣ 用 thread 对象调用 start 方法, start负责启动一个线程

使用 Runnable 接口这种方式和继承 Thread类 这两种方式没有本质区别, 都是重写 run 方法, 自己描述线程执行哪些操作, 然后通过 start 启动线程

class  MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("一个新的thread线程");
    }
}
public class Test3 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        // 启动一个线程
        thread.start();

        System.out.println("主线程");
    }
}

执行效果和上述代码都一致

🚗🚗🚗
同样可以使用匿名内部类的方式:

public class Test4 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("一个新的thread线程");
            }
        } );
        // 启动一个线程
        thread.start();
        
        System.out.println("主线程");
    }
}

执行效果和上述代码都一致


1.3, 推荐写法

上面两种方式哪种更好呢❓❓

Thread类 中还提供了很多方法可以被子类重写, 但我们创建线程, 需要重写的只有 run 方法, 所以当我们不需要重写其他方法时, 最好是实现 Runnable 接口来创建线程

同时, Runnable 接口是一个函数式接口, 这个接口中只提供了 run 方法, 所以实现 Runnable 接口来创建线程时, 更推荐的写法是 lambda 表达式

对 lambda表达式 不太熟悉的读者可以看看 介绍lambda表达式语法及使用方式 的这篇文章

使用 lambda表达式 的写法 :

    public static void main(String[] args) {
    	// lambda表达式
        Thread thread = new Thread( () -> {
        	System.out.println("一个新的thread线程");
        });
        // 启动一个线程
        thread.start();
        System.out.println("主线程");
    }

2, 感受多线程

由于主线程和 thread 线程中都只有一句打印操作, 所以我们不太能感受到多线程的效果, 接下来我们对上述代码做出如下修改 :
1️⃣ run 方法中循环打印 10 次, 每打印一次休眠1秒
2️⃣ main 方法中循环打印 10 次, 每打印一次休眠1秒

Thread 类中提供了一个 sleep 静态方法, 参数设置为1000, 表示休眠 1 秒再继续执行线程
可能会导致 InterruptedException 这个受查异常(编译时异常), 表示线程在休眠时提前中断(唤醒), 需要用 try-catch 捕获, 否则编译无法通过
sleep 方法会在下一篇文章 [介绍Thread类的常用方法] 中再做说明

    public static void main(String[] args) {
        Thread thread = new Thread( () -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("一个新的thread线程");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        // 启动 thread 线程
        thread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

**加粗样式**

📌可以看到, 程序会 “交替” 执行两条打印操作, 因为主线程和 thread 线程是并发执行的
📌并且没有固定顺序先打印谁, 再打印谁, 这也就说明了, 多线程在并发执行时, 操作系统对线程的调度是无序的, 随机的

尽管会有 “优先级” 这一说, 但只是理论上会优先调度, 并不是绝对


如果把上述代码中的 start 方法改成 run 方法, 我们程序员来调用 run 方法也是可以的, 但这样就不能创建出 thread 线程了, 这种状态下就是单线程

在单线程这个状态下, 就会先执行完 run 方法里的循环打印, 才执行第二个循环打印
在这里插入图片描述


总结

以上就是本篇的全部内容, 主要介绍了
1️⃣什么是线程: 是进程里的一个执行流
2️⃣进程和线程的区别
如何用代码创建线程

两个重要概念 :
1️⃣进程包含线程, 一个进程里有一个或多个线程
2️⃣线程是操作系统进行调度执行的基本单位

如果本篇对你有帮助,请点赞收藏支持一下,小手一抖就是对作者莫大的鼓励啦😋😋😋~


上山总比下山辛苦
下篇文章见

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灵魂相契的树

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值