06.JAVAEE之线程4

embedded/2024/9/23 6:41:00/

1.定时器

1.1 定时器是什么

定时器也是软件开发中的一个重要组件.
类似于一个 " 闹钟 ". 达到一个设定的时间之后 , 就执行某个指定好的代码.
约定一个时间,时间到达之后,执行某个代码逻辑,
定时器非常常见,尤其是在进行网络通信的时候,

 需要有等待的最大时间,等待的最大时间通过定时器实现。

在标准库里,也是有现成的定时器的实现的 

主线程执行 schedule 方法的时候,就是把这个任务给放到 timer 对象中了,
于此同时,timer 里头也包含一个线程,这个线程叫做"扫描线程”,一旦时间到,扫描线程就会执行刚才安排的任务了。
仔细观察,可以发现,整个进程其实没有结束!! 就是因为 Timer 内部的线程,阻止了进程结束.
Timer 里,是可以安排多个任务的
java">import java.util.Timer;
import java.util.TimerTask;// 定时器
public class Demo25 {public static void main(String[] args) {Timer timer = new Timer();// 给定时器安排了一个任务, 预定在 xxx 时间去执行.timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("3000");}}, 3000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("2000");}}, 2000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("1000");}}, 1000);System.out.println("程序启动!");}
}

 1.2 如何实现定时器

Timer timer = new Timer();

1.Timer 中需要有一个线程,扫描任务是否到时间,可以执行了

2.需要有一个数据结构,把所有的任务都保存起来.

【具体使用什么数据结构好呢?

假设使用数组(ArrayList),此时,扫描线程, 就需要不停的遍历数组中的每个任务判定每个任务是否都到达执行时间.

使用优先级队列,是更好的办法!!给 Timer 中添加的这些任务, 都是带有一个"时间’定是时间小的先执行。最先执行的就是时间最小的任务!!如果时间最小的任务,还没到时间呢,其他任务更不会到时间了!!优先级队列,可以使用 O(1)时间,来获取到时间最小的任务的!!

3.还需要创建,个类,通过类的对象来描述一个任务.(至少要包含任务内容和时间)

使用绝对的时间戳更为方便

对于优先级队列,要求里面的元素是可比较的,所以需要重写比较方法。

  • 如果发现队列为空,应该咋办呢?

好的办法,就是阻塞等待,等到队列不空为止 =>阻塞队列不就是这样的嘛~~
wait 要想使用, 需要搭配 synchronized,不能单独使用!!wait 进行的操作有三个:
1)释放锁 =>前提是, 先拿到锁,
2)等待通知
3) 通知到来之后, 唤醒,重新获取锁
这个方法,是一个线程中(比如主线程中),给队列添加元素

出现忙等时

忙等的过程,确实在等,但是也消耗了很多 cpu

1个notify起到两个xiaog

之所以咱们的代码,使用的是 PriorityQueue,而不是 PriorityBlockingQueue,其实就是因为要处理两个 wait 的地方使用阻塞版本的优先级队列,不方便实现这样的两处等待~~

java">mport java.util.PriorityQueue;// 通过这个类, 描述了一个任务
class MyTimerTask implements Comparable<MyTimerTask> {// 要有一个要执行的任务private Runnable runnable;// 还要有一个执行任务的时间private long time;// 此处的 delay 就是 schedule 方法传入的 "相对时间"public MyTimerTask(Runnable runnable, long delay) {this.runnable = runnable;this.time = System.currentTimeMillis() + delay;}@Overridepublic int compareTo(MyTimerTask o) {// 这样的写法, 就是让队首元素是最小时间的值// 到底是谁 - 谁, 不要背!! 你可以试试!!return (int) (this.time - o.time);// 如果是想让队首元素是最大时间的值// return o.time - this.time;}public long getTime() {return time;}public Runnable getRunnable() {return runnable;}
}// 咱们自己搞的定时器
class MyTimer {// 使用一个数据结构, 保存所有要安排的任务.private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();// 使用这个对象作为锁对象.private Object locker = new Object();public void schedule(Runnable runnable, long delay) {synchronized (locker) {queue.offer(new MyTimerTask(runnable, delay));locker.notify();}}// 搞个扫描线程.public MyTimer() {// 创建一个扫描线程Thread t = new Thread(() -> {// 扫描线程, 需要不停的扫描队首元素, 看是否是到达时间.while (true) {try {synchronized (locker) {// 不要使用 if 作为 wait 的判定条件, 应该使用 while// 使用 while 的目的是为了在 wait 被唤醒的时候, 再次确认一下条件.while (queue.isEmpty()) {// 使用 wait 进行等待.// 这里的 wait, 需要由另外的线程唤醒.// 添加了新的任务, 就应该唤醒.locker.wait();}MyTimerTask task = queue.peek();// 比较一下看当前的队首元素是否可以执行了.long curTime = System.currentTimeMillis();if (curTime >= task.getTime()) {// 当前时间已经达到了任务时间, 就可以执行任务了task.getRunnable().run();// 任务执行完了, 就可以从队列中删除了.queue.poll();} else {// 当前时间还没到任务时间, 暂时不执行任务.// 暂时先啥都不干, 等待下一轮的循环判定了.locker.wait(task.getTime() - curTime);}}} catch (InterruptedException e) {e.printStackTrace();}}});t.start();}
}public class Demo26 {public static void main(String[] args) {MyTimer timer = new MyTimer();timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("3000");}}, 3000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("2000");}}, 2000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("1000");}}, 1000);System.out.println("程序开始执行");}
}

 2.线程池

线程诞生的意义,是因为进程的创建/销毁, 太重量了(比较慢)有对比,才有伤害,和进程比,线程,是更快了,但是如果进一步提高创建销毁的频率, 线程的开销也不能忽视了!

两种典型的办法,进一步提高效率:

1.协程(轻量级线程)

相比于线程,把系统调度的过程,给省略了.(程序猿手工调度当下,一种比较流行的并发编程的手段. 但是在 Java 圈子里,协程还不够流行.

2.线程池

线程池最大的好处就是减少每次启动、销毁线程的损耗。
在使用第一个线程的时候,提前把 2345..线程创建好 (培养感情)
后续如果想使用新的线程,不必重新创建了,直接拿过来就能用!!!(此时创建线程的开销就被降低了)

2.1 线程池的使用 

把线程创建好,放在池子里,后续用的时候直接从池子里来取~~

为什么从池子取, 的效率比新创建线程,效率更高??? 

从池子取,这个动作,是纯粹用户态的操作.

创建新的线程,这个动作,则是需要 用户态 +内核态 相互配合,完成的操作

 出现的问题

很多时候 构造一个对象,希望有多种构造方式.
多种方式,就需要使用多个版本的构造方法来分别实现.
但是构造方法要求方法的名字必须是类名,不同的构造方法,就只能通过 重载 的方式来区分了.(重载 =>参数类型/个数 不同)

上面两个代码并没有构成重载,故编译失败。

工厂设计模式

解决方案:使用工厂设计模式

使用工厂设计模式,就能解决这个问题.
使用普通的方法,代替构造方法完成初始化工作.普通方法就可以使用方法的名字来区分了.也就不再收到重载的规则制约了
实践中, 一般单独搞一个类,给这个类搞一些静态方法, 由这样的静态方法负责构造出对象。

使用不同的方法名做出区分

java">import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Demo27 {public static void main(String[] args) {ExecutorService service = Executors.newFixedThreadPool(4);service.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello");}});}
}

Executors 创建线程池的几种方式

  • newFixedThreadPool: 创建固定线程数的线程池
  • newCachedThreadPool: 创建线程数目动态增长的线程池.
  • newSingleThreadExecutor: 创建只包含单个线程的线程池. 
  • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer. 

上述这几个工厂方法生成的线程池,本质上都是对一个 类 进行的封装,ThreadPoolExecutor这个类,功能非常丰富,提供了很多参数,标准库上述的几个工厂方法,其实就是给这个类填写了不同的参数用来构造线程池了【Executors 本质上是 ThreadPoolExecutor 类的封装】

  • 不同的拒绝策略有不同的效果

使用线程池,需要设置线程的数目.数目设置多少合适??

java">ExecutorService service = Executors.newFixedThreadPool(4);

在接触到实际代码之前是无法确定的。

一个线程,执行的代码,主要有两类:
1.cpu 密集型: 代码里主要的逻辑是在进行 算术运算/逻辑判断

2.IO密集型: 代码里主要进行的是 Io操作,
假设一个线程的所有代码都是 cpu 密集型代码,这个时候,线程池的数量不应该超过 N(设置 N 就是极限了)设置的比 N 更大,这个时候,也无法提高效率了.(cpu 吃满了)此时更多的线程反而增加调度的开销.
假设一个线程的所有代码都是 Io密集的,这个时候不吃 CPU,此时设置的线程数,就可以是超过 N.较大的值一个核心可以通过调度的方式,来并发执行~

我们就可以知道:

代码不同, 线程池的线程数目设置就不同,无法知道一个代码,具体多少内容是 cpu 密集, 多少内容是Io密集

正确做法: 使用实验的方式,对程序进行性能测试,测试过程中尝试修改不同的线程池的线程数目,看哪种情况下,最符合要求

 2.2 线程池的实现

java">import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;class MyThreadPool {// 任务队列private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);// 通过这个方法, 把任务添加到队列中public void submit(Runnable runnable) throws InterruptedException {// 此处咱们的拒绝策略, 相当于是第五种策略了. 阻塞等待~~ (这是下策)queue.put(runnable);}public MyThreadPool(int n) {// 创建出 n 个线程, 负责执行上述队列中的任务.for (int i = 0; i < n; i++) {Thread t = new Thread(() -> {// 让这个线程, 从队列中消费任务, 并进行执行.try {Runnable runnable = queue.take();runnable.run();} catch (InterruptedException e) {e.printStackTrace();}});t.start();}}
}public class Demo28 {public static void main(String[] args) throws InterruptedException {MyThreadPool myThreadPool = new MyThreadPool(4);for (int i = 0; i < 1000; i++) {int id = i;myThreadPool.submit(new Runnable() {@Overridepublic void run() {System.out.println("执行任务: " + id);}});}}
}

主线:

线程概念 -> Thread 用法 ->线程安全问题 ->wait notify -> 线程案例


http://www.ppmy.cn/embedded/17964.html

相关文章

【C++杂货铺】多态

目录 &#x1f308;前言&#x1f308; &#x1f4c1;多态的概念 &#x1f4c1; 多态的定义及实现 &#x1f4c2; 多态的构成条件 &#x1f4c2; 虚函数 &#x1f4c2; 虚函数重写 &#x1f4c2; C11 override 和 final &#x1f4c2; 重载&#xff0c;覆盖&#xff08;重写…

【Python爬虫】CSS选择器选择多个属性

1.selenium.webdriver和BeautifulSoup 对于单个元素的属性&#xff0c;为每个属性加中括号[] browser Chorme() browser.find_element(byBy.CSS_SELECTOR, value"ul[idp][classl]") 若有从属关系&#xff0c;如下 <select id"dropdown" name"d…

UNIXUNIX

格林尼治是一个地名&#xff0c;位于英国伦敦&#xff0c;也就是伦敦的标准时间。在格林尼治有个天文台&#xff0c;可以观察天上太阳和星星以确定太阳的自转和公转&#xff0c;将地球自转一周的时间间隔分为24小时。 GMT是以前的计时时间&#xff0c;因为存在一个棘手的问题&a…

Reactjs常用组件

1 react 1.1 useState 让函数组件具有维持状态的能力 const[count, setCount]useState(0); 1.2 useEffect 执行副作用&#xff0c;useEffect 的第二个参数告诉 React 用到了哪些外部变量 类似于Vue watch的作用 useEffect(fn, deps); 1.每次 render 后执行&#xff1a;不…

MYSQL45道练习题---持续更新中

来源&#xff1a; Mysql_45道练习题 - 简书 共四张表&#xff1a; ①、course表&#xff1a; CId&#xff1a;课程id Cname&#xff1a;课程名称 TId&#xff1a;老师id ②、student学生表&#xff1a; SId&#xff1a;学生id Sname&#xff1a;…

python爬虫学习------scrapy第二部分(第三十天)

&#x1f388;&#x1f388;作者主页&#xff1a; 喔的嘛呀&#x1f388;&#x1f388; &#x1f388;&#x1f388;所属专栏&#xff1a;python爬虫学习&#x1f388;&#x1f388; ✨✨谢谢大家捧场&#xff0c;祝屏幕前的小伙伴们每天都有好运相伴左右&#xff0c;一定要天天…

深入浅出 SQL 优化:全面提升查询性能的技巧

文章目录 前言一、表结构分析1. 索引分析2. 数据类型分析3. 思考反范式设计的适用场景与潜在风险3.1数据冗余3.2 数据一致性3.3 更新性能 4. 关注临时表的创建与使用。4.1.尽量减少临时表的使用&#xff0c;以降低系统资源的消耗。4.2 使用合适的索引和数据类型优化临时表的性能…

VMware17Pro虚拟机安装macOS教程(超详细)

目录 1. 前言2. 下载所需文件3. 安装VMware3.1 安装3.2 启动并查看版本信息3.3 虚拟机默认位置配置 4. 安装补丁4.1 解压补丁4.2 结束VMware相关进程4.3 运行补丁包 5. 安装macOS5.1 新建虚拟机5.2 修改虚拟机配置5.3 安装操作系统5.3.1 选择 ISO 映像文件5.3.2 开启虚拟机5.2.…