Java Web 实战 02 - 多线程基础篇(1)

news2024/9/21 4:30:59

Java Web 实战 02 - 多线程基础篇 - 1

  • 一 . 认识线程
    • 1.1 概念
      • 1.1.1 什么是线程?
      • 1.1.2 为什么要有多个线程?
      • 1.1.3 进程和线程的区别(面试题)
    • 1.2 第一个多线程程序
    • 1.3 创建线程
      • 1.3.1 继承Thread类
      • 1.3.2 实现Runnable接口
      • 1.3.3 继承 Thread 类 , 使用匿名内部类
      • 1.3.4 实现 Runnable , 使用匿名内部类
      • 1.3.5 lambda 表达式来定义任务(推荐)
    • 1.4 多线程的好处
    • 1.5 多线程的使用场景
    • 1.6 小结

大家好 , 这篇文章给大家带来的是多线程相关的基础知识 , 我们先介绍一下什么是线程、创建线程的方法、多线程的好处以及使用场景等。

由于 C 站的编辑器不太好用 , 导致许多排版没能生效 , 大家可移步至这里观看

https://www.yuque.com/jialebihaitao/study/qzym2pw332lm6k7q?singleDoc# 《2. 多线程 (基础)》

感谢大家的支持~

一 . 认识线程

1.1 概念

1.1.1 什么是线程?

线程和进程之间 , 确实有一定的联系 .

线程(Thread) : 更加轻量的进程 , 也是一种实现并发编程的方案 , 创建线程和销毁线程的时候 , 比创建进程销毁进程更加轻量
个人理解 : 线程是比进程还小的单位 , 一个进程里面有多个线程 , 每个线程分别完成自己的任务 , 他们可以同时进行

举个栗子 :
一家三口来饭店吃饭 , 可是这个时候只有老板在 , 端茶做菜忙活不过来 , 所以就把老板娘叫过来了 . 这时候 , 就有两个线程 “老板” “老板娘”
此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中老板娘是老板叫来的,所以老板我们一般被称为主线程

1.1.2 为什么要有多个线程?

我们先来思考个问题,为什么要有多个进程?

CPU 单个核心已经开发到极致了 , 要想提升算力 , 就需要用多个核心 . 就需要"并发编程"
引入并发编程最大的目的就是为了能够充分的利用好 CPU 的多核资源。
使用多进程这种编程模式是完全可以做到并发编程的 , 并且也能够使 CPU 多核被充分利用

但是在有些场景下会存在问题。
如果需要频繁的 创建 / 销毁 进程,多进程这个时候就会比较低效。

例如你写了一个服务器程序,服务器要同一时刻给很多客户提供服务,那么这个时候就要用到并发编程了
典型的做法就是给每个客户端分配一个进程 , 提供一对一的服务。

客户端访问就创建,客户端离开了就销毁。

但是创建 / 销毁 进程本身就是一个比较低效的操作。

  1. 创建PCB

    PCB 也叫 进程控制块。它的作用是操作系统表示进程的属性的结构体 , 这个结构体里就包含了一些表示进程的核心信息。

  2. 分配系统资源(尤其是内存资源)

    分配资源就比较浪费时间了
    这个是要在系统内核资源管理模块 , 进行一系列遍历操作的

  3. 把PCB加到内核的双向链表中

为了提高这个场景下的效率,我们就引入了线程这个概念 , 线程也叫做"轻量级进程"
一个线程其实是包含在进程中的。(一个进程里面可以有很多个线程)
每个线程其实也有自己的 PCB (所以一个进程里面有可能对应多个 PCB )
同一个进程里的多个线程之间共用同一份系统资源

这就意味着新创建的线程不必重新分配系统资源,只需要复用之前的即可。

因此创建线程只需要 :

  1. 创建PCB
  2. 把PCB加到内核的链表中

这就是线程相对于进程做出的重大的改进,也就是线程更轻量的原因。
举个栗子吧

江南皮革厂老总生意非常好 , 他想扩充他的生意。现在有两种方案
方案一 : 再租个厂子
方案二 : 在原来的厂子基础上进行扩建。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hFNqhUXI-1678060185045)(https://www.yuque.com/api/filetransfer/images?url=https%3A%2F%2Fjialebihaitao.oss-cn-beijing.aliyuncs.com%2Fimage-20220722204738607.png&sign=9c6b6fafff0317ef8dd6acabb3b109c7aa439ca246747a9d6df6c0a348cae980)]

线程是包含在进程内部的"逻辑执行流"。(线程可以执行一段单独的代码,多个线程之间是并发举行的。)
操作系统进行调度的时候,其实也是以"线程为单位"进行调度的。
创建线程的开销比创建进程要小,销毁线程的开销也比销毁进程要小
那么如果把进程比作一座工厂 , 线程就是工厂内部的流水线

再举个栗子 :

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-45EvnWmO-1678060185046)(https://www.yuque.com/api/filetransfer/images?url=https%3A%2F%2Fjialebihaitao.oss-cn-beijing.aliyuncs.com%2Fimage-20220814122502425.png&sign=d3d9e61173e5a1c8c03d66738f4ac4f3c0f69a8cd7703384631c7679dad30289)]

1.1.3 进程和线程的区别(面试题)

  1. 进程是包含线程的 , 线程是在进程内部的

每个进程至少有一个线程 , 叫做主线程

  1. 每个进程有独立的虚拟地址空间 , 也有自己独立的文件描述符表 . 同一个进程的多个线程之间 , 共用这一份虚拟地址空间和文件描述符表
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XgCC7w7T-1678060185046)(C:\Users\xht13\Desktop\64032db09e1bf-16779299509917.jpg)]

  2. 进程是操作系统中"资源分配"的基本单位 , 线程是操作系统中"调度执行"的基本单位

  3. 多个进程同时执行的时候 , 如果一个进程挂了 , 一般不会影响到别的进程但是同一个进程中的多个线程之间 , 如果一个线程挂了 , 就很有可能把整个进程带走 , 同一个进程中的其他线程也就没了

1.2 第一个多线程程序

即使是一个最简单的"Hello World"程序 , 其实在运行的时候 , 也涉及到线程了 .

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

虽然在上述代码中 , 我们并没有手动创建其他线程
但是 Java 程序在运行的时候 , 内部也会创建出多个线程 .
一个进程里面至少会有一个线程 , 运行这个程序 , 操作系统就会创建出一个 Java 进程 , 在这个 Java 进程里面就会有一个线程调用 main 方法
谈到多进程 , 经常会谈到"父进程" “子进程”

进程A里面创建了进程B
A是B的父进程 , B是A的子进程

但是在多线程里面 , 没有"父线程" "子线程"这种说法 , 即使它们之间也存在创建与被创建的关系 , 但是仍然认为线程之间地位是相等的

1.3 创建线程

1.3.1 继承Thread类

  1. 继承 Thread 类来创建一个线程类 , 然后重写里面的 run 方法
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Hello Mythread");
    }
}
  1. 创建 MyThread 类的实例
Thread t = new MyThread();
  1. 调用 start 方法启动线程
t.start();

整体的代码 :

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Hello Mythread");
    }
}
public class Demo1 {
    public static void main(String[] args) {
        // 创建一个线程
        // 在Java中,创建一个线程,离不开一个重要的类:Thread
        // 创建方式:写一个子类,继承Thread,重写里面的run方法
        Thread t = new MyThread();//向上转型:父类类型 对象名 = new 子类类型();
        t.start();

        System.out.println("Hello main");
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U2WITMdf-1678060185047)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220816132455107.png#id=XLNRY&originHeight=698&originWidth=1783&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

在 start 之前,线程只是准备好了,并没有真正被创建出来,只有执行了 start 方法之后,才在操作系统中真正创建了线程。

在操作系统中创建线程:

  1. 创建 PCB
  2. 把 PCB 加到链表中

那么我们来看一下运行结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-an8xz2vA-1678060185047)(https://www.yuque.com/api/filetransfer/images?url=https%3A%2F%2Fjialebihaitao.oss-cn-beijing.aliyuncs.com%2Fimage-20220725094059915.png&sign=7d9df01fef1999dfa6cb02f8ee10f9ce86e1842e8ff9c8908961f8ab0665c351)]

在这个代码中 , 虽然我们是先启动线程 , 再打印 “Hello main”
但是实际运行结果 , 是先打印 “Hello main” , 再打印 “Hello MyThread”
那这是怎么回事呢 ?

  1. 每个线程是独立的执行流

main 对应的线程是一个执行流
MyThread 对应的线程又是一个执行流
main 线程和 MyThread 各跑各的 , 互不影响 .
这两个执行流是 并发(并行+并发) 的执行关系

  1. 此时两个线程执行的先后顺序 , 取决于操作系统调度器的具体实现

这里面的调度器里面的调度规则 , 可以简单地视为"随机调度"
因此我们看到的虽然是先打印 “Hello main” , 后打印 “Hello MyThread” , 但是不是一直都是这样的 . 当前看到的先打印 main , 大概率是因为受到创建线程自身的开销影响的
哪怕我们运行1000次 , main在前 , 我们也不能说第1001次的时候 , main还是在前面的 .
所以我们需要注意 : **编写多线程的代码的时候 , 默认情况下 , 代码是无序执行的 , 是操作系统"随机调度的" , 所以我们不要想当然的认为多线程的执行顺序是从上到下的 . **

但是我们还是可以影响到线程执行的先后顺序的 , 但是调度器自身的"随机调度"的行为修改不了
调度器依然是"随机调度" , 咱们最多能做到的就是让某个线程先等待一会 , 等待另一个线程执行完了我们再去执行

那么我们再来看一下进程结束的信息

我们觉得之前运行的太快了 , 我们可以让他不结束 , 这样就可以观察一下里面都有什么线程了

class MyThread extends Thread {
    @Override
    public void run() {
        while(true) {
            System.out.println("Hello Mythread");
        }
    }
}
public class Demo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();

        while(true) {
            System.out.println("Hello main");
        }
    }
}

我们可以看到 , 程序进入死循环正在疯狂执行 , 那么我们可以使用 Java 官方提供给我们的工具来查看有哪些线程
我们找到 JDK 的安装目录 , 里面的 bin 目录有个 jconsole 工具
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lX0l057R-1678060185047)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220725105251529.png#id=j6SIE&originHeight=742&originWidth=884&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

一进来就看到了我们的进程 , 点击连接 , 我们就能看到线程具体信息了 .
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RSlFwmVy-1678060185048)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220725105335938.png#id=P4LW0&originHeight=742&originWidth=884&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-99sSUaBT-1678060185048)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220725105343856.png#id=pZZT4&originHeight=742&originWidth=884&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vNbuXX3j-1678060185048)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220725105428734.png#id=tj4wh&originHeight=742&originWidth=884&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

要注意的情况是 :
想要查看具体的线程信息 , 需要保证程序是一直在运行的状态 , 如果你把程序终止运行了 , 那么你就观察不到任何信息了
还有可能是正在运行但是还是什么也看不到 , 那么尝试一下用管理员方式打开再去试一下 , 应该就没问题了 .
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XdLdmANE-1678060185049)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220725111417441.png#id=EThA8&originHeight=1679&originWidth=1866&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

那么刚才的程序执行的太快了 , 不方便我们观察执行结果 .
那么我们可以使用 sleep 函数 , 来让线程适当休息一下

使用 **Thread.sleep()** 的方式进行休眠
sleep 是 Thread 的静态方法 , 类名直接调用即可

sleep 函数的参数是时间 , 单位是 ms , 意思是想让线程休息多长时间

时间单位的换算 :
1 s = 1000 ms
1 ms = 1000 us
1 us = 1000 ns
1 ns = 1000 ps
进行一次网络通信 , 花的时间大概就是 us-ms 级的
进行一次读写硬盘 , 花的时间大概就是 ns-us 级的
进行一次读写内存 , 花的时间大概就是 ps - ns 级的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fO4FoVuw-1678060185049)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220725112723610.png#id=XDyWT&originHeight=1030&originWidth=1920&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

我们发现 , 当我们写上 sleep 函数的时候 , 报错了 .
所以我们要处理一下 , 点击报错位置 ,alt + 回车
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9m0U4HH7-1678060185050)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220725112819913.png#id=IXHKz&originHeight=1030&originWidth=1920&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PyjDAEFv-1678060185050)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220725112835715.png#id=a7wnG&originHeight=1030&originWidth=1920&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

就自动把异常捕获了 , 但是 run 函数这里面只能使用 try catch语句 , 不能使用其他的捕获异常的操作 .
因为 run 函数是重写的方法 , 原函数就没有提供其他的捕获异常的方法
在这里插入图片描述

我们的 main 方法里面要进行异常捕获 , 就有两种方法了 , 一个是 try catch , 一个是 throws 抛出异常 , throws 是交给上一层(在这里面就是JVM)来处理 , 推荐使用 try catch 进行捕获
在这里插入图片描述

class MyThread extends Thread {
    @Override
    public void run() {
        while(true) {
            System.out.println("Hello Mythread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Demo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();

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

查看一下运行结果 :
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i2V0GjAQ-1678060185051)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220725113342669.png#id=PyHoX&originHeight=1030&originWidth=1920&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

那么这里面就会有一个常见面试题 :
谈一谈 Thread 的 run 和 start 的区别

使用 start , 可以看到两个线程并发执行 , 两组打印交替出现
使用 run , 只打印 Hello MyThrad , 没有打印 Hello main
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q7yZigpk-1678060185051)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220725113953894.png#id=lanyM&originHeight=1030&originWidth=1920&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

直接调用 run , 并没有创建出新的线程 , 只是在之前的线程中(在这里面也就是main线程) , 执行了 run 里面的内容 , 所以只打印了 Hello MyThread
使用 start , 则是创建新的线程 , 新的线程里面调用 run , 新线程和旧线程之间是并发执行的关系

1.3.2 实现Runnable接口

class MyRunnable implements Runnable {
    @Override
    public void run() {
        while(true) {
            System.out.println("Hello Mythread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Demo2 {
    public static void main(String[] args) {
        Runnable 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();
            }
        }
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Luv86kIj-1678060185052)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220725120356054.png#id=Wtoeo&originHeight=751&originWidth=1677&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

//线程本身:Runnable runnable = new MyRunnable();
//任务内容:Thread t = new Thread(runnable);
//这两句代码,把任务内容和线程本身,给分离开了,这样耦合度就降低了。
//这样的好处就是让任务的内容和线程关系不大,假设这个任务不想通过多线程执行了,换成别的方式执行,这时候代码的改动也不会特别大

那么有个问题,为什么刚才使用 Thread、Runnable、Interruption 等都不需要 import?

因为这几个类都在 java.lang 里面,默认自动导入的
还有一种情况,是不需要导包的 ,那就是这几个类在同一个包里面,就不需要导包。

1.3.3 继承 Thread 类 , 使用匿名内部类

这个方法仍然是继承 Thread 类,但是不再显式继承,而是使用"匿名内部类"

我们之前在数据结构里面学过,使用优先级队列 PriorityQueue 就可以使用 Comparable 或者 Comparator 来指定比较规则。
使用这两个接口的时候,就可以使用匿名内部类的写法

public class Demo2 {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                while(true) {
                    System.out.println("Hello MyThread");
                    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();
            }
        }
    }
}

1.3.4 实现 Runnable , 使用匿名内部类

public class Demo3 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while(true) {
                    System.out.println("Hello MyThread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        Thread t = new Thread(runnable);
        t.start();

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

我们还可以这样写

public class Demo4 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true) {
                    System.out.println("Hello MyThread");
                    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();
            }
        }
    }
}

1.3.5 lambda 表达式来定义任务(推荐)

public class Demo5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while(true) {
                System.out.println("Hello MyThread");
                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 表达式 , lambda 表达式的写法是不需要重写 run 方法的

lambda表达式其实也是一种匿名函数(只使用一次,还没有名字)
像lambda表达式这种能简化代码的,叫做"语法糖"

除了上面这5种创建线程的方式,还有好几种,不介绍了,其实至少有7种方式来可以创建线程
对于创建线程的方式到底有哪些,这也是个经典面试题了

1.4 多线程的好处

使用多线程,能够充分的利用CPU多核资源
比如这个操作:

public class Demo7 {
    private static final long num = 20_0000_0000;//20亿

    public static void serial() {
        long begin_time = System.currentTimeMillis();
        long n1 = 0;
        for (long i = 0; i < num; i++) {
            n1++;
        }
        long n2 = 0;
        for (long i = 0; i < num; i++) {
            n2++;
        }
        long end_time = System.currentTimeMillis();
        System.out.println("单线程消耗的时间:" + (end_time - begin_time) + "ms");
    }
    public static void main(String[] args) {
        serial();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eFgNqf66-1678060185052)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220815114338919.png#id=Tyj3z&originHeight=1030&originWidth=1920&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

我们明显可以看到:单线程执行的效率是不太高的,基本运行了大约2s,直接就给人一个等待的感觉了。
那么我们来看看多线程的速度如何?

public class Demo8 {
    private static final long num = 20_0000_0000;

    public static void concurrency() {

        long begin_time = System.currentTimeMillis();

        Thread t1 = new Thread(() -> {
            long a = 0;//注意:a b不能在外面定义,访问不到
            for (long i = 0; i < num; i++) {
                a++;
            }
        });

        Thread t2 = new Thread(() -> {
            long b = 0;
            for (long i = 0; i < num; i++) {
                b++;
            }
        });

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

        long end_time = System.currentTimeMillis();
        System.out.println("多线程消耗的时间:" + (end_time - begin_time) + "ms");
    }
    public static void main(String[] args) {
        concurrency();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l8I42tPA-1678060185053)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220815115220638.png#id=aP0W7&originHeight=1030&originWidth=1920&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

多线程确实嗷嗷快。
但是有一个问题:
这个代码涉及到三个进程 t1 t2 main ,他们都是并发执行的,谁先执行完谁后执行完是不确定的 , 很有可能 t1 t2 线程还没执行完 , main 线程就结束战斗了 .
就比如 1000m 长跑,main 是裁判,t1 t2 是两名运动员。t1 t2 两个兄弟在 main 的一声哨响中出发了,然后main 就直接停表了 , 那么实际上测出的 t1 t2 两名运动员的成绩误差是很大的,所以我们需要注意一下,我们可以采取让 main 等到 t1 t2 到达终点再停止计时 . 这里我们需要用到 join 这个关键字
join关键字 是等待线程结束

在这里的意思就是等待 t1 t2 结束 , main 线程再结束

在主线程当中调用 **t1.join()** 意思就是让 main 线程等待t1执行完
所以代码应该改成这样才合理

public class Demo8 {
    private static final long num = 20_0000_0000;

    public static void concurrency() {

        long begin_time = System.currentTimeMillis();

        Thread t1 = new Thread(() -> {
            long a = 0;
            for (long i = 0; i < num; i++) {
                a++;
            }
        });

        Thread t2 = new Thread(() -> {
            long b = 0;
            for (long i = 0; i < num; i++) {
                b++;
            }
        });

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

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long end_time = System.currentTimeMillis();
        System.out.println("多线程消耗的时间:" + (end_time - begin_time) + "ms");
    }
    public static void main(String[] args) {
        concurrency();
    }
}

这时候的运行时间才更精确一些
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MD5F9XY2-1678060185053)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220815115935063.png#id=GiEk4&originHeight=1030&originWidth=1920&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

我们可以把单线程的执行方式和多线程的执行方式放在一起比较一下

public class Demo9 {
    private static final long num = 20_0000_0000;//20亿

    public static void serial() {
        long begin_time1 = System.currentTimeMillis();
        long n1 = 0;
        for (long i = 0; i < num; i++) {
            n1++;
        }
        long n2 = 0;
        for (long i = 0; i < num; i++) {
            n2++;
        }
        long end_time1 = System.currentTimeMillis();
        System.out.println("单线程消耗的时间:" + (end_time1 - begin_time1) + "ms");
    }

    public static void concurrency() {

        long begin_time = System.currentTimeMillis();

        Thread t1 = new Thread(() -> {
            long a = 0;
            for (long i = 0; i < num; i++) {
                a++;
            }
        });

        Thread t2 = new Thread(() -> {
            long b = 0;
            for (long i = 0; i < num; i++) {
                b++;
            }
        });

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

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long end_time = System.currentTimeMillis();
        System.out.println("多线程消耗的时间:" + (end_time - begin_time) + "ms");
    }

    public static void main(String[] args) {
        serial();
        concurrency();
    }
}

看一下运行结果 , 多线程要比单进程快一倍左右
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pIeissxv-1678060185054)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220815120221879.png#id=FesDN&originHeight=1030&originWidth=1920&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

那么我们之前没加 join 的时候 , 多线程不是很快嘛 , 这怎么加上 join 之后咋还慢了?

加上 join 之后才是裁判等到选手到终点,才停止计时。
如果没有 join 的限制,main、t1、t2都是同时向下走的,走的过程中,调度顺序是不确定的。
最极端的情况就是一直在执行 main 线程 , t1 t2 两个线程都没有被执行 , 这样的结果肯定是不对的
有可能先执行 main ,再执行 t1 ,再执行 t2
有可能先执行 t1,再执行 t2,再去执行 main,再去执行 t1…
是有很多种可能性的。

那么我们继续来看 , 单线程消耗时间的时间是1095ms,多线程消耗的时间是581ms,那么为什么不是单线程执行时间的一半呢(即547.5s)呢?

  1. 创建线程,也是有开销的
  2. 两个线程在CPU上不一定就是纯并发执行,有可能一部分时间并行执行,一部分时间并发执行
  3. 线程的调度也是有开销的。

1.5 多线程的使用场景

  1. 在CPU密集型区域

代码中的大部分工作,都是在使用CPU进行计算,使用多线程,就可以充分利用CPU多核资源,可以提高效率

  1. 在IO密集型场景

I 指的是 Input,O 指的是 Output
读写磁盘、读写网卡等等这些都是 IO 操作,需要花费很长的时间等待,但是像这种 IO 操作,基本都是不消耗 CPU 就可以快速完成的工作,那么这时候 CPU 就在摸鱼,就可以给 CPU 找点活干

比如在食堂打饭,要排队很久,有的同学就拿出手机背背单词,这个时候就算是等待 IO 结束,我们给 CPU指定点活

1.6 小结

多线程的创建顺序是由 start 的顺序决定的 , 但是执行顺序是不确定的 , 这取决于系统的调度器怎么处理
意思就是我们先创建线程,不一定就是先去执行。
举个栗子:

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

这样的情况就不知道谁先被执行了 , 具体线程里的任务啥时候执行 , 要看调度器

比如:我跟 A B C 依次确定了关系 , 但是我不一定第一天就去跟 A 搞事情 , 跟 B/C 谁发生关系这都是不确定的 , 视心情而定

但是我们这样呢

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

这样的意思就是我跟 A 确定关系一年了,我再去跟 B C 接触
那么大概率就是先执行 A (因为都接触一年了) , 也不排除先执行 B/C (可能网恋,见不了面 , 就先跟本地的 B/C 交往)


到此 , 本篇文章就结束了 , 敬请期待后续!
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/391614.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux嵌入式开发 | 汇编驱动LED(1)

文章目录&#x1f697; &#x1f697;Linux嵌入式开发 | 汇编驱动LED&#xff08;1&#xff09;&#x1f697; &#x1f697;初始化IO&#x1f697; &#x1f697;STM32&#x1f697; &#x1f697;使能GPIO时钟&#x1f697; &#x1f697;设置IO复用&#x1f697; &#x1f6…

3.5多线程

一.线程的状态1.NEW安排了工作,还未开始行动把Thread对象创建好了,但是还没有调用startjava内部搞出来的状态,与PCB的状态没什么关系2.TERMINATED工作完成了操作系统的线程执行完毕,销毁了,但是Thread对象还在,获取的对象3.RUNNABLE可以工作的,又可以分为正在工作中和即将开始工…

聊聊内存那些事(基于单片机系统)

单片机的RAM和ROM单片机的ROM&#xff0c;叫只读程序存储器&#xff0c;是FLASH存储器构成的&#xff0c;如U盘就是FLASH存储器。所以&#xff0c;FLASH和ROM是同义的。单片机的程序&#xff0c;就是写到FLASH中了。而RAM是随机读/写存储器&#xff0c;用作数据存储器&#xff…

SpringBoot笔记(一)入门使用

一、为什么用SpringBootSpringBoot优点创建独立Spring应用内嵌web服务器自动starter依赖&#xff0c;简化构建配置自动配置Spring以及第三方功能提供生产级别的监控、健康检查及外部化配置无代码生成、无需编写XMLSpringBoot缺点人称版本帝&#xff0c;迭代快&#xff0c;需要时…

电路基础(1)电路模型和电路定律

电路中的电压、电流之间具有两种约束&#xff0c;一种是由电路元件决定的元件约束&#xff1b;另一种是元件间连接而引入的几何约束&#xff08;就是拓扑约束&#xff09;&#xff0c;后者由基尔霍夫定律来表达。基尔霍夫定律是集总参数电路的基本定律。 1.电路和电路模型电源又…

电路模型和电路定律(2)——“电路分析”

各位CSDN的uu们你们好呀&#xff0c;好久没有更新电路分析的文章啦&#xff0c;今天来小小复习一波&#xff0c;之前那篇博客&#xff0c;小雅兰更新了电路的历史以及电压电流的参考方向&#xff0c;这篇博客小雅兰继续&#xff01;&#xff01;&#xff01; 电阻元件 电压源和…

FFMPEG 安装教程windowslinux(CentOS版)

ps: 从笔记中迁移至blog 版本概述 Windows 基于win10 Linux 基于CentOS 7.6 一.Windows安装笔记 1.下载安装 https://ffmpeg.org/download.html 2 解压缩&#xff0c;拷贝到需要目录&#xff0c;重命名 3 追加环境变量 echo %PATH%setx /m PATH "%PATH%;F:\dev_tools\…

用C/C++制作一个简单的俄罗斯方块小游戏

用C/C制作一个简单的俄罗斯方块小游戏 用C/C制作一个简单的俄罗斯方块小游戏 0 准备1 游戏界面设计 1.1 界面布局1.2 用 EasyX 显示界面1.3 音乐播放 2 方块设计 2.1 方块显示2.2 随机生成一个方块2.3 方块记录 3 方块移动和旋转 3.1 方块的移动3.2 方块的旋转3.3 方块的碰撞和…

基于 WebSocket、Spring Boot 教你实现“QQ聊天功能”的底层简易demo

目录 前言 一、分析 1.1、qq聊天功能分析 1.2、WebSocket介绍 1.2.1、什么是消息推送呢&#xff1f; 1.2.2、原理解析 1.2.3、报文格式 二、简易demo 2.1、后端实现 2.1.1、引入依赖 2.1.2、继承TextWebSocketHandler 2.1.3、实现 WebSocketConfigurer 接口 2.2、…

LeetCode096不同的二叉搜索树(相关话题:卡特兰数)

目录 题目描述 解题思路 代码实现 进出栈序列理解卡特兰数分析策略 相关知识 参考文章 题目描述 给你一个整数 n &#xff0c;求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种&#xff1f;返回满足题意的二叉搜索树的种数。 示例 1&#xff1a; …

《程序员面试金典(第6版)》面试题 02.07. 链表相交

题目描述 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点&#xff0c;返回 null 。 图示两个链表在节点 c1 开始相交&#xff1a; 题目数据 保证 整个链式结构中不存在环。 注意&#xff0c;函数返回结果…

socket本地多进程通信基本使用方法和示例

目录 前言&#xff1a; socket是什么 socket基本原理框图 socket基本函数 1 socket() 函数 2 bind()函数 3 connect()函数 4 listen() 函数 5 accept() 函数 6 read() write() send() recv()函数 7 close()函数 8 字节序转换&#xff08;hton&#xff09; 示例代码 …

使用 Pulumi 打造自己的多云管理平台

前言在公有云技术与产品飞速发展的时代&#xff0c;业务对于其自身的可用性提出了越来越高的要求&#xff0c;当跨区域容灾已经无法满足业务需求的情况下&#xff0c;我们通常会考虑多云部署我们的业务平台&#xff0c;以规避更大规模的风险。但在多云平台部署的架构下&#xf…

埃安自研版图扩至夸克电驱,动力研发团队已超1000人

埃安的三电自研版图正在扩大。3月3日&#xff0c;广汽集团旗下埃安发布了一项名为“夸克电驱”的技术产品&#xff0c;相比主流电驱体积减少一倍&#xff0c;同时电机功率密度比主流电驱增加了一倍。此前&#xff0c;比亚迪刚刚发布易四方动力系统&#xff0c;特斯拉也在投资者…

HTML常见标签

文章目录一、HTML基础标签注释标签标题标签段落标签换行标签格式化标签图片、音频、视频标签超链接标签列表标签表格标签布局标签表单标签表单标签概述form标签属性表单项标签综合案例一、HTML基础标签 基础标签就是和文字相关的标签 标签描述<h1> ~ <h6>定义标题…

【项目管理】晋升为领导后,如何开展工作?

兵随将转&#xff0c;作为管理者&#xff0c;你可以不知道下属的短处&#xff0c;却不能不知道下属的长处。晋升为领导后&#xff0c;如何开展工作呢&#xff1f; 金九银十&#xff0c;此期间换工作的人不在少数。有几位朋友最近都换了公司&#xff0c;职位得到晋升&#xff0c…

前端——1.相关概念

这篇文章主要介绍前端入门的相关概念 1.网页 1.1什么是网页&#xff1f; 网站&#xff1a;是指在因特网上根据一定的规则&#xff0c;使用HTML等制作的用于展示特定内容相关的网页集合 网页&#xff1a;是网站中的一“页”&#xff0c;通常是HTML格式的文件&#xff0c;它要…

JAVA后端部署项目三步走

1. JAVA部署项目三步走 1.1 查看 运行的端口 lsof -i:8804 &#xff08;8804 为端口&#xff09; 发现端口25111被监听 1.2 杀死进程,终止程序 pid 为进程号 kill -9 pid 1.3 后台运行jar包 nohup java -jar -Xms128M -Xmx256M -XX:MetaspaceSize128M -XX:MaxM…

C++笔记之lambda表达式

引言 Lambda表达式是从C 11版本引入的特性&#xff0c;利用它可以很方便的定义匿名函数对象&#xff0c;通常作为回调函数来使用。大家会经常拿它和函数指针&#xff0c;函数符放在一起比较&#xff0c;很多场合下&#xff0c;它们三者都可以替换着用。 语法 [ captures ] (…

javaScript基础面试题 ---宏任务微任务

宏任务微任务一、为什么JS是单线程语言&#xff1f;二、JS是单线程&#xff0c;怎样执行异步代码&#xff1f;1、JS是单线程语言 2、JS代码执行流程&#xff0c;同步执行完&#xff0c;再进行事件循环&#xff08;微任务、宏任务&#xff09; 3、清空所有的微任务&#xff0c;再…