JavaEE之多线程(创建线程的五种写法)详解

😽博主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

  1. 创建一个自己的类, 继承自这个 Thread
  2. 根据刚才的类, 创建出实例
  3. 调用 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);
            }
        }
    }
}

两个线程都在执行, 互不干扰, 运行结果为: 

我们可以发现:  每一秒打印的顺序都是随机的, 这里涉及到一个重要结论, 当一个进程中有多个线程时, 这些线程执行的先后顺序, 是完全随机的. (因为操作系统内核中, 有一个"调度器" 模块, 这个模块的实现方式类似 "随机调度" 的效果)

什么是"随机调度"

  1. 一个进程什么时候被调度到 cpu 上执行的时机是不确定的
  2. 一个线程什么时候从 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 表达式是我们最常用, 最推荐, 最简洁的写法


最后,祝大家天天开心,更上一层楼!关注我🌹,我会持续更新学习分享...🖋️

  • 45
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 23
    评论
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值