Java Web 实战 02 - 多线程基础篇(1)
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 多核被充分利用
但是在有些场景下会存在问题。
如果需要频繁的 创建 / 销毁 进程,多进程这个时候就会比较低效。
例如你写了一个服务器程序,服务器要同一时刻给很多客户提供服务,那么这个时候就要用到并发编程了
典型的做法就是给每个客户端分配一个进程 , 提供一对一的服务。客户端访问就创建,客户端离开了就销毁。
但是创建 / 销毁 进程本身就是一个比较低效的操作。
创建PCB
PCB 也叫 进程控制块。它的作用是操作系统表示进程的属性的结构体 , 这个结构体里就包含了一些表示进程的核心信息。
分配系统资源(尤其是内存资源)
分配资源就比较浪费时间了
这个是要在系统内核资源管理模块 , 进行一系列遍历操作的把PCB加到内核的双向链表中
为了提高这个场景下的效率,我们就引入了线程这个概念 , 线程也叫做"轻量级进程"。
一个线程其实是包含在进程中的。(一个进程里面可以有很多个线程)
每个线程其实也有自己的 PCB (所以一个进程里面有可能对应多个 PCB )
同一个进程里的多个线程之间共用同一份系统资源。
这就意味着新创建的线程不必重新分配系统资源,只需要复用之前的即可。
因此创建线程只需要 :
- 创建PCB
- 把PCB加到内核的链表中
这就是线程相对于进程做出的重大的改进,也就是线程更轻量的原因。
举个栗子吧
江南皮革厂老总生意非常好 , 他想扩充他的生意。现在有两种方案
方案一 : 再租个厂子
方案二 : 在原来的厂子基础上进行扩建。
线程是包含在进程内部的"逻辑执行流"。(线程可以执行一段单独的代码,多个线程之间是并发举行的。)
操作系统进行调度的时候,其实也是以"线程为单位"进行调度的。
创建线程的开销比创建进程要小,销毁线程的开销也比销毁进程要小
那么如果把进程比作一座工厂 , 线程就是工厂内部的流水线
再举个栗子 :
1.1.3 进程和线程的区别(面试题)
- 进程是包含线程的 , 线程是在进程内部的
每个进程至少有一个线程 , 叫做主线程
-
每个进程有独立的虚拟地址空间 , 也有自己独立的文件描述符表 . 同一个进程的多个线程之间 , 共用这一份虚拟地址空间和文件描述符表
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XgCC7w7T-1678060185046)(C:\Users\xht13\Desktop\64032db09e1bf-16779299509917.jpg)]](https://img-blog.csdnimg.cn/49a120b03dfd4fdbbc325d299000fa03.png)
-
进程是操作系统中"资源分配"的基本单位 , 线程是操作系统中"调度执行"的基本单位
-
多个进程同时执行的时候 , 如果一个进程挂了 , 一般不会影响到别的进程但是同一个进程中的多个线程之间 , 如果一个线程挂了 , 就很有可能把整个进程带走 , 同一个进程中的其他线程也就没了
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类
- 继承 Thread 类来创建一个线程类 , 然后重写里面的 run 方法
class MyThread extends Thread {@Overridepublic void run() {System.out.println("Hello Mythread");}
}
- 创建 MyThread 类的实例
Thread t = new MyThread();
- 调用 start 方法启动线程
t.start();
整体的代码 :
class MyThread extends Thread {@Overridepublic 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=)]](https://img-blog.csdnimg.cn/0c8f9ff47f4349629f6fe91f180e7e2d.png)
在 start 之前,线程只是准备好了,并没有真正被创建出来,只有执行了 start 方法之后,才在操作系统中真正创建了线程。
在操作系统中创建线程:
- 创建 PCB
- 把 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)]](https://img-blog.csdnimg.cn/590b019e46e6489fa71ce7ef9768e44f.png)
在这个代码中 , 虽然我们是先启动线程 , 再打印 “Hello main”
但是实际运行结果 , 是先打印 “Hello main” , 再打印 “Hello MyThread”
那这是怎么回事呢 ?
- 每个线程是独立的执行流
main 对应的线程是一个执行流
MyThread 对应的线程又是一个执行流
main 线程和 MyThread 各跑各的 , 互不影响 .
这两个执行流是 并发(并行+并发) 的执行关系
- 此时两个线程执行的先后顺序 , 取决于操作系统调度器的具体实现
这里面的调度器里面的调度规则 , 可以简单地视为"随机调度"
因此我们看到的虽然是先打印 “Hello main” , 后打印 “Hello MyThread” , 但是不是一直都是这样的 . 当前看到的先打印 main , 大概率是因为受到创建线程自身的开销影响的
哪怕我们运行1000次 , main在前 , 我们也不能说第1001次的时候 , main还是在前面的 .
所以我们需要注意 : **编写多线程的代码的时候 , 默认情况下 , 代码是无序执行的 , 是操作系统"随机调度的" , 所以我们不要想当然的认为多线程的执行顺序是从上到下的 . **
但是我们还是可以影响到线程执行的先后顺序的 , 但是调度器自身的"随机调度"的行为修改不了
调度器依然是"随机调度" , 咱们最多能做到的就是让某个线程先等待一会 , 等待另一个线程执行完了我们再去执行
那么我们再来看一下进程结束的信息
我们觉得之前运行的太快了 , 我们可以让他不结束 , 这样就可以观察一下里面都有什么线程了
class MyThread extends Thread {@Overridepublic 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=)]](https://img-blog.csdnimg.cn/cccb400195a34ea39877e05530ecd95d.png)
一进来就看到了我们的进程 , 点击连接 , 我们就能看到线程具体信息了 .
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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=)]](https://img-blog.csdnimg.cn/7b481a569be54680b8f69da4aab7d3e6.png)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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=)]](https://img-blog.csdnimg.cn/624f25404ea346af8cb1b4b2e3d6c1c4.png)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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=)]](https://img-blog.csdnimg.cn/3854fc0a234c4b83b2177c5aa889b70a.png)
要注意的情况是 :
想要查看具体的线程信息 , 需要保证程序是一直在运行的状态 , 如果你把程序终止运行了 , 那么你就观察不到任何信息了
还有可能是正在运行但是还是什么也看不到 , 那么尝试一下用管理员方式打开再去试一下 , 应该就没问题了 .
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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=)]](https://img-blog.csdnimg.cn/0e9f50eda1b74371aca5fe01b67fc1e8.png)
那么刚才的程序执行的太快了 , 不方便我们观察执行结果 .
那么我们可以使用 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=)]](https://img-blog.csdnimg.cn/d48aac206ca140a68d8bca00139f3e1d.png)
我们发现 , 当我们写上 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=)]](https://img-blog.csdnimg.cn/1215fc96ddf74b10a0e72fb9dec1c6c9.png)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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=)]](https://img-blog.csdnimg.cn/19c60a5bbdbf40f2b43f8000cb050cf1.png)
就自动把异常捕获了 , 但是 run 函数这里面只能使用 try catch语句 , 不能使用其他的捕获异常的操作 .
因为 run 函数是重写的方法 , 原函数就没有提供其他的捕获异常的方法

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

class MyThread extends Thread {@Overridepublic 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=)]](https://img-blog.csdnimg.cn/00a32ab3f0944b739c4b263b56b786a6.png)
那么这里面就会有一个常见面试题 :
谈一谈 Thread 的 run 和 start 的区别
使用 start , 可以看到两个线程并发执行 , 两组打印交替出现
使用 run , 只打印 Hello MyThrad , 没有打印 Hello main
直接调用 run , 并没有创建出新的线程 , 只是在之前的线程中(在这里面也就是main线程) , 执行了 run 里面的内容 , 所以只打印了 Hello MyThread
使用 start , 则是创建新的线程 , 新的线程里面调用 run , 新线程和旧线程之间是并发执行的关系
1.3.2 实现Runnable接口
class MyRunnable implements Runnable {@Overridepublic 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=)]](https://img-blog.csdnimg.cn/082bc6ceeb04435fb5497d1295244ecd.png)
//线程本身: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() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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=)]](https://img-blog.csdnimg.cn/dd527a26030d4ba193c4409c728867b1.png)
我们明显可以看到:单线程执行的效率是不太高的,基本运行了大约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=)]](https://img-blog.csdnimg.cn/3462e0f503314a999bb5464168b798c1.png)
多线程确实嗷嗷快。
但是有一个问题:
这个代码涉及到三个进程 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=)]](https://img-blog.csdnimg.cn/95a0d6fff55c475080d6902d5f0abadd.png)
我们可以把单线程的执行方式和多线程的执行方式放在一起比较一下
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=)]](https://img-blog.csdnimg.cn/db48d53ec8ab4ea1baf3dc90221bd9ff.png)
那么我们之前没加 join 的时候 , 多线程不是很快嘛 , 这怎么加上 join 之后咋还慢了?
加上 join 之后才是裁判等到选手到终点,才停止计时。
如果没有 join 的限制,main、t1、t2都是同时向下走的,走的过程中,调度顺序是不确定的。
最极端的情况就是一直在执行 main 线程 , t1 t2 两个线程都没有被执行 , 这样的结果肯定是不对的
有可能先执行 main ,再执行 t1 ,再执行 t2
有可能先执行 t1,再执行 t2,再去执行 main,再去执行 t1…
是有很多种可能性的。
那么我们继续来看 , 单线程消耗时间的时间是1095ms,多线程消耗的时间是581ms,那么为什么不是单线程执行时间的一半呢(即547.5s)呢?
- 创建线程,也是有开销的
- 两个线程在CPU上不一定就是纯并发执行,有可能一部分时间并行执行,一部分时间并发执行
- 线程的调度也是有开销的。
1.5 多线程的使用场景
- 在CPU密集型区域
代码中的大部分工作,都是在使用CPU进行计算,使用多线程,就可以充分利用CPU多核资源,可以提高效率
- 在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 交往)
到此 , 本篇文章就结束了 , 敬请期待后续!

相关文章:
Java Web 实战 02 - 多线程基础篇(1)
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 实现 Runnab…...
C/C++开发,无可避免的多线程(篇三).协程及其支持库
一、c20的协程概念 在c20标准后,在一些函数中看到co_await、co_yield、co_return这些关键词,这是c20为协程实现设计的运算符。 协程是能暂停执行以在之后恢复的函数。原来我们调用一个功能函数时,只要调用了以后,就要完整执行完该…...
高级信息系统项目管理(高项 软考)原创论文项目背景合集
以下为原创的高项论文项目背景合集5篇,建议自己以此为基础,再多多打磨完善一下,避免雷同,同时使项目背景更加真实可信。 一、某市智慧工地系统建设项目 某市住建局智慧工地系统建设项目是在该市住建局促进建筑行业转型升级和科技创新,强化工程质量安全,推动建筑业高质量…...
锁屏面试题百日百刷-Hive篇(十一)
锁屏面试题百日百刷,每个工作日坚持更新面试题。锁屏面试题app、小程序现已上线,官网地址:https://www.demosoftware.cn。已收录了每日更新的面试题的所有内容,还包含特色的解锁屏幕复习面试题、每日编程题目邮件推送等功能。让你…...
一看就懂,等保2.0工作流程这么做
等保2.0相关国家标准于2019年12月1日开始实施,标志着我国网络安全等级保护工作进入一个崭新的阶段,对于加强我国网络安全保障工作,提升网络安全保护能力具有十分重要的意义。很多行业主管单位要求行业客户开展等级保护工作,合理地…...
Kerberos 域委派攻击之非约束性委派
CSDN文章自动迁移自博客在Windows 2000 Server 首次发布 Active Directory 时,Microsoft 必须提供一种简单的机制来支持用户通过 Kerberos 向 Web Server 进行身份验证并需要代表该用户更新后端数据库服务器上的记录的方案。这通常称为“Kerberos 双跳问题”&#x…...
【容器运行时】一文理解 OCI、runc、containerd、docker、shim进程、cri、kubelet 之间的关系
参考 docker,containerd,runc,docker-shim 之间的关系Containerd shim 进程 PPID 之谜内核大神教你从 Linux 进程的角度看 DockerRunC 简介OCI和runCContainerd 简介从 docker 到 runCDockershim究竟是什么技术干货|Docker和 Con…...
spark兼容性验证
前言 Apache Spark是专门为大规模数据处理而设计的快速通用的计算引擎,Spark拥有Hadoop MapReduce所具有的优点,但不同于Mapreduce的是Job中间输出结果可以保存在内存中,从而不再需要读写HDFS,因此Spark能更好的适用于数据挖掘与…...
docker逃逸复现--pid=host模式下的逃逸
漏洞原理当docker以--pidhost模式启动时,你可以通过在容器进程中注入一些shellcode进行逃逸。相当于给了docker Linux中的CAP_SYS_PTRACE权限--pidhost:意味着宿主机与容器公享一套pid,如此做容器就可以访问并跟踪宿主机的进程Linux中的CAP_S…...
【环境配置】Windows系统下搭建Pytorch框架
【环境配置】Windows系统下搭建Pytorch框架 在Windows Serve 2019系统下搭建Pytorch框架 目录 【环境配置】Windows系统下搭建Pytorch框架1.用驱动总裁安装显卡驱动2.在cmd运行nvidia-smi3.安装cuda4.安装cudnn5.安装pytorch的命令1.首次安装2.操作失误需要重新安装6.安装torc…...
Dockerfile简单使用入门
什么是 Dockerfile? Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。 docker build命令用于从Dockerfile构建映像。可以在docker build命令中使用-f标志指向文件系统中任何位置的Dockerfile。 例如࿱…...
什么是CCC认证3C强制认证机构
什么是CCC认证3C强制认证机构? 3C认证的全称为“强迫性产物认证轨制”,它是中国政府为掩护消费者人身平安和国度平安、增强产物品质治理、按照法律法规履行的一种产物及格评定轨制。所谓3C认证,便是中国强迫性产物认证轨制,英文名…...
C语言-基础了解-18-C共用体
C共用体 一、共用体 共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式 二、定义共同体 为了定义共用体&…...
Vue基础18之github案例、vue-resource
Vue基础18github案例静态页面第三方样式引入(以bootstrap举例)App.vueSearch.vueList.vue列表展示接口地址使用全局事件总线进行兄弟间组件通信Search.vueList.vue完善案例List.vueSearch.vue补充知识点:{...this.info,...this.dataObj}效果呈…...
UE4 c++ Mediaplayer取消自动播放,运行时首帧为黑屏的问题
0,前言 工作需要使用C制作一个ue4的视频插件,其中一个功能是能够选择 运行时是否自动播放 视频的功能。 在实现时遇见了一个问题,取消自动播放之后,运行时首帧是没有取到的,在场景里面看是黑色的。就这个问题我想到了使…...
C语言-基础了解-17-C结构体
C结构体一、c结构体C 数组允许定义可存储相同类型数据项的变量,结构是 C 编程中另一种用户自定义的可用的数据类型,它允许您存储不同类型的数据项。结构体中的数据成员可以是基本数据类型(如 int、float、char 等),也可…...
Python爬虫实践:优志愿 院校列表
https://www.youzy.cn/tzy/search/colleges/collegeList获取目标网址等信息打开开发人员工具(F12),拿到调用接口的地址,以及接口请求参数等信息,如下curl https://uwf7de983aad7a717eb.youzy.cn/youzy.dms.basiclib.ap…...
Java框架学习 | MySQL和Maven笔记
1.MySQL提问式思考 为什么要有数据库?MySQL的优劣势?Java的优劣势? JavaMySQL开源具有大量的社区成员和丰富的资源免费/具有大量的社区成员和丰富的资源可扩展性多态、继承和接口等分区、复制和集群等方式扩展数据库的容量和性能安全性有许…...
C++入门教程||C++ 变量作用域||C++ 常量
C 变量作用域 作用域是程序的一个区域,一般来说有三个地方可以声明变量: 在函数或一个代码块内部声明的变量,称为局部变量。在函数参数的定义中声明的变量,称为形式参数。在所有函数外部声明的变量,称为全局变量。 我…...
想找工作,这一篇15w字数+的文章帮你解决
文章目录前言一 专业技能1. 熟悉GoLang语言1.1 Slice1.2 Map1.3 Channel1.4 Goroutine1.5 GMP调度1.6 垃圾回收机制1.7 其他知识点2. 掌握Web框架Gin和微服务框架Micro2.1 Gin框架2.2 Micro框架2.3 Viper2.4 Swagger2.5 Zap2.6 JWT3. 熟悉使用 MySQL 数据库3.1 索引3.2 事务3.3…...
利用ngx_stream_return_module构建简易 TCP/UDP 响应网关
一、模块概述 ngx_stream_return_module 提供了一个极简的指令: return <value>;在收到客户端连接后,立即将 <value> 写回并关闭连接。<value> 支持内嵌文本和内置变量(如 $time_iso8601、$remote_addr 等)&a…...
树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法
树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作,无需更改相机配置。但是,一…...
Opencv中的addweighted函数
一.addweighted函数作用 addweighted()是OpenCV库中用于图像处理的函数,主要功能是将两个输入图像(尺寸和类型相同)按照指定的权重进行加权叠加(图像融合),并添加一个标量值&#x…...
IP如何挑?2025年海外专线IP如何购买?
你花了时间和预算买了IP,结果IP质量不佳,项目效率低下不说,还可能带来莫名的网络问题,是不是太闹心了?尤其是在面对海外专线IP时,到底怎么才能买到适合自己的呢?所以,挑IP绝对是个技…...
ubuntu22.04有线网络无法连接,图标也没了
今天突然无法有线网络无法连接任何设备,并且图标都没了 错误案例 往上一顿搜索,试了很多博客都不行,比如 Ubuntu22.04右上角网络图标消失 最后解决的办法 下载网卡驱动,重新安装 操作步骤 查看自己网卡的型号 lspci | gre…...
6.9-QT模拟计算器
源码: 头文件: widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QMouseEvent>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic:Widget(QWidget *parent nullptr);…...
Ray框架:分布式AI训练与调参实践
Ray框架:分布式AI训练与调参实践 系统化学习人工智能网站(收藏):https://www.captainbed.cn/flu 文章目录 Ray框架:分布式AI训练与调参实践摘要引言框架架构解析1. 核心组件设计2. 关键技术实现2.1 动态资源调度2.2 …...
使用homeassistant 插件将tasmota 接入到米家
我写一个一个 将本地tasmoat的的设备同通过ha集成到小爱同学的功能,利用了巴法接入小爱的功能,将本地mqtt转发给巴法以实现小爱控制的功能,前提条件。1需要tasmota 设备, 2.在本地搭建了mqtt服务可, 3.搭建了ha 4.在h…...
多模态学习路线(2)——DL基础系列
目录 前言 一、归一化 1. Layer Normalization (LN) 2. Batch Normalization (BN) 3. Instance Normalization (IN) 4. Group Normalization (GN) 5. Root Mean Square Normalization(RMSNorm) 二、激活函数 1. Sigmoid激活函数(二分类&…...
Java求职者面试:微服务技术与源码原理深度解析
Java求职者面试:微服务技术与源码原理深度解析 第一轮:基础概念问题 1. 请解释什么是微服务架构,并说明其优势和挑战。 微服务架构是一种将单体应用拆分为多个小型、独立的服务的软件开发方法。每个服务都运行在自己的进程中,并…...


![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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)]](https://img-blog.csdnimg.cn/9e47c5b3d52a41d4affc2e6f4b29845f.png)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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)]](https://img-blog.csdnimg.cn/05b5524becc4432098580401f55b60c5.png)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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=)]](https://img-blog.csdnimg.cn/a90cf498a13941e5b49028c8f6a62fdb.png)