Java学习-多线程-2-定时器Timer与线程之间的通信

原创 2018年04月16日 18:15:39

一:定时器Timer

Timer是一种定时器工具,用来在一个后台线程计划执行指定任务;它可以计划执行一个任务一次或反复多次。

首先看下Timer定时执行的例子;我们用Timer实现三秒后输出hello,I have done it;代码如下所示:

public class Test2 {
    static public void main(String... args){
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello,I have done it");
            }
        },3000);
        while (true){
            // 此处打印每秒的时间,让结果更清晰
            System.out.println(new Date().getSeconds());
            try {
                //  睡眠一秒,每秒打印一次当前的秒数
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 输出结果:
    54
    55
    56
    hello,I have done it
    57
    58

可以看到,在三秒之后输出了想要输出的语句;那么这个执行过程是什么样的呢?首先需要了解一下TimerTask类

/**
 * A task that can be scheduled for one-time or repeated execution by a Timer.
 *
 * @author  Josh Bloch
 * @see     Timer
 * @since   1.3
 */

public abstract class TimerTask implements Runnable {
}

我们可以看到,TimerTask类是一个抽象类,并且实现了Runnale接口。显然,当我们在创建一个匿名内部类并重写了run方式时,就相当于创建了一个线程,并在run方法里面实现自己的逻辑。

再看Timer类的schedule方法,它是一个重载方法,查看源码如下所示:

// 等待多久后执行task,只执行一次
public void schedule(TimerTask task, long delay) {
        if (delay < 0)
            throw new IllegalArgumentException("Negative delay.");
        sched(task, System.currentTimeMillis()+delay, 0);
    }
 // 在某个时间执行task,只执行一次
public void schedule(TimerTask task, Date time) {
        sched(task, time.getTime(), 0);
    }
// 等待多久之后执行task,并且每隔多久执行一次
public void schedule(TimerTask task, long delay, long period) {
        if (delay < 0)
            throw new IllegalArgumentException("Negative delay.");
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, System.currentTimeMillis()+delay, -period);
    }
// 第一次执行时间,之后每隔多久就再次执行
public void schedule(TimerTask task, Date firstTime, long period) {
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, firstTime.getTime(), -period);
    }

我们所用的就是过多久就执行,并只执行一次的那个方法;接下来我们再看个例子,延迟3秒后执行,并且执行之后每两秒执行一次,代码如下:

public class Test2 {
    static public void main(String... args){
        Timer timer = new Timer();
        MyTimerTask myTimerTask = new MyTimerTask();
        timer.schedule(myTimerTask,3000,2000);
        while (true){
            // 此处打印每秒的时间,让结果更清晰
            System.out.println(new Date().getSeconds());
            try {
                //  睡眠一秒,每秒打印一次当前的秒数
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class MyTimerTask extends TimerTask{
    @Override
    public void run() {
        System.out.println("you still have lots more to work on!");
    }
}
  • 输出结果如下:
    0
    1
    2
    you still have lots more to work on!
    3
    4
    you still have lots more to work on!
    5
    6
    you still have lots more to work on!
    7
    8
    you still have lots more to work on!
    9

可以清晰的看到:在三秒之后执行了一次,并且之后每过2s都再次执行。此处没有用匿名内部类,而是自己定义了一个类去继承抽象类TimerTask,效果都是一样的。

看到这儿,我们可以定义自己的需求去定时执行任务,比如;我们想要实现一个功能,首先是等两秒之后执行,然后再是等四秒执行,再是等两秒执行,如此交替反复执行;该如何实现该功能呢?代码如下:

public class Test2 {
    static public void main(String... args){
        Timer timer = new Timer();
        MyTimerTask myTimerTask = new MyTimerTask();
        timer.schedule(myTimerTask,2000);
        while (true){
            // 此处打印每秒的时间,让结果更清晰
            System.out.println(new Date().getSeconds());
            try {
                //  睡眠一秒,每秒打印一次当前的秒数
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class MyTimerTask extends TimerTask{
    private static int i = 0;
    @Override
    public void run() {
        // 执行一次,数量加1
        i++;
        System.out.println("you still have lots more to work on!");
        // 根据i的值来判断应该延迟多少s执行
        new Timer().schedule(new MyTimerTask(),2000+(i % 2 == 0 ? 0 : 2000));
    }
}

输出结果如下:

  • 52
    53
    you still have lots more to work on!
    54
    55
    56
    57
    you still have lots more to work on!
    58
    59
    you still have lots more to work on!
    0
    1
    2
    3
    you still have lots more to work on!
    4
    5

解析:因为需要两秒和四秒交替执行,所以用原本的方式是不能实现的,那么就应该继承TimerTask类来自己实现run方法的逻辑,我们在类中定义一个静态变量i,通过i的奇偶值来判断应该等待两秒还是等待四秒执行,并在run方法里面动态的传入需要等待的时间,以此来达到目的。

该程序执行时,首先我们第一次调用时传的2000,第一次输出时是在2s后,当输出完之后,再次调用timer对象的schedule方法,此时,传入的TimerTask是自己定义的MyTimerTask类,此时i的值为1,而传入delay值的表达式为:2000+(i % 2 == 0 ? 0 : 2000),1%2 不等于0;那么再次调用的时候,delay的值就是4000,然后再执行到这儿的时候,i的值为2,此时delay的值就为2000;如此交替执行下,就能实现想要达到的结果。

现在实现定时任务都是用开源框架quartz,它的cron表达式能够很好的定义时间去执行job

二:线程之间的通信

当多个线程一起工作时,我们希望它们能够有规律的执行,此时就要用到线程通信。

例如:我们想要实现两个线程同时输出(子线程和主线程),子线程输出2次,主线程输出4次,如此循环10次。那么应该怎么做呢?我们很容易想到定义两个线程来输出;代码如下:

public class Test2 {
    static public void main(String... args){
        final Work work = new Work();
        // 子线程输出2次,循环10次
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1;i <=10; i++ ){
                    work.subSay(i);
                }
            }
        }).start();
        // 主线程输出4次,循环10次
        for (int i = 1; i <= 10; i++){
            work.mainSay(i);
        }
    }
}
class Work{
    public void subSay(int j){
        for (int i = 1; i <= 2; i++){
            System.out.println(Thread.currentThread().getName()+"子线程输出第"+i+"次,第"+j+"轮");
        }
    }
    public void mainSay(int j){
        for (int i = 1; i <= 4; i++){
            System.out.println(Thread.currentThread().getName()+"主线程输出第"+i+"次,第"+j+"轮");
        }
    }
}

我们很容易想到,将两个线程的输出操作封装到一个对象里,然后启动两个线程去调打印方法,如上图所示,该程序的执行结果并不能达到我们的要求,结果就不展示了,因为上面的程序执行,没有同步,没有通信,那么就是谁抢到cpu的执行权谁就执行,所以执行的顺序是凌乱的;接下来我们改造一下该Work类来达到我们的目的。

public class Test2 {
    static public void main(String... args){
        final Work work = new Work();
        // 子线程输出2次,循环10次
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1;i <=10; i++ ){
                    work.subSay(i);
                }
            }
        }).start();
        // 主线程输出4次,循环10次
        for (int i = 1; i <= 10; i++){
            work.mainSay(i);
        }
    }
}
class Work{
    /**
     * 定义一个变量,当该值为true时,子线程打印,为false时,主线程打印
     */
    private static boolean flag = true;
    public synchronized void subSay(int j){
        if (!flag){
            try {
                // 等待,让出cpu执行权
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        for (int i = 1; i <= 2; i++){
            System.out.println(Thread.currentThread().getName()+"子线程输出第"+i+"次,第"+j+"轮");
        }
        // 自己执行完成之后改变状态
        flag = false;
        // 唤醒主线程
        this.notify();
    }
    public synchronized void mainSay(int j){
        if (flag){
            try {
                // 等待,让出cpu执行权
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        for (int i = 1; i <= 4; i++){
            System.out.println(Thread.currentThread().getName()+"主线程输出第"+i+"次,第"+j+"轮");
        }
        // 改变状态让子线程打印
        flag = true;
        // 唤醒子线程
        this.notify();
    }
}

执行结果为:

  • Thread-0子线程输出第1次,第1轮
    Thread-0子线程输出第2次,第1轮
    main主线程输出第1次,第1轮
    main主线程输出第2次,第1轮
    main主线程输出第3次,第1轮
    main主线程输出第4次,第1轮

    Thread-0子线程输出第1次,第10轮
    Thread-0子线程输出第2次,第10轮
    main主线程输出第1次,第10轮
    main主线程输出第2次,第10轮
    main主线程输出第3次,第10轮
    main主线程输出第4次,第10轮

可以看到,两个线程现在就能完美协调的进行打印数据了;为什么呢?我来解释一下:
首先我们在两个方法都加上了synchronized,这样就会让子线程在执行打印方法时,主线程不会去执行主线程的打印方法;可能有人会问,我主线程又没有执行子线程的打印方法,为什么还要等待子线程执行完成呢?前面我们知道,synchronized是对象锁,而两个线程都是调用相同对象的方法,所以,当锁被子线程持有时,主线程就获取不到work对象的锁,就不会执行同步方法里面的代码。然后子线程进入方法之后会有两种情况:

  1. 此时该子线程执行,那么执行完之后释放锁,由主线程执行
  2. 不该子线程执行,那么调用wait()方法,释放锁,主线程执行完之后,唤醒子线程。

主线程依然如此,所以就会出现两个线程有序的进行工作。

代码中,我们判断flag状态时用的是if,这样是不好的,应该将if换成while,换成while的目的是为了防止虚假唤醒

虚假唤醒就是一些obj.wait()会在除了obj.notify()和obj.notifyAll()的其他情况被唤醒,而此时是不应该唤醒的。

介绍Object的两个方法:

  1. wait() 线程进入等待状态,释放锁,让出cpu的执行权
  2. notify() 唤醒一个在等待的线程

注意:这两个方法必须在同步代码块或者同步方法中才能够使用

三:notify 与notifyAll 的区别

notify只会随机通知一个在等待的对象,而notifyAll会通知所有在等待的对象,并且所有对象都会继续运行
  • notify:当有多个线程在等待锁时,使用notify会随机唤醒一个线程,被唤醒的线程会获得对象锁,然后继续运行
  • notifyAll:当有多个线程在等待锁时,使用notifyAll会唤醒所有等待的线程,然后被唤醒的线程些就会去争抢锁,谁抢到了锁,谁就执行;没有抢到锁的线程会进到线程的锁池中。

有兴趣的小伙伴可以试试实现一个线程不断制造面包,另外五个线程去消费面包,当有面包是才能去消费。

四:wait与sleep的区别

当某个线程持有某个对象的锁时,如果使用wait方法会释放该对象的锁,将cpu的执行权让给其他线程去执行。
而sleep则不会释放线程持有所持有对象的锁。

版权声明: https://blog.csdn.net/weixin_38956287/article/details/79959649

《多线程编程》学习之十:定时器Timer的使用,线程安全的单例模式

一、定时器Timer的使用          定时器 Timer类主要的作用是设置计划任务,它在内部使用多线程的方式进行处理;而抽象的TimerTask类负责封装任务,它实现了Runnable接口。 ...
  • studyhxz
  • studyhxz
  • 2016-11-08 16:21:39
  • 1724

java多线程之定时器Timer

在java中,定时计划任务功能主要使用的就是Timer对象,主要有如下技术点: 实现指定事件执行指定任务 实现按照指定周期执行任务 定时器Timerschedule(TimerTask task,Da...
  • mockingbirds
  • mockingbirds
  • 2016-07-24 10:18:36
  • 4083

.NET Framework中定时器timer的单线程与多线程使用讲解

如果你需要使用规律的时间间隔重复执行一些方法,最简单的方式是使用定时器(timer)。与下边的例子相比,定时器可以便捷、高效地使用内存和资源: ? 1 2 3 4...
  • hoiven
  • hoiven
  • 2016-05-10 13:50:18
  • 6807

vc中定时器并非多线程

VC++中timer很容易给人感觉是多线程的。其实不然,他是通过消息触发事件的。通过SetTimer函数设定定时器后,在规定时间内向消息队列中加入wm_timer消息来触发事件。并且只有该消息返回后才...
  • qq_23992597
  • qq_23992597
  • 2016-06-13 14:53:55
  • 2216

Delphi定时器线程

UntMyTaskTimer.pas //任务定时器--APC函数(异步过程调用) //由于在调用APC函数时,SleepEx会锁死当前线程 //所以,需要创建单独的线程来处理 unit Unt...
  • liang08114
  • liang08114
  • 2017-12-19 10:54:14
  • 228

定时器和多线程的区别和联系

向原作者致敬!!!  1 软件定时器  很多同学在工程中喜欢使用软件定时器,因为其使用简单,仅需设置一个时长和其OnTime事件即可使用。确实,软件定时器在某些持续性不强的重复性工作中效率...
  • sunka1982
  • sunka1982
  • 2015-07-14 17:20:35
  • 3945

Qt线程和定时器

新的线程run里面一定要有exec的调用,否则无法接受消息的。class myQThr : public QThread { Q_OBJECT public: myQThr(QObject *in...
  • ztz0223
  • ztz0223
  • 2013-05-15 14:05:08
  • 10801

例说多线程定时器System.Timers.Timer

System.Timers.Timer是多线程定时器,如果一个Timer没有处理完成,到达下一个时间点,新的Timer同样会被启动,所以在使用Timer时需要注意。下面的实例显示了Timer的使用方法...
  • tiana0
  • tiana0
  • 2016-05-08 17:07:03
  • 12000

java定时器和多线程实践记录

这几天因为需要测试mongodb读写分离的问题,因此写了个定时查询程序,并且用到了多线程,以达到定时启动多个线程查询数据库的效果,下边代码记录备忘: package timmer; import j...
  • tuzongxun
  • tuzongxun
  • 2015-12-14 17:46:54
  • 1815
收藏助手
不良信息举报
您举报文章:Java学习-多线程-2-定时器Timer与线程之间的通信
举报原因:
原因补充:

(最多只允许输入30个字)