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

文章目录

  • 前言
  • 1. 线程概念
    • 1.1 线程是什么
    • 1.2 为什么需要线程
    • 1.3 进程和线程的区别
    • 1.4 Java线程和操作系统线程的关系
  • 2. 第一个Java多线程程序
  • 3. 创建线程的方法
    • 3.1 继承 Thread 类
    • 3.2 实现 Runnable 接口
  • 总结


前言

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

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

关注收藏, 开始学习吧


1. 线程概念

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

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

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

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

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

1.1 线程是什么

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

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

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

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

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

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

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

1.2 为什么需要线程

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

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

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

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

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

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

1.3 进程和线程的区别

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

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

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

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

进程和线程的区别:

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

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 写的进程. 并且要确保你自己的代码正在运行.
【多线程初阶】第一次认识线程_第6张图片
然后我们点击不安全的连接即可, 不用担心安全问题.
【多线程初阶】第一次认识线程_第7张图片
【多线程初阶】第一次认识线程_第8张图片
这样我们就看到了该程序中创建的三个线程, 以及一个 main 方法的主线程.
【多线程初阶】第一次认识线程_第9张图片

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);
            }
        }
    }
}

实现效果:
【多线程初阶】第一次认识线程_第10张图片
可以看到, 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();
            }
        }

    }

}

总结

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

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

你可能感兴趣的:(多线程学习之路,开发语言,java,java-ee,多线程)