多线程编程设计模式(单例,阻塞队列,定时器,线程池)(二)+https://developer.aliyun.com/article/1413586
简单使用
public static void main(String[] args) { // 使用上述阻塞队列实现生产者消费者模型 MyBlockingQueue queue = new MyBlockingQueue(); // 生产者模型 Thread t1 = new Thread(() -> { int num = 1; while (true) { try { queue.put(num+""); System.out.println("生产者生产:" + num); num++; Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); // 消费者模型 Thread t2 = new Thread(() ->{ while (true) { try { String ret = queue.take(); System.out.println("消费者消费:" + ret); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); t1.start(); t2.start(); }
说明:在生产者中使用sleep,就是生产的慢,消费的快,每生产出一个就被消费
如果在消费者中使用sleep,就是生产的快,消费的慢,会有大量的数据存储在阻塞队列之中,当队列为满时,就要阻塞等待,让消费者先消费
以上就是关于生产者消费者模型的所有内容,接下来介绍另一种设计模式–定时器
四.定时器
1.引言
定时器也是常见的开发组件之一,主要用来定时
执行任务.这种操作也是很常见的,比如在进行网络通信的时候,如果客户端向服务器发送了一个请求,但是服务器迟迟没有响应,那客户端需要一直等下去吗?这显然不是一个好的方案,我们应该设置等待期限,到达期限之后再去执行其他任务(重新发送一次请求?直接退出?)
2.定时器的使用
在java的标准库内部也实现了定时器,被封装为一个类Timer
从他的源码部分我们可以了解到关于Timer类的一些知识
- 每个Timer类都对应着一个后台线程,用于执行未来的任务或者间隔重复执行默写任务
- Timer类通过stop或者cancel方法结束
- Timer类内部的任务通过**优先级队列(堆)**进行管理的,调用任务的时间复杂度为O(logN),N是同时调度的任务数
// 创建出Timer类 Timer timer = new Timer(); // 通过schedule方法进行任务的设置 timer.schedule(new TimerTask() { // 任务1将在1s后执行 @Override public void run() { System.out.println("这是任务1"); } },1000);
timer类是通过schedule方法进行任务的设置,Timertask是一个匿名内部类,这个类就是定时器要执行的任务,以及任务执行的时间的一个抽象
所以,schedule方法实际上有两个参数,第一个参数是要执行的任务,第二参数是任务的执行时间的间隔(以当前时间为基准)
public void schedule(TimerTask task, long delay) { // 如果间隔时间<0 非法 抛异常 if (delay < 0) throw new IllegalArgumentException("Negative delay."); // 调用sched方法执行任务 参数1:要执行的任务 参数2:要执行任务的绝对时间(当前时间+间隔时间) // 参数3:period 任务重复执行的时间 这里设置为0 代表默认只执行一次 sched(task, System.currentTimeMillis()+delay, 0); }
一个简单的使用
public static void main(String[] args) { // 创建出Timer类 Timer timer = new Timer(); // 通过schedule方法进行任务的设置 timer.schedule(new TimerTask() { // 任务1将在1s后执行 @Override public void run() { System.out.println("这是任务1"); } },1000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("这是任务2"); } },2000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("这是任务3"); timer.cancel();// 执行完所有的任务后 终止timer内部的线程 否则会一直等待 } },3000); System.out.println("定时器的使用"); }
三个任务依次执行
3.定时器的模拟实现
由上述源码我们可以总结出要实现定时器的一些关键要点
- 要有一个Timer类,表示定时器
- Timer类内部有一个方法schedule用于定时执行任务
- Timer类内部要有一个线程,专门用于根据执行时间执行任务
- 要有一个数据结构根据时间的先后顺序执行任务
- 要有一个类似于TimerTask的类用于管理要执行的任务
首先创建出要管理的任务类
// 通过这个类 描述了一个任务 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; } // 重写compareTo方法 设置为time小的先执行 public int compareTo(MyTimerTask o) { return (int)(this.time - o.time); } // 设置获取方法 public Runnable getRunnable() { return runnable; } public long getTime() { return time; } }
创建出模拟计时器类MyTimer
class MyTimer { // 使用优先级队列管理数据 队列的元素是任务类 private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(); // 因为调用schedule的线程和本身的扫描线程都会对queue进行修改 // 所以存在线程安全问题 要加锁 // 创建用于加锁的对象 private Object locker = new Object(); // 提供schedule方法 public void schedule(Runnable runnable,long delay) { synchronized (locker) { // 所以 schedule方法的作用就是将一个任务转化为队列的一个元素 queue.offer(new MyTimerTask(runnable,delay)); locker.notify();// 进行唤醒 // 此处的唤醒两处的wait // 一是为空 需要新元素添加进来 此处需要wait // 二是距离最快执行任务还有一定的时间 为了减少cpu资源的开销与调度 需要扫描线程进行阻塞等待 } } // 扫描线程属于定时器类 public MyTimer() { // MyTimer类的扫描线程 用于管理要执行的任务 Thread t = new Thread(() -> { // 因为要不断的进行扫描 判断是否要执行对应的任务 此处应使用循环 while(true) { try { synchronized(locker) { // 队列为空 没有要执行的任务 阻塞等待 使用wait方法 // 等到有新的元素添加进队列之后再唤醒 // 所以在schedule方法中进行唤醒 // 此处也不能使用sleep方法进行阻塞等待 因为在等待的过程中可能添加新的任务 // 新的任务的执行时间有可能比当前队首元素的执行时间更早 要更换执行顺序 while(queue.isEmpty()) { locker.wait(); } // 不为空 取出队首元素 并判断是否需要执行 MyTimerTask task= queue.peek(); long curTime = System.currentTimeMillis(); if(curTime >= task.getTime()) { // 达到要执行任务的时间 执行任务 task.getRunnable().run(); // 执行完毕之后需要将此任务从队列中删除 queue.poll(); }else { // 走到这里代表还未到执行任务的时间 // 如果不等待 则会一直进行while循环 会占用cpu资源 // 所以这里可以让扫描线程阻塞等待 一直等待到最短的任务执行时间到了 locker.wait(task.getTime() - curTime ); } } }catch (InterruptedException e) { e.printStackTrace(); } } }); // 启动线程 t.start(); } }
简单使用
public static void main(String[] args) { MyTimer timer = new MyTimer(); timer.schedule(new Runnable() { @Override public void run() { System.out.println("3000"); } }, 3000); timer.schedule(new Runnable() { @Override public void run() { System.out.println("2000"); } }, 2000); timer.schedule(new Runnable() { @Override public void run() { System.out.println("1000"); } }, 1000); System.out.println("程序开始执行"); }
执行结果
4 总结:定时器类模拟实现的一些补充说明
- 关于线程安全
- 关于线程等待
使用wait的地方有两处,所以schedule方法中的notify操作会唤醒两处的wait - 优先级队列中存储的元素必须是能够进行比较的,所以任务类MyTimerTask也要能够进行比较,比较的依据是执行时间的远近,可以让MyTimerTask类实现Comparable接口或者使用Comparator来构造比较器进行比较
定时器的模拟实现虽然代码不多,但是要考虑的地方很多,逻辑性较强,各位读者后续可以勤加练习!!!
五.线程池
1.前言
线程又被称为轻量级进程,原因在于多个线程公用同一个进程的内存资源,省去了内存创建和销毁的开销,但是有对比才有伤害,如果进一步的提高调度的频率,线程的开销也就无法避免了,为了进一步的提高效率,又设计出了两种更加高效的方式
- 协程:轻量级线程,他省去了线程通过cpu的调度,而是程序员自己手动去调度,进一步降低了开销,提高了效率;但是这种方式在java的圈子里并不是很流行,原因在于第二种方式线程池更加成熟,使用者更为广泛
- 线程池:通过提前创建好线程,在使用的时候直接从线程池里面拿取线程,大大减少了用户态和内核态的交互,进一步提高了效率
2.线程池的基本概念
“xx池"其实在计算机中经常遇到,比如"线程池”“字符串池”“常量池”"数据库连接池"等等,"池"
这种思想类似于现实生活中的资源共享,重复利用,通过这种方式能够提高物品的使用效率,降低环境的负载
线程池也是起类似的作用,通过预先创建好一些线程存储到"线程池"内部,在需要调度线程的时候就拿来使用,使用完毕之后不销毁线程,而是重新放到线程池中,这样就省去了线程的开辟和销毁的开销,进一步的提高了效率
"池"这种操作其实还涉及到计算机交互的一个知识,即纯用户态的操作比内核态-用户态交互的方式效率更高!!!直接从线程池中获取线程就属于纯用户态的操作,而通过操作系统创建/销毁线程就属于内核态-用户态的交互,所以线程池的效率更高
为什么说纯用户态的操作效率更高呢?主要有以下三点原因:
- 减少上下文的切换:由用户态转换为内核态涉及到上下文的切换,即将处理器的执行状态由用户态转换为内核态或反之,更改处理器的执行状态需要保存当前执行状态,这涉及到寄存器的保存,权限切换操作,开销较大
- 减少了系统调用的次数:纯用户态的操作不需要访问系统资源,减少了系统调用的次数,进一步提高了效率
- 减少了权限校验和安全检查:当访问内核态的数据时,涉及到频繁地权限检验和安全检查.而纯用户态的操作并不需要进行权限校验和安全检查
内核态的操作就相当于从保险柜里获取数据,要想获取,必须现有钥匙,还要经过一系列的检查,权限认证(不是自己人就不能打开),操作流程繁杂,获取数据的速度慢
多线程编程设计模式(单例,阻塞队列,定时器,线程池)(四)+https://developer.aliyun.com/article/1413589