【多线程初阶】第一次认识线程


前言

本文讲解内容为多线程的基础内容, 带大家了解一下什么是线程, 线程与进程的区别是什么, 以及创建第一个Java多线程程序.

在进行线程的学习前, 可以先简单了解一下操作系统中进程的概念, 有助于更好的理解线程.

关注收藏, 开始学习吧🧐


1. 线程概念

在正式讲解线程前, 我们先聊一下进程, 操作系统中引入进程, 目的是为了能够实现多个任务, 并发执行的效果. 但是进程是比较重量级的, 有一个问题, 如果频繁的创建或者销毁进程, 由于操作系统的不断进行资源分配, 其所消耗的成本是比较高的.

那么操作系统是如何进行优化的呢, 这就要说到我们今天的主角 ---- 线程. 其实进程当中是包含线程的, 一个进程里可以有一个线程, 或者多个线程. 而每个线程都是一个独立的执行流, 多个线程之间, 也是并发执行的.

那么线程是怎么减轻重量的呢? 在一个进程中的多个线程之间, 是共用一份系统资源的. 如内存空间, 文件描述符表等等, 只有在进程启动, 创建第一个线程的时候, 需要花成本去申请系统资源, 一旦进程 (第一个线程) 创建完毕, 再创建新的线程, 也不必再申请资源了, 这样创建和销毁的效率就提高了不少.

操作系统真正调度的是线程, 而不是进程. 请大家时刻牢记下面两个概念:

  • 线程 ---- 操作系统 调度运行 的基本单位
  • 进程 ---- 操作系统 资源分配 的基本单位

1.1 线程是什么

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

我们不妨设想一个场景去理解:

一家公司要去银行办理业务, 既要进行财务转账, 又要进行福利发放, 还得进行缴社保.

如果只有张三一个会计就会忙不过来, 耗费的时间特别长. 为了让业务更快的办理好, 张三又找来两位同事李四, 王五一起来帮助他,
三个人分别负责一个事情, 分别申请一个号码进行排队, 自此就有了三个执行流共同完成任务, 但本质上他们都是为了办理一家公司的业务.

此时, 我们就把这种情况称为多线程, 将一个大任务分解成不同小任务, 交给不同执行流就分别排队执行. 其中李四, 王五都是张三叫来的,
所以张三一般被称为主线程 (Main Thread).

再用我们本身的计算机去举一个简单例子:

在同一个程序中, 内部想要并发的完成多组任务, 此时就使用多线程比较合适, 因为可以更高效, 更节省资源.
比如我们计算机中有一个直播软件, 这是一个程序, 它需要同时录制画面, 录制声音, 进行网络传输等等. 这些程序中的功能, 其实就是一个个的线程, 也就是多线程.
而多个程序之间, 就是多个进程了, 比如该直播软件是一个进程, 而打开QQ后, QQ也是一个进程. 由于隔离性, 两个进程间是互不影响的.

1.2 为什么需要线程

首先, “并发编程” 成为 “刚需”.

  • 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源.
  • 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.

其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量.

  • 创建线程比创建进程更快.
  • 销毁线程比销毁进程更快.
  • 调度线程比调度进程更快.

最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池”(ThreadPool) 和 “协程”(Coroutine).

关于线程池我们后面再介绍. 关于协程的话题我们此处暂时不做过多讨论.

1.3 进程和线程的区别

下面我们举一个更贴近生活的案例来讲解:

有一个滑稽老铁在拧100个螺丝, 效率是比较低的.
在这里插入图片描述
如果我们采用多进程的方式来改进一下, 虽然也可以提高效率, 但这样就相当于多准备一间房间, 多准备一个桌子, 并且此时双方也看不到对方的进度.
在这里插入图片描述
而如果采用多线程的方式, 此时就相当于有两个滑稽老铁(两个线程), 房间和桌子也都只需要准备一个, 不需要额外准备空间, 拧螺丝的效率一样可以大大提升, 并且也都能看到对方的进度.
在这里插入图片描述
接下来, 我们进一步提高滑稽老铁的个数, 此时分给每个滑稽老铁的螺丝越来越少了, 拧螺丝的效率越来越高了.
在这里插入图片描述
但是多线程也有一些问题, 放在我们这个例子中, 如果当滑稽老铁数量越来越多后, 房间挤的满满的, 在挤来挤去的过程中, 把拧螺丝的桌子给弄翻了, 这样就会导致整个拧螺丝的工作都作废了.

所以多线程中当一个线程出现异常了, 此时很容易将整个进程都弄奔溃, 其他线程也随之崩溃了.

在进行了举例子的讲解后, 我们再给大家一些书面概念.

进程和线程的区别:

  • 进程是包含线程的. 每个进程至少有一个线程存在,即主线程.
  • 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
  • 进程之间具有独立性, 一个进程挂了, 不会影响到别的进程. 同一个进程里的多个线程之间, 一个线程挂了, 可能会把整个进程都带走, 并影响其他的线程.
  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位.
    在这里插入图片描述

1.4 Java线程和操作系统线程的关系

线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.

2. 第一个Java多线程程序

多线程程序和普通程序的区别:

  • 每个线程都是一个独立的执行流
  • 多个线程之间是 “并发” 执行的.

Java 标准库中提供了一个类 Thread 能够表示一个线程, 先给大家看一个多线程程序.

import java.util.Random;

public class ThreadDemo {

    private static class MyThread extends Thread {
        @Override
        public void run() {
            Random random = new Random();
            while (true) {
                // 打印线程名称
                System.out.println(Thread.currentThread().getName());
                try {
                    // 随机停止运行 0-9 秒
                    Thread.sleep(random.nextInt(10000));
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    public static void main(String[] args) {

        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.start();
        t2.start();
        t3.start();

        Random random = new Random();
        while (true) {
            // 打印线程名称
            System.out.println(Thread.currentThread().getName());
            try {
                // 随机停止运行 0-9 秒
                Thread.sleep(random.nextInt(10000));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }

}

使用 jconsole 命令观察线程

可以在自己 java 目录下的 bin 目录中找到, 博主给一个我自己的地址供大家参考. 使用这个jdk提供的工具, 就能够给我们查看出 Java 进程里面的线程详情.
C:\Program Files\Java\jdk1.8.0_192\bin

进入bin目录后, 找到jconsole.exe文件, 双击打开.
在这里插入图片描述
打开后, 我们选择自己刚才运行的线程并进行连接. 注意, jconsole 只能分析 Java 进程, 不能识别非 Java 写的进程. 并且要确保你自己的代码正在运行.
在这里插入图片描述
然后我们点击不安全的连接即可, 不用担心安全问题.
在这里插入图片描述
在这里插入图片描述
这样我们就看到了该程序中创建的三个线程, 以及一个 main 方法的主线程.
在这里插入图片描述

3. 创建线程的方法

3.1 继承 Thread 类

  1. 继承 Thread 类来创建一个线程类
class MyThead extends Thread {
    // 需要重写run()方法
    @Override
    public void run() {
        while (true) {
            System.out.println("hello t!");
            try {
            	// 休眠 1s
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
  1. 创建 MyThread 类的实例
// 向上转型
Thread t = new MyThread();
  1. 调用 start 方法启动线程
// 线程开始运行
t.start();

完整代码

class MyThead extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("hello t!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread t = new MyThead();
        t.start();
        while (true) {
            System.out.println("hello main");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

实现效果:
在这里插入图片描述
可以看到, main 方法的主线程, 与我们创建的 t 线程在同时执行.

3.2 实现 Runnable 接口

  1. 实现 Runnable 接口
class MyRunnable implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("hello t");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
  1. 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
Runnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
  1. 调用 start 方法
t.start();

完整代码

class MyRunnable implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("hello t");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class ThreadDemo2 {

    public static void main(String[] args) {

        MyRunnable runnable = new MyRunnable();
        Thread t = new Thread(runnable);

        t.start();

        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

}

对比上面两种方法:

  • 继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.
  • 实现 Runnable 接口, this 表示的是 MyRunnable 的引用. 需要使用 Thread.currentThread()

其他变形

  • 匿名内部类创建 Thread 子类对象
public class ThreadDemo3 {

    public static void main(String[] args) {

        Thread t = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello t");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        t.start();

        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

}
  • 匿名内部类创建 Runnable 子类对象
public class ThreadDemo4 {

    public static void main(String[] args) {

        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello t");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        t.start();

        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

}
  • lambda 表达式创建 Runnable 子类对象

lambda 表达式, 本质上是一个匿名函数.
lambda 表达式的基本写法

() -> {

}

()里面放参数, 如果只有一个参数, ()可以省略
{}里面放函数体, 可以写各种 Java 代码, 如果只有一行代码, 也可以省略{}

public class ThreadDemo5 {

    public static void main(String[] args) {

        Thread t = new Thread( () -> {
            while (true) {
                System.out.println("hello t");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t.start();

        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

}

总结

✨ 本节目标主要为,认识多线程并掌握多线程程序的编写, 重点需要掌握进程与线程的区别, 以及创建线程都有哪些方法. 本人还会继续更新多线程相关内容, 请持续关注.
✨ 想了解更多的多线程知识, 可以收藏一下本人的多线程学习专栏, 里面会持续更新本人的学习记录, 跟随我一起不断学习.
✨ 感谢你们的耐心阅读, 博主本人也是一名学生, 也还有需要很多学习的东西. 写这篇文章是以本人所学内容为基础, 日后也会不断更新自己的学习记录, 我们一起努力进步, 变得优秀, 小小菜鸟, 也能有大大梦想, 关注我, 一起学习.

再次感谢你们的阅读, 你们的鼓励是我创作的最大动力!!!!!

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

慧天城寻

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

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

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

打赏作者

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

抵扣说明:

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

余额充值