JavaEE & 线程案例 & 定时器 & 线程池 and 工厂模式,
JavaEE & 线程案例 & 定时器 & 线程池 and 工厂模式,
- 欢迎光临 ^ V ^
文章目录
- JavaEE & 线程案例 & 定时器 & 线程池 and 工厂模式
- 1. 定时器
- 1.1 定时器Timer的使用
- 1.1.1 核心方法schedule
- 1.1.2 定时器管理多个线程
- 1.1.3 定时器的使用场景
- 1.2 自己实现一个定时器
- 1.2.1 属性
- 1.2.2 建立一个MyTask对象
- 1.2.3 schedule方法
- 1.2.4 构造方法初步设`计`
- 1.2.5 构造方法最终设计
- 1.3 测试MyTimer
- 1.4 补充
- 1.4.1 例子1
- 1.4.2 例子2
- 1.5 顺带一题
- 1.5.1 后者
- 1.5.2 前者
- 2. 线程池
- 2.1 用户态和内核态
- 2.2 标准库线程池类ExecutorService
- 2.3 工厂模式
- 2.3.1 开[A的构造厂]
- 2.3.2 开[A的比较器厂]
- 2.3.3 测试
- 2.4 ExecutorService的属性和方法
- 2.4.1 通过工厂类构造
- 2.4.2 submit方法
- 2.4.3 ThreadPoolExecutor类的属性
- 2.4.4 线程池的拒绝策略
- 2.5 模拟实现线程池
- 2.6 线程池的固定线程数的确定(理论)
JavaEE & 线程案例 & 定时器 & 线程池 and 工厂模式
1. 定时器
- 定时器,可以理解为闹钟
- 我们设立一个时间,时间一到,让一个线程跑起来~
- 而Java标准库提供了一个定时器类:
- Timer ,from java.util
1.1 定时器Timer的使用
1.1.1 核心方法schedule
- 传入任务引用(TimerTask task)和 “定时”(long delay / ms)
- 由于TimerTask不是函数式接口,是普通的抽象类
- 所以只能用匿名内部类,而不能用lambda表达式
- 写法
public static void main(String[] args) { Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("好耶 ^ v ^"); } } },1000); System.out.println("不好耶 T . T"); }
-
TimerTask实现了Runnable
- 不能传Runnable对象过去,这属于向下转型~
-
- 是Runnable的一个“封装”
- 所以,重写run方法,合情合理~
- 只不过不能用
-
而在Timer的schedule方法内部,则将这个线程保存起来,定时后执行~
- 而这,有一个细节,就是执行完后,程序并没有结束,进程并没退出
原因是:
- Timer内置了一个前台线程
- 阻止进程退出~
- 这并不是重点,其实就是timer在等待被安排下一个任务~
1.1.2 定时器管理多个线程
public class Test { public static void main(String[] args) { Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("星期一好耶 ^ v ^"); } },1000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("星期二好耶 ^ v ^"); } },2000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("星期三好耶 ^ v ^"); } },3000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("星期四好耶 ^ v ^"); } },4000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("星期五好耶 ^ v ^"); } },5000); System.out.println("今天不好耶 T . T"); } }
- 那么就安排多个任务呗~
1.1.3 定时器的使用场景
-
应用场景特别多
- 尤其是网络编程
-
而这个任务等待,不应该是无期限的
- 超时:504 【gateway timeout】
-
定时器可以强制终止请求:浏览器内部都有一个定时器,发送请求后,定时器就开始定时;若在规定时间内,响应数据没有返回,就会强制终止请求
-
这个方法一般在任务的run方法中调用,确定是否及时
- 这种特殊语法不是我们能理解的,并且目前我们不需要用到这个用法~
1.2 自己实现一个定时器
- 想法一,根据任务们的时间
- 在添入的时候,就让他们启动并以对应的时间"睡下"
- 有点像睡眠排序法这个消遣的笑话~
- 显然这个方法是不科学的,线程到达一个量级,进程必然装不下
- 系统必然卡死崩掉
- 在添入的时候,就让他们启动并以对应的时间"睡下"
- 想法二,根据时间,到了时间自动启动~
- 将任务们按照时间长短排序
- 每次只看最早启动的任务就好
- 当然,等待时间是同步的~
- 每个任务都有在等
- 启动,再去看接下来的任务~
- 如果两个任务同时启动,顺序则不能确定~
是不是触动你的DNA了?
- 没错,搞一个堆就好了
- 每次可见堆顶元素~
- 而小根堆堆顶正是我们这里的最早启动的任务~
- 旧堆顶取走后,新堆顶又是剩余的最早启动的任务~
- 而定时器的核心数据结构就是:优先级队列 ===> 堆
- 而定时器可能被多线程使用,所以线程安全问题也要被保证
- 队列为空,队列为“满”的时候,对操作也要有限制(不应该有无限个任务)
- 这就需要我们的阻塞队列~
即,定时器底层就是一个阻塞优先级队列! ===> PriorityBlockingQueue
- 对于PriorityBlockingQueue,我这里并不会去模拟~
1.2.1 属性
class MyTask { public Runnable runnable; public long time; } public class MyTimer { private PriorityBlockingQueue<MyTask> tasks = new PriorityBlockingQueue<>(); }
阻塞优先级队列中的元素应该有如下两个信息:
- MyTask
1.2.2 建立一个MyTask对象
- 获取当前时间方法:
System.currentTimeMillis()
class MyTask { public Runnable runnable; public long time; //绝对时间戳~ //方便判断~ //这个不是定时时间 public MyTask(Runnable runnable, long delay) { this.runnable = runnable; this.time = delay + System.currentTimeMillis(); } }
1.2.3 schedule方法
public void schedule(Runnable runnable, long delay) { MyTask myTask = new MyTask(runnable, delay); tasks.put(myTask); }
- 构造一个myTask对象插入到队列中~
1.2.4 构造方法初步设计
public MyTimer() { Thread t = new Thread(() -> { try { MyTask myTask = tasks.take(); long nowTime = System.currentTimeMillis(); if(myTask.time <= nowTime) { //启动 }else { //不能启动 } } catch (InterruptedException e) { e.printStackTrace(); } }); } }
- 定时器被构造出来后,应该就已经启动“母线程”
- 就应该尝试【take】了
- 只不过队列为空,要阻塞等待~
- 之后通过schedule安排任务~【put】
- 启动:
- 调用run方法
- 不能启动:
- 将任务返回队列
1.2.5 构造方法最终设计
- 在构造方法初步设计有两个很严重的BUG
- 可以停止观看去想一想~
对于1. 比较规则:
-
只需要让MyTask实现比较接口
-
当然也可以传比较器~(lambda表达式)
-
两种方式都OK~
-
左减右大于0
-
如果代表此对象大于该对象代表升序排列 ===> 小根堆
-
如果代表此对象小于该对象代表降序排列 ===> 大根堆
-
对于2. “没有等待”以及“盲目等待”
- 上述代码只会判断一次~
- 应该套上一个循环~
-
wait等待,唤醒起来比较方便安全
- sleep不是一个很好的选择~
- 因为新任务的插入,要进行唤醒
- 超过限定时间,自动醒来
- wait需要有锁,这里我把循环体整个框起来了
- 我用的是“同步锁”
-
“盲目等待” 代表,这里放回去后,计算器又会判断是否可启动
- 这样就会导致一段时间内,这个任务反复被拿来拿去无数次~
- 相当于,上课时看表,一秒看一次,忙等
- 而计算机,1ms就可以看很多很多次~
-
那么我们只需要在schedule时唤醒一下,让他才判断一次就行了~
- 这防止新插入的任务更早而被忽略
-
大大减少判断次数!
- 最终版:
public void schedule(Runnable runnable, long delay) { MyTask myTask = new MyTask(runnable, delay); tasks.put(myTask); synchronized (locker) { locker.notify(); } } private Object locker = new Object(); public MyTimer() { Thread t = new Thread(() -> { while(true) { synchronized (locker) { try { MyTask myTask = tasks.take(); long nowTime = System.currentTimeMillis(); if(myTask.time <= nowTime) { myTask.runnable.run(); }else { tasks.put(myTask); locker.wait(myTask.time - nowTime); } } catch (InterruptedException e) { e.printStackTrace(); } } } }); t.start(); }
- 别忘了启动线程~
1.3 测试MyTimer
-
用MyTimer替换之前的Timer
-
TimeTask也可替换为Runnable,不过没关系,向上转型~
public static void main(String[] args) { MyTimer timer = new MyTimer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("星期一好耶 ^ v ^"); } },1000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("星期二好耶 ^ v ^"); } },2000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("星期三好耶 ^ v ^"); } },3000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("星期四好耶 ^ v ^"); } },4000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("星期五好耶 ^ v ^"); } },5000); System.out.println("今天不好耶 T . T"); }
- 测试结果正常:
- 退出代码130,是按ctrl + f2
1.4 补充
- 你可能也发现了,代码之中并没有完全保证,一个线程一定会在规定的时间后执行
- 因为一个定时器,只能运行一个线程,没有并发性
- 只是和main线程并发~
- 所以,如果一个线程运行时间较长,会导致其后的任务“被迫延时”
- 而判断条件不是等于等于,也有这一方面原因
- 另一方面原因是,可能因为调度问题有误差~
- 此时这个定时器,就只能起到,保证任务执行顺序的功能~
1.4.1 例子1
- 例如以下测试代码:
public static void main(String[] args) { MyTimer timer = new MyTimer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("星期一好耶 ^ v ^"); try { Thread.sleep(5000); System.out.println("已过去五秒"); } catch (InterruptedException e) { e.printStackTrace(); } } },1000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("星期二好耶 ^ v ^"); } },2000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("星期三好耶 ^ v ^"); } },3000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("星期四好耶 ^ v ^"); } },4000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("星期五好耶 ^ v ^"); } },5000); System.out.println("今天不好耶 T . T"); } }
- 第一个任务要花5秒,而还差1秒,第二个任务就应该启动~
- 而现象是这样的:
- 后面的任务已经受严重延迟~
1.4.2 例子2
- 如果一个任务死循环了,会导致后面的任务无限延期
- 就会导致下面这种情况:
注意:
- 这并不是我写的定时器有问题 ,Java标准库的定时器,就是这样子的, 一个定时器一个时间段里只能执行一个任务
- 现象跟MyTimer是一样的
- 就是这两个例子那样
- 一个任务时间太长,会导致下一个任务延迟
- 只起“区分先后”的作用
1.5 顺带一题
问:wait的同步锁的位置不同,结果会怎么样?
- 例如:
- 这两种锁的框法不同,结果一样吗?
1.5.1 后者
- 重点就在于,没有保证take与wait是原子的~
1.5.2 前者
- 保证原子性后:
2. 线程池
- 跟字符串常量池和数据库连接池一样
- 这个池的作用就提高效率,节省开销~
- 即使线程很轻量,但是积少成多就不能忽略~
- 只要再池子里去拿,就要比从系统申请要快~
提高效率还能提高轻量化线程“协程”,Java标准库还不支持
而线程池是一个重要的途径~
- 从线程池里拿线程,纯纯的用户态操作
- 而从系统上申请,就必须设计用户态和内核态之间的切换
- 真正的创建线程,是在内核态完成的
2.1 用户态和内核态
- 操作系统 = 内核 + 配套的应用程序
- 内核:各种系统管理和驱动,而内核就是为了支持应用程序的
- 这里不仅仅指核心~
- 因为进程管理这是他的工作之一
- 逻辑核心们也只是他的打工人~
-
需要内核支持,才能运行的应用程序~
- 例如,println,打印到屏幕,需要通过硬件管理~
-
即,内核给那么多人服务,那么就不一定及时
举个栗子:
- 去银行打印资料,前台可以帮你打印
- 而前台在同时会去帮助其他人,给你打印好了还要好一会儿才给你~
- 你也可以去自助打印机打印
- 这样的时间消耗就只会缩短在 “打印需求” 内去消耗
-
也就是说,我们在申请线程时
-
内核态申请 ==> 内核要顾及进程管理和其他管理与驱动~
-
用户态去拿 ==> 只需要在进程管理这个单项里去拿线程~
-
-
当然,线程的诞生,还是要内核态申请
- 放进线程池,之后在线程池里用户态拿就好~
2.2 标准库线程池类ExecutorService
-
Java标准库实现了一个接口,ExecutorService,在进程中服务线程执行~
- 通过这个池的服务,不需要每次都申请~
-
但是这个接口不是通过new子类对象去实例化的,而是用一个静态方法去实例化~
- 而这里的Executors类就是“工厂类”
- 这个类就是为了构造“线程池”而存在的
- 这个类可以调用各种静态方法
- 而这些静态方法使用起来简单
- 并且可以构造各种满足我们特殊需要的对象
2.3 工厂模式
-
“工厂”
- 即“对象工厂”,可以工厂生产出不同的对象
- 有员工去帮你生产,使用简单
- 降低使用成本
- 相同原料可以有不同产品,避免参数列表相同导致无法触发重载
- 重要作用!
-
而工厂模式其实就是,把一个类/接口的构造方法,交给一个“工厂类”去定义
- 即,将构造方法打包成类
Executors工厂:
- 重点掌握
你也可以自己“开个厂”
- 就比如说,一个【堆】,泛型类是我们的自定义类
- 而我们的自定义类要我们去规定比较方法
public class A { int a1; int a2; int a3; int a4; int a5; int a6; }
- 假设我们A类有六个成员(都是int类型)
- 要求建立6个堆,每个堆以不同的比较规则去创建
- 每次创建都好麻烦,都要写个比较器~
- 要求建立6个堆,每个堆以不同的比较规则去创建
- 只需要“开个比较器厂”,把这些构造方法包装起来就好~
- 以后构造的时候,通过不同的方法名调用对应的构造方法~
- 比较器Comparator
- 构造方法基本都没有参数列表的,那么就不能用重载去解决~
- 比较器的不同主要不是因为构造方法,而是compare被怎么重写有关~
- compare方法重写也只能重写一个
- 构造方法基本都没有参数列表的,那么就不能用重载去解决~
2.3.1 开[A的构造厂]
public static A createA1(int a) { //匿名内部类优先捕获全局性质变量,这里在代码块内,a1就为全局性变量~ return new A() { { this.a1 = a; } }; } public static A createA2(int a) { return new A() { { this.a2 = a; } }; } public static A createA3(int a) { return new A() { { this.a3 = a; } }; } public static A createA4(int a) { return new A() { { this.a4 = a; } }; } public static A createA5(int a) { return new A() { { this.a5 = a; } }; } public static A createA6(int a) { return new A() { { this.a6 = a; } }; } }
2.3.2 开[A的比较器厂]
class CreateComparatorA { public static Comparator<A> createA1() { return ((o1, o2) -> { return o1.a1 - o2.a1; }); } public static Comparator<A> createA2() { return ((o1, o2) -> { return o1.a2 - o2.a2; }); } public static Comparator<A> createA3() { return ((o1, o2) -> { return o1.a3 - o2.a3; }); } public static Comparator<A> createA4() { return ((o1, o2) -> { return o1.a4 - o2.a4; }); } public static Comparator<A> createA5() { return ((o1, o2) -> { return o1.a5 - o2.a5; }); } public static Comparator<A> createA6() { return ((o1, o2) -> { return o1.a6 - o2.a6; }); } }
2.3.3 测试
public class A { int a1; int a2; int a3; int a4; int a5; int a6; //参数列表相同无法特定构造特定成员~ @Override public String toString() { return "A{" + "a1=" + a1 + ", a2=" + a2 + ", a3=" + a3 + ", a4=" + a4 + ", a5=" + a5 + ", a6=" + a6 + '}' + '\n'; } public static void main(String[] args) { PriorityQueue<A> queue1 = new PriorityQueue<>(CreateComparatorA.createA1()); PriorityQueue<A> queue2 = new PriorityQueue<>(CreateComparatorA.createA2()); PriorityQueue<A> queue3 = new PriorityQueue<>(CreateComparatorA.createA3()); PriorityQueue<A> queue4 = new PriorityQueue<>(CreateComparatorA.createA4()); PriorityQueue<A> queue5 = new PriorityQueue<>(CreateComparatorA.createA5()); PriorityQueue<A> queue6 = new PriorityQueue<>(CreateComparatorA.createA6()); queue1.offer(createA.createA1(2)); queue1.offer(createA.createA1(1)); queue1.offer(createA.createA1(4)); queue1.offer(createA.createA1(3)); queue1.offer(createA.createA1(5)); System.out.println(queue1); } }
- 结果:
- 确实以a1为标准~
当然,工厂当然不只可以生产构造方法:
- 还能生产那些我们需要的:重复参数列表的方法
- 例如生产A的toString()方法~
- 不额外说了~
2.4 ExecutorService的属性和方法
2.4.1 通过工厂类构造
- 不给固定容量,按需创建线程池~
- 跟定时器有关~
最重点的一个:
- 提供固定容量的线程池构造方法~
2.4.2 submit方法
- 提交线程~
public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(10); pool.submit(() -> { System.out.println("好耶 ^ v ^ "); }); }
2.4.3 ThreadPoolExecutor类的属性
- 既然是构造ThreadPoolExecutor
- 那么它的属性就至关重要~
下面是Java的官方文档的内容:
- 这个类在【util应用应用工具】的【concurrent并发包】中~
- 简称【JUC】
- 通过这个构造方法让我们看到很多属性~
- 我也只讲解这几个属性~
- 也不讲这个方法咋实现的
- corePoolSize为核心线程数
- maximumPoolSize为最大线程数
- 核心线程,即不能随意停止的线程
- 最大线程数里【包含核心线程和“临时线程”】
- 这个“临时线程”就相当于公司里的实习生
- 关键时期来应急
- 这个核心线程就相当于正式员工
- 不能随意辞退
- 这个“临时线程”就相当于公司里的实习生
线程池会在任务少的空闲期,根据这些参数进行线程调整,把一些临时线程给销毁了~
- keepAliveTime(long) 和 unit(TimeUnit)
-
keepAliveTime 为临时线程存活时间~
- “实习生”并不是立即被辞退
- 而是跟这个参数有关
- 允许最多活多久~
-
unit ==> 时间单位
- BlockingQueue< Runnable > workQueue
- 线程池要管理很多任务
- 通过阻塞队列来组织~
- 方便程序员控制线程数据交互
- submit提交到这个阻塞队列里~
- ThreadFactory threadFactory
- 线程工厂 ,跟工厂模式有关~
- 不细讲
- RejectedExecutionHandler handler
- 线程池的拒绝执行应对策略~
- 池子满了,继续往里添加线程,如何应对?如何拒绝?
- 线程池满了是不依赖阻塞队列的
- 这个任务要不要干最好立马给出决策!
- 一般是空了依赖阻塞队列~
- 还有阻塞队列的线程安全性和解耦合性也很好
- 线程池满了是不依赖阻塞队列的
2.4.4 线程池的拒绝策略
2.5 模拟实现线程池
public class MyThreadPool { private BlockingQueue<Runnable> pool = new LinkedBlockingQueue<>(); public void submit(Runnable runnable) throws InterruptedException { pool.put(runnable); } //实现固定线程数的线程池 //不是容量,是确确实实的线程数 public MyThreadPool(int number) { for (int i = 0; i < number; i++) { Thread thread = new Thread(() -> { Runnable runnable = null; try { while(true) { runnable = pool.take(); runnable.run(); } } catch (InterruptedException e) { e.printStackTrace(); } }); thread.start(); } } }
-
我们的简单实现,不涉及2.4.3的属性~
-
注意:这里的线程数,是工作人数,定量
- 而阻塞队列里的线程数,则是这些人做的”任务“
- 测试:
public static void main(String[] args) throws InterruptedException { MyThreadPool myThreadPool = new MyThreadPool(10); for (int i = 1; i <= 1000; i++) { int id = i; //线程id,变量捕获~ myThreadPool.submit(() -> { System.out.println("好耶^ v ^ " + id); }); }
- 提供固定的工作人员 * 10
- 源源不断塞1000个任务~
- 工作人员疯抢~
——
- 数据顺序无序很正常
- 线程调度无序嘛~
- 程序还未结束
- 这是因为十个工作人员“吸血鬼”还等着任务呢~
线程池中如何体现,“用户态拿”:
注意:
- 线程 不等于 任务
- 任务必须依托线程才能执行
——
2.6 线程池的固定线程数的确定(理论)
-
至于线程池固定线程数,设置为多少合适?
- 最好最科学的方式就是去测试!
-
cpu密集型,主要做一些计算工作,要在cpu上运行~
-
IO密集型,主要等待一些IO操作(读写硬盘/读写网卡),不怎么吃cpu
- 如果你的线程全是使用cpu的,那就得设置线程数少于核心数~
- 如果全是使用IO的,那就可以设置很多很多线程,远超核心数
-
而实际情况不会这么极端,所以这个线程数一定是要看实际情况的
- 所以就要测试!
- 通过一些数据去看看哪个固定线程数是OK的
- 例如执行时间…检测资源使用状态~
- 控制变量法~
文章到此结束!谢谢观看 !
可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭🦆!
多线程初阶已经结束~ 后续会出线程进阶的博客!
敬请期待吧~
用户评论