定时器,Timer,java

本文详细介绍了Java中的Timer类如何实现定时任务,包括schedule方法的基本用法,以及如何处理定时器的线程模式(前台/后台)、任务执行顺序和线程安全问题。通过模拟实现了一个简单的定时器,展示了如何使用优先级队列和加锁机制确保任务执行的正确性。
摘要由CSDN通过智能技术生成

        有的时候我们需要设定一个时间,当到了设定时间的时候让定时器自动去执行某个逻辑,比如qq空间发说说有一个功能就是定时发送。

        Java标准库中提供了定时器的实现,就是java.util.Timer这个类。

Timer实现定时器

        Timer类实现定时器功能的核心方法就是schedule。最基础的schedule方法有两个参数,

        第一个参数task指定即将要执行的任务代码,第二个参数dalay指定多长时间之后执行任务代码。

        TimerTask类实现了Runnable接口,对要执行的任务进行了一个封装。Runnable这个接口在创建Thread实例的时候也遇到过,Runnable接口就相当于把“做事情”给抽象出来了。

        delay表示的是相对时间,在执行任务前延迟的ms,就是从任务添加到定时器中开始计时。时间过去了就执行任务代码,每个任务都对应一个delay时间,单位是ms。

简单用Timer实现一下定时器:

package thread;

import java.util.Timer;
import java.util.TimerTask;

public class ThreadDemo1 {
    public static void main(String[] args) throws InterruptedException {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hhh,3000");
            }
        },3000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hhh,1000");
            }
        }, 1000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hhh,2000");
            }
        }, 2000);
        Thread.sleep(4000);
        System.out.println("main方法结束");
    }
}

        执行之后你会发现,所有操作都执行完了,但是进程没有终止。这是因为Timer里面本身内置了一个线程,这个线程默认是一个前台线程,前台线程会阻塞进制终止,为什么要设置成前台线程,你想一想如果是后台线程,调用timer的main方法只有几行代码,很快就执行完要终止线程了,那么这时候进程就会终止了,但是放入的任务还没执因为等待的时间还没到。可以通过Timer timer = new Timer(true);将定时器内置的线程改成后台线程,来试一试,改成后台线程试一试执行效果。设置成前台线程还有一个原因,因为timer不知道什么时候会有任务添加进来,于是就一直“严阵以待”,我们可以在调用timer.cancel()方法,让线程主动结束。

        看到 @Override public void run() ,是不是感觉很熟悉,创建Thread实例也有这个重写run方法。创建Thread实例的时候我们可以将这个用lambda表达式简写,那么这里可不可以用lambda表达式简化呢?答案是不可以,因为这里schedule(TimerTask task, long delay),需要的参数是TimerTask类型,使用lambda表达式有一个要求,就是必须是函数式接口。。

        而TimerTask显然不是。Thread可以使用是因为Thread的构造方法需要的参数类型是Runnable,Runnable是函数式接口,我们传入实现Runnable接口的类的实例,此时会发生向上转型。

        但是呢,编译器有的时候会给你优化成下面这样的形式

        看起来和lambda表达式很像,就很容易误以为可以使用lambda表达式,但是实际上,你看看那个箭头和“->”还是有区别的,不信的话你可以写代码的时候去试试用lambda表达式,绝对会报错。个人任务编译器这样优化是为了代码看起来更简洁优美。

schedule还有很多不同参数的方法,简单说一下具体是什么意思

schedule(TimerTask task, long delay)

TimerTask task表示到时间后要执行的任务,具体的任务可以重写到TimerTask的run方法里,由Timer调用run方法(run方法就是回调函数)

long delay表示的是相对时间,从任务放入队列后开始计时的一段时间间隔,这段时间过去后,就开始执行任务。也就是在任务执行前延迟的ms数。

schedule(TimerTask task, Date time)

TimerTask task同上

Date time表示的是一个指定时间,Date类有一个构造函数 Date(int year, int month, int date),用于创建特定的日期对象。其中,year是从1900年开始计算的年数,month是从0开始计算的月份(0代表1月,1代表2月,以此类推),date是月份中的日期。

然而,这个构造函数在JDK 1.1后已被弃用。原因是这种方式容易引起混淆,且不支持国际化。后续有参数Date的schedule方法就不介绍了

schedule(TimerTask task, long delay, long period)

TimerTask task同上,long delay同上

long period指连续任务之间的时间间隔。

就是这个任务,需要重复执行很多次,period就是重复执行任务的时间间隔。

这个任务放到定时器中,先等delay时间过去,然后开始执行,之后每次重复执行只需要等一个period时间。

scheduleAtFixedRate(TimerTask task, long delay, long period)

TimerTask task同上,long delay同上, long period同上

该方法,如果由于任何原因(如垃圾回收或其他后台活动)而延迟执行,将会连续执行两次或更多次以“赶上”进度,使用与对于时间很敏感的活动。

简单模拟实现定时器

        首先思考一下一个定时器需要什么。

        定时器可以放各种任务,那么就需要一个容器;

        定时器可以到时间执行任务,那么就要有一个线程来不断的扫描容器里的任务有没有到时间,到时间了就由这个线程来执行;

        定时器需要等待时间最短的先执行,那这个容器就可以用优先级队列,扫描线程只需要扫描优先级队列的头,看看时间到没到就行。

        任务类需要有一个run方法来放要执行的任务,还需要有一个时间time,表示等待结束的时间。 

        因为任务类要放到优先级队列里,优先级队列要求放进去的元素得是可比的,所以需要实现Comparable接口,重写compareTo方法。

package thread;

import java.util.PriorityQueue;

/**
 * 模拟任务类
 */
abstract class MockTimeTask implements Comparable<MockTimeTask>{
    // time 是ms级别的时间戳
    public long time;
    public void run() {

    }
    public int compareTo(MockTimeTask o) {
//        return (int)(o.time - this.time);不确定顺序就试一试
        return (int)(this.time - o.time);
    }
}

/**
 * 模拟定时器
 */
class MockTimer {
    // 任务队列
    private volatile PriorityQueue<MockTimeTask> queue = null;
    // 扫描线程
    private Thread thread = null;
    // 锁对象
    private Object locker = new Object();
    public MockTimer () {
        this.queue = new PriorityQueue<>();
        // 线程开始扫描任务队列的头,到时间就执行run方法,执行完就删除元素,没到时间就啥也不干
        this.thread = new Thread(() -> {
            while (true) {
                // 如果任务对列为空则什么也不干
                if (queue.isEmpty()) {
                    continue;
                }
                MockTimeTask task = queue.peek();
                // 当前时间
                long currentTime = System.currentTimeMillis();
                if (currentTime >= task.time) {
                    // 时间到了就执行任务
                    task.run();
                    queue.poll();
                } else {
                    // 时间没到就什么也不干
                    continue;
                }
            }
            });
        thread.start();

    }

    // 向任务队列中添加任务, delay是相对时间
    public void schedule(MockTimeTask task, long delay) {
        //time是ms级别的时间戳,这样更方便比较是不是到时间了
        task.time = System.currentTimeMillis() + delay;
        queue.add(task);
    }
}
public class ThreadDemo3 {
    public static void main(String[] args) {
        MockTimer mockTimer = new MockTimer();
        mockTimer.schedule(new MockTimeTask() {
            @Override
            public void run() {
                super.run();
                System.out.println("hhh1000");
            }
        }, 1000);
        mockTimer.schedule(new MockTimeTask() {
            @Override
            public void run() {
                super.run();
                System.out.println("hhh2000");
            }
        }, 2000);
    }
}

但是此时还有问题,线程安全问题。

        主线程在对queue进行添加,thread线程在对queue进行删除,多个线程对同一个变量进行“写”操作,就有可能引起线程安全问题。可以对其进行加锁,来解决线程安全问题。要对操作queue的地方都加上锁,并且加锁的锁对象都得是同一个,因为这样才能构成锁竞争从而解决线程安全问题。

        


class MockTimer {
    // 任务队列
    private volatile PriorityQueue<MockTimeTask> queue = null;
    // 扫描线程
    private Thread thread = null;
    // 锁对象
    private Object locker = new Object();
    public MockTimer () {
        this.queue = new PriorityQueue<>();
        this.thread = new Thread(() -> {
            while (true) {
                synchronized (locker) {//给有关queue的操作加上锁
                    if (queue.isEmpty()) {
                        continue;
                    }
                    MockTimeTask task = queue.peek();
                    // 当前时间
                    long currentTime = System.currentTimeMillis();
                    if (currentTime >= task.time) {
                        // 时间到了就执行任务
                        task.run();
                        queue.poll();
                    } else {
                        // 时间没到就什么也不干
                        continue;
                    }
                }
            }});
        thread.start();

    }
    public void schedule(MockTimeTask task, long delay) {
        synchronized (locker) {
            task.time = System.currentTimeMillis() + delay;
            queue.add(task);
       }//给有关queue的操作加上锁
    }
}

        加锁方式不是绝对的,有的地方也可以进行改动,比如这里在thread里面加锁,也可以在判空的代码下加锁。

        虽然线程安全问题解决了,但是还是有些问题存在。

最终的代码:

package thread;

import java.util.PriorityQueue;

/**
 * 模拟任务类
 */
abstract class MockTimeTask implements Comparable<MockTimeTask>{
    // time 是ms级别的时间戳
    public long time;
    public void run() {

    }
    @Override
    public int compareTo(MockTimeTask o) {
//        return (int)(o.time - this.time);不确定顺序就试一试
        return (int)(this.time - o.time);
    }
}

/**
 * 模拟定时器
 */
class MockTimer {
    // 任务队列
    private volatile PriorityQueue<MockTimeTask> queue = null;
    // 扫描线程
    private Thread thread = null;
    // 锁对象
    private Object locker = new Object();
    public MockTimer () {
        this.queue = new PriorityQueue<>();
        // 线程开始扫描任务队列的头,到时间就执行run方法,执行完就删除元素,没到时间就啥也不干
        this.thread = new Thread(() -> {
            while (true) {
                synchronized (locker) {
                    // 如果任务对列为空则等待,等向队列中加入元素的时候再唤醒
                    while (queue.isEmpty()) {//这里可以写if,但是养成好习惯,每次被唤醒就是“沧海桑田”,还是再判断一下
                        try {
                            locker.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    MockTimeTask task = queue.peek();
                    // 当前时间
                    long currentTime = System.currentTimeMillis();
                    if (currentTime >= task.time) {
                        // 时间到了就执行任务
                        task.run();
                        queue.poll();
                    } else {
                        // 时间没到就等待
                     //Thread.sleep(task.time - currentTime);
                     // 这里不可以用sleep,因为如果有新增任务,等待时间更短 ,就可能会错过这个任务要执行的时间
                        try {
                            locker.wait(task.time - currentTime);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }

                    }
                }
            }});
        thread.start();

    }

    // 向任务队列中添加任务, delay是相对时间
    public void schedule(MockTimeTask task, long delay) {
        //time是ms级别的时间戳,这样更方便比较是不是到时间了
        synchronized (locker) {
            task.time = System.currentTimeMillis() + delay;
            queue.add(task);
            locker.notify();
        }
    }
}
public class ThreadDemo3 {
    public static void main(String[] args) {
        MockTimer mockTimer = new MockTimer();
        mockTimer.schedule(new MockTimeTask() {
            @Override
            public void run() {
                super.run();
                System.out.println("hhh1000");
            }
        }, 1000);
        mockTimer.schedule(new MockTimeTask() {
            @Override
            public void run() {
                super.run();
                System.out.println("hhh2000");
            }
        }, 2000);

    }

}

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值