多线程初阶系列目录
持续更新中
1.第一次认识线程
…
文章目录
- 多线程初阶系列目录
- 前言
- 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.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 类
- 继承 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);
}
}
}
}
- 创建 MyThread 类的实例
// 向上转型
Thread t = new MyThread();
- 调用 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 接口
- 实现 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);
}
}
}
}
- 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
Runnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
- 调用 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();
}
}
}
}
总结
✨ 本节目标主要为,认识多线程并掌握多线程程序的编写, 重点需要掌握进程与线程的区别, 以及创建线程都有哪些方法. 本人还会继续更新多线程相关内容, 请持续关注.
✨ 想了解更多的多线程知识, 可以收藏一下本人的多线程学习专栏, 里面会持续更新本人的学习记录, 跟随我一起不断学习.
✨ 感谢你们的耐心阅读, 博主本人也是一名学生, 也还有需要很多学习的东西. 写这篇文章是以本人所学内容为基础, 日后也会不断更新自己的学习记录, 我们一起努力进步, 变得优秀, 小小菜鸟, 也能有大大梦想, 关注我, 一起学习.
再次感谢你们的阅读, 你们的鼓励是我创作的最大动力!!!!!