赞
踩
目录
定时器是软件开发中的一个重要组件. 类似于一个 "闹钟". 达到一个设定的时间之后, 就执行某个指定 好的代码。
定时器是一种实际开发中非常常用的组件
比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连.
比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除).
类似于这样的场景就需要用到定时器.
在Java标准库中也提供了一个Timer类,核心方法为 schedule 。schedu方法可以理解为为定时器分配任务。schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后 执行 (单位为毫秒).。我们下面简单看一下他的用法:
- Timer timer = new Timer();
- timer.schedule(new TimerTask() {
- @Override
- public void run() {
- System.out.println("hello");
- }
- }, 3000);
TimeTask实际上是实现了Runnable接口的一个类,所以要重写run方法。
大家看到Timer能够在指定的一段时间后执行某个任务,很自然会想到Thread.sleep。虽然sleep方法的确也能在一段时间后再去执行某个任务,但是在sleep期间线程处于阻塞状态,其他任务也无法进行,而Timer等待期间线程可以完成其他工作,提高cpu利用率。
首先我们明确一下设计思路
首先定时器内是可以加入多个任务等待执行的,所以我们需要一个容器储存,既然我们需要按照时间顺序来执行任务我们很自然会想到PriorityQueue(优先级队列)这个东西,但是我们是处于多线程的状态,还需考虑线程安全问题,结合我们上次讲的BlockingQueue(阻塞队列),其实我们java内是自带了PriorityBlockingQueue(优先级阻塞队列)供我们使用。
其次我们的每个任务是需要通过一定的方式来描述出来,这里我们自定义一个类来描述。
同时我们需要一个线程,通过这个线程来扫描定时器内部任务,执行其中时间到了的任务。这里我们在构造方法里面来实现。实现Timer类一被创建就能够即使扫描将要执行的任务。
当然由于多线程的线程安全问题,我们还需要使用synchronized将一些关键操作锁住,我们下面结合代码再分析一下。
- import java.util.Timer;
- import java.util.concurrent.PriorityBlockingQueue;
-
- class MyTask implements Comparable<MyTask>{
- //任务要干什么
- private Runnable command;
- //等待时长
- private long time;
-
- public MyTask(Runnable command,long after){
- this.command=command;
- this.time=System.currentTimeMillis()+after;
- }
-
- public void run(){
- command.run();
- }
-
- public long getTime(){
- return time;
- }
-
- @Override
- public int compareTo(MyTask o) {
- //时间小的在前面
- return (int)(this.time-o.time);
- }
- }
-
- class MyTimer {
- private Object locker=new Object();
-
- private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
-
- public void schedule(Runnable command,long after){
- MyTask myTask=new MyTask(command,after);
- synchronized (locker) {
- queue.put(myTask);
- locker.notify();
- }
- }
-
- public MyTimer(){
- Thread t=new Thread(()->{
- while(true){//不断扫描
- try{
- synchronized (locker) {
- //队列为空则wait
- if(queue.isEmpty()){
- locker.wait();
- }
- MyTask myTask = queue.take();
- long curTime = System.currentTimeMillis();
- if (myTask.getTime() > curTime) {
- // 时间还没到, 塞回到队列中
- queue.put(myTask);
- locker.wait(myTask.getTime()-curTime);
- } else {
- // 时间到了~~, 直接执行任务
- myTask.run();
- }
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- });
- t.start();
- }
- }
-
- public class MyTimerDemo {
- public static void main(String[] args) {
- MyTimer myTimer=new MyTimer();
- myTimer.schedule(new Runnable() {
- @Override
- public void run() {
- System.out.println("test");
- }
- },3000);
- }
- }
首先我们需要注意的是,由于PriorityBlockingQueue涉及到比较操作,所以我们需要在MyTask类内部实现Comparable接口。
之后我们需要对入队和出队等一系列操作加锁,否则可能会出现下面的情况:
假设现在9点,我们有一个任务需要在10点执行,此时我们的线程准备进入等待1h状态,但是此时突然有一个9点30需要执行的任务插入,执行notify操作,但由于操作没有加锁,可能导致notify比wait早一点执行导致实际上并没有唤醒线程。
最后,为什么队列为空时需要进行wait操作?
当刚创建队列还未添加任务时,此时队列为空,假如不进行等待操作,此时就会直接进入加锁操作,开始执行take,由于队列为空进入阻塞状态,且并没有释放锁。此时就会来到schedule方法准备执行put方法,但是在进行加锁操作时发现锁已经被占用,而另一边也因为队列为空一直处于阻塞状态无法释放锁,就形成了死锁。
之前我们学习过字符串常量池,它的存在是为了减少系统资源的开销。而线程池同样如此,虽然创建线程 / 销毁线程的开销已经比较小了,但是在面对非常多线程的情况下计算机资源还是略显捉急,所以就有了线程池。线程池最大的好处就是减少每次启动、销毁线程的损耗。
那为啥把线程放进池子就比从系统这里创建线程要来的快呢?
实际上,从池子里去涉及的是纯用户态操作
而通过系统来创建,设计内核态操作
通常我们认为,牵扯到内核态的操作要比纯用户态更加低效
使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
返回值类型为 ExecutorService
通过 ExecutorService.submit 可以注册一个任务到线程池中.
- ExecutorService pool = Executors.newFixedThreadPool(10);
- pool.submit(new Runnable() {
- @Override
- public void run() {
- System.out.println("hello");
- }
- });
Executors 创建线程池的几种方式:
1.newFixedThreadPool: 创建固定线程数的线程池
2.newCachedThreadPool: 创建线程数目动态增长的线程池.
3.newSingleThreadExecutor: 创建只包含单个线程的线程池.
4.newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.
不知道大家有没有注意到我们ExecutorService的初始化方式好像和之前的其他类不太一样,是使用Executors来初始化的,而不是原本的构造方法。Executors 本质上是 ThreadPoolExecutor 类的封装。这种在原本的构造方法外进行包装作用的方法就叫做工厂方法,这种模式也被称作工厂模式。而工厂模式的意义实际上是为了突破一些构造方法的限制。比如下面的例子。
假如我们想用笛卡尔坐标系和极坐标系两种方式来表示一个点,可能会写出这样的代码
- class point{
- public Point(double x,double y){};//笛卡尔坐标系
- public Point(double x,double y){};//极坐标系
- }
-
我们想要重载构造方法,但是由于参数都是一样的,并且构造方法要求同名,所以很明显上面的方法会编译错误。
- public static PointMakeByXY(double x,double y){
- Point p=new Point();
- p.setX(x);
- p.setY(y);
- return p;
- }
-
- public static PointMakeByRA(double r,double a){
- Point p=new Point();
- p.setR(r);
- p.setA(a);
- return p;
- }
但是我们可以把它们包装成一个静态方法,这样就可以通过类名调用。
这里我们只是为了帮助大家理解线程池概念,所以自己动手实现一个非常简单的线程池,实际使用我们还是使用java库自带的。
由于插入的任务一下子可能很多,所以我们采用阻塞队列存储
-
- import java.util.concurrent.BlockingQueue;
- import java.util.concurrent.LinkedBlockingQueue;
-
- class MyThreadPool{
- private BlockingQueue<Runnable> queue=new LinkedBlockingQueue<>();
- // 核心方法, 往线程池里插入任务
- public void submit(Runnable runnable){
- try {
- queue.put(runnable);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- // 设定线程池里有几个线程
- public MyThreadPool(int n){
- for(int i=0;i<n;i++){
- Thread t=new Thread(()->{
- while(!Thread.currentThread().isInterrupted()){
- try {
- Runnable runnable=queue.take();//获取任务
- runnable.run();
- } catch (InterruptedException e) {
- e.printStackTrace();
- break;
- }
- }
- });
- t.start();
- }
- }
- }
-
- public class MyThreadPoolDemo {
- public static void main(String[] args) {
- MyThreadPool myThreadPool = new MyThreadPool(10);//初始化时所有线程处于WAITING
- for (int i = 0; i < 100; i++) {
- myThreadPool.submit(new Runnable() {
- @Override
- public void run() {
- System.out.println("hello");
- }
- });
- }
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。