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

一:定时器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则不会释放线程持有所持有对象的锁。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DurantJiang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值