😽博主CSDN主页: 小源_😽
🖋️个人专栏: JavaEE
😀努力追逐大佬们的步伐~
目录
1. 前言
2. 操作系统"内核"
3. 创建线程的五种写法 (我们重点要掌握最后一种写法!!)
3.1 继承 Thread, 重写 run
3. 2 实现 Runnable 接口, 重写 run
3.3 继承 Thread, 重写 run, 使用匿名内部类
3.4 实现 Runnable, 重写 run, 使用匿名内部类
3.5 [常用/推荐] 使用 lambda 表达式
4. 小结
1. 前言
我们在写代码的时候, 可以使用多进程进行并发编程, 也可以使用多线程并发编程.
但是多进程并发编程在 Java 中不太推荐, 因为很多和多进程编程相关的 api 在 Java 标准库中都没有提供, 并且在上篇文章中我们讲解了多线程并发编程时效率更高(在需要频繁创建和销毁进程的时候), 并且对于 Java 进程, 需要启动 Java 虚拟机, 导致开销更大 (搞多个 Java 进程就是搞多个 Java 虚拟机)
而系统提供了多线程编程的 api, Java 标准库中把这些 api 封装了, 在代码中可以直接使用. 我们重点学习 Thread 这样的类
本章重点:
本文着重讲解了创建线程的五种写法
2. 操作系统"内核"
我们在学习创建线程之前, 需要先了解操作系统"内核", 它是操作系统中最核心的模块 (用来管理与硬件和给软件提供稳定的运行环境)
操作系统有两个状态: 内核态和用户态, 并且各有自己的空间 (内核空间, 用户空间)
比如我们平时运行的普通的应用程序 (如 idea, java, 画图板, qq音乐......) 都是运行在用户态的, 当操作这些从程序时, 不是应用程序直接操作的, 而是需要调用系统的 api, 在内核中完成操作
为什么要划分出这两个状态呢??
最主要的目的是为了 "稳定": 防止你的应用程序破坏硬件设备或者软件资源
系统封装了一些 api, 这些 api 都是一些合法的操作, 应用程序只能调用这些 api, 就不至于对系统火与硬件设备产生危害 (如果应用程序可以直接操作硬件, 极端情况下, 代码出现 bug, 可能把硬件烧坏)
3. 创建线程的五种写法 (我们重点要掌握最后一种写法!!)
每个线程都是一个独立的执行流, 每个线程都能够独立的去 cpu 上调度执行
3.1 继承 Thread, 重写 run
- 创建一个自己的类, 继承自这个 Thread
- 根据刚才的类, 创建出实例
- 调用 Thread 的 start 方法
package thread;
// 1. 创建一个自己的类, 继承自这个 Thread
// 这个 Thread 类能直接使用, 不需要导入包, 是因为 Java 标准库中, 有一个特殊的包 java.long, 和 String 类似 (也在 java.long 包中)
class MyThread extends Thread {
// 这里重写的 run 入口方法必须手动指定, 针对原有的 Thread 进行扩展 (把一些能复用的复用, 需要扩展的扩展)
@Override
public void run() {
// run 方法就是该线程的入口方法. 和 main 方法类似, main 方法是一个进程的入口方法 (也可以说 main 方法是主线程的入口方法)
// 一个进程至少有一个线程, 这个进程中的第一个线程就叫做"主线程", 如果一个进程只有一个线程, 即 main 线程就是主线程
System.out.println("hello world");
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
// 2. 根据刚才的类, 创建出实例. (线程实例,才是真正的线程).
MyThread t = new MyThread();
// Thread t = new MyThread();
// 3. 调用 Thread 的 start 方法, 才会真正调用系统的 api, 在系统内核中创建出线程, 然后线程就会执行上面的 run 方法了
t.start();
}
}
按照之前的理解 (没有学习多线程之前), 如果一个代码出现了死循环, 最多只能执行一个, 另一个循环是进不去的, 下面我们来创建两个线程
package thread;
class MyThread2 extends Thread {
@Override
public void run() {
while (true) {
System.out.println("hello thread");
// (我们使用的 sleep 是 Java 中封装后的版本, 是 Thread 提供的静态方法) 加 sleep 来降低循环的速度 (这里让 t 线程睡眠 1s (1000ms 等于 1s)), 先写第 19 行代码把鼠标指针放在 sleep 上, 按 Alt + Enter 即可
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Thread t = new MyThread2();
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
两个线程都在执行, 互不干扰, 运行结果为:
我们可以发现: 每一秒打印的顺序都是随机的, 这里涉及到一个重要结论, 当一个进程中有多个线程时, 这些线程执行的先后顺序, 是完全随机的. (因为操作系统内核中, 有一个"调度器" 模块, 这个模块的实现方式类似 "随机调度" 的效果)
什么是"随机调度":
- 一个进程什么时候被调度到 cpu 上执行的时机是不确定的
- 一个线程什么时候从 cpu 上下来, 给别人让位的时机也是不确定的
这是主流操作系统"抢占式执行"的体现, 但是给我们的多线程的安全问题埋下了伏笔
刚才我们只是通过打印的方式看到了两个执行流, 我们也可以使用一些第三方工具更直观地看到多个线程的情况
在 jdk 中, 有一个叫 jconsole 的工具
选择本地进程中我们刚刚执行的 ThreadDemo2 代码, 然后直接连接即可,
直接选择不安全的连接即可
线程是在正在不停的运行的, 当我们点击 Thread-0 线程的详细情况的一瞬间, 相当于"咔嚓"一个快照把这一瞬间的 Thread-0 线程的状态展示出来了 (再次点击时, 线程的详细情况可能会改变)
这里的"堆栈跟踪", 就是线程的调用栈, 描述了线程当前执行到哪个方法的第几行代码, 以及这个方法是如何一层一层调用过去的
除了 main 线程和 t 线程, 其余的线程都是 JVM 自带的线程, 完成一些垃圾回收, 以及监控统计各种指标 (如果我们的代码出现问题, 就可以从中提取一些参考和线索), 把统计指标通过网络的方式, 传输给其他程序,
3. 2 实现 Runnable 接口, 重写 run
只是实现接口时改变, 其余和上面的代码类似
package thread;
// Runnable 可理解为 "可执行的", 通过这个接口, 就可以抽象出一段可以被其他实体执行的代码
class MyThread3 implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("hello runnable");
//睡眠 1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
Thread t = new Thread(new MyThread3());
t.start();
while (true) {
System.out.println("hello main");
//睡眠 1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
3.3 继承 Thread, 重写 run, 使用匿名内部类
内部类是在一个类里面定义的类, 最多使用的就是匿名内部类 (这个类没有名字, 不能重复使用, "用一次就扔掉")
package thread;
public class ThreadDemo4 {
public static void main(String[] args) {
// 写 { 是因为要定义一个新的类, 继承自 Thread, {} 中定义子类的属性和方法, 此处最主要的目的是重写 run 方法
// t 指向的实例不是单纯的 Thread, 而是新定义的匿名内部类 (Thread 的子类)
Thread t = new Thread() {
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
3.4 实现 Runnable, 重写 run, 使用匿名内部类
package thread;
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
// Thread 构造的方法的参数, 填写了 Runnable 的匿名内部类的实例
@Override
public void run() {
while (true) {
System.out.println("hello runnable");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
3.5 [常用/推荐] 使用 lambda 表达式
这是最简洁的写法
lambda 主流语言都有: c++, Python 中叫做 lambda, JS, GO 直接叫做匿名函数
因为方法不能脱离类单独存在, 所以导致上面几种方法为了设置回调函数 而套上了一层类
因此引入了 lambda 表达式 (就是一个匿名函数/方法), Java语法首创, 函数式接口属于 lambda 背后的实现, 相当于在没有破坏原有规则 (方法不能脱离类单独存在) 的基础上, 给了lambda 一个合理的解释
package thread;
public class ThreadDemo6 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
4. 小结
上述的5种写法都是等价的, 可以互相转换的, 用 lambda 表达式是我们最常用, 最推荐, 最简洁的写法
最后,祝大家天天开心,更上一层楼!关注我🌹,我会持续更新学习分享...🖋️