《疯狂java讲义》学习(45):线程通信&线程组

版权声明:本文为博主原创文章,如若转载请注明出处 https://blog.csdn.net/tonydz0523/article/details/87388706

1.线程通信

当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,但我们可以通过一些机制来保证线程协调运行。

1.1 传统的线程通信

假设现在系统中有两个线程,这两个线程分别代表存款者和取钱者——现在假设系统有一种特殊的要求,系统要求存款者和取钱者不断地重复存款、取钱的动作,而且要求每当存款者将钱存入指定账户后,取钱者就立即取出该笔钱。不允许存款者连续两次存钱,也不允许取钱者连续两次取钱。
为了实现这种功能,可以借助于Object类提供的wait()、notify()和notifyAll() 3个方法,这3个方法并不属于Thread类,而是属于Object类。但这3个方法必须由同步监视器对象来调用,这可分成以下两种情况。

  • 对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这3个方法。
  • 对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这3个方法。

关于这3个方法的解释如下:

  • wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。该wait()方法有3种形式——无时间参数的wait(一直等待,直到其他线程通知),带毫秒参数的wait和带毫秒、毫微秒参数的wait(这两种方法都是等待指定时间后自动苏醒)。调用wait()方法的当前线程会释放对该同步监视器的锁定。
  • notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程。
  • notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。

程序中可以通过一个旗标(flag)来标识账户中是否有存款,当旗标为false,表明账户中没有存款,存款者线程可以向下执行,当存款者把钱存入账户后将旗标设为true,并调用notify()或notifyAll()方法来唤醒其他线程;当存款者线程进入线程体后,如果旗标为true就调用wait()方法让线程等待。
当旗标为true时,表明账户中已经存入了存款,则取钱者线程可以向下执行,当取钱者把钱从账户中取出后,将旗标设为false,并调用notify()或notifyAll()方法来唤醒其他线程;当取钱者线程进入线程体后,如果旗标为false就调用wait()方法让该线程等待。
本程序为Account类提供draw()和deposit()两个方法,分别对应该账户的取钱、存款等操作,因为这两个方法可能需要并发修改Account类的balance Field,所以这两个方法都使用synchronized修饰成同步方法。除此之外,这两个方法还使用了wait()、notifyAll()来控制线程的协作。

package Synchronized;

public class Account {
    private String accountNo;
    private double balance;
    private boolean flag = false;

    public Account() { }

    // 构造器
    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }
    public String getAccountNo() {
        return this.accountNo;
    }

    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    public double getBalance() {
        return this.balance;
    }

    public synchronized void draw(double drawAmount) {
        try {
            //如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
            if (!flag) {
                wait();
            } else {
                //执行取钱操作
                System.out.println(Thread.currentThread().getName() + "取钱:" + drawAmount);
                balance -= drawAmount;
                System.out.println("账户余额为:" + balance);
                // 将标识设为false
                flag = false;
                // 唤醒其他线程
                notifyAll();
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }

    public synchronized void deposit(double depositAmount) {
        try {
            // 如果flag为真,表明账户中已有人存钱进去,存钱方法阻塞
            if (flag) {    //①
                wait();
            } else {
                System.out.println(Thread.currentThread().getName() + "存款" + depositAmount);
                // 标识账户已经有存款旗标设为true
                flag = true;
                // 唤醒其他线程
                notifyAll();
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }
    public int hashCode() {
        return accountNo.hashCode();
    }

    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj != null && obj.getClass() == Account.class) {
            Account target = (Account)obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
}

对存款者线程而言,当程序进入deposit()方法后,如果flag为true,则表明账户中已有存款,程序调用wait()方法阻塞;否则程序向下执行存款操作,当存款操作执行完成后,系统将flag设为true,然后调用notifyAll()来唤醒其他被阻塞的线程——如果系统中有存款者线程,存款者线程也会被唤醒,但该存款者线程执行到①号代码处时再次进入阻塞状态,只有执行draw()方法的取钱者线程才可以向下执行。同理,取钱者线程的运行流程也是如此。
程序中的存款者线程循环100次重复存款,而取钱者线程则循环100次重复取钱,存款者线程和取钱者线程分别调用Account对象的deposit()、draw()方法来实现:

package Synchronized;

public class DrawThread extends Thread {
    // 模拟用户账户
    private Account account;
    // 当前取钱线程希望取的钱数
    private double drawAmount;
    public DrawThread(String name, Account account, double drawAmount) {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }
    // 重复执行100次取钱操作
    public void run() {
        for (int i=0; i < 100; i++) {
            account.draw(drawAmount);
        } 
    }
}

deposit实现:

package Synchronized;

public class DepositThread extends Thread {
    // 模拟用户账户
    private Account account;
    // 当前存款线程所希望存的钱数
    private double depositAmount;

    public DepositThread(String name, Account account, double depositAmount) {
        super(name);
        this.account = account;
        this.depositAmount = depositAmount;
    }
    // 重复执行100次存款操作
    public void run() {
        for (int i=0; i<100; i++) {
            account.deposit(depositAmount);
        }
    }
}

主程序可以启动任意多个存款线程和取钱线程,可以看到所有的取钱线程必须等存款线程存钱后才可以向下执行,而存款线程也必须等取钱线程取钱后才可以向下执行:

package Synchronized;

public class DrawTest {
    public static void main(String[] args) {
        // 创建一个账户
        Account acct = new Account("12345", 0);
        new DrawThread("取钱", acct, 800).start();
        new DepositThread("存钱-1", acct, 800).start();
        new DepositThread("存钱-2", acct, 800).start();
        new DepositThread("存钱-3", acct, 800).start();
    }
}

运行该程序,可以看到存款者线程、取钱者线程交替执行的情形,每当存款者向账户中存入800元之后,取钱者线程立即从账户中取出这笔钱。存款完成后账户余额总是800元,取钱结束后账户余额总是0元。
程序最后被阻塞无法继续向下执行,这是因为3个存款者线程共有300次存款操作,但1个取钱者线程只有100次取钱操作,所以程序最后被阻塞!

1.2 使用Condition控制线程通信

如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait()、notify()、notifyAll()方法进行线程通信了。
当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。
Condition将同步监视器方法(wait()、notify()和notifyAll())分解成截然不同的对象,以便通过将这些对象与Lock对象组合使用,为每个对象提供多个等待集(wait-set)。在这种情况下,Lock替代了同步方法或同步代码块,Condition替代了同步监视器的功能。
Condition实例被绑定在一个Lock对象上。要获得特定Lock实例的Condition实例,调用Lock对象的newCondition()方法即可。Condition类提供了如下3个方法:

  • await():类似于隐式同步监视器上的wait()方法,导致当前线程等待,直到其他线程调用该Condition的signal()方法或signalAll()方法来唤醒该线程。该await()方法有更多变体,如long awaitNanos(long nanosTimeout)、void awaitUninterruptibly()、awaitUntil(Date deadline)等,可以完成更丰富的等待操作。
  • signal():唤醒在此Lock对象上等待的单个线程。如果所有线程都在该Lock对象上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该Lock对象的锁定后(使用await()方法),才可以执行被唤醒的线程。
  • signalAll():唤醒在此Lock对象上等待的所有线程。只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。

下面程序中Account使用Lock对象来控制同步,并使用Condition对象来控制线程的协调运行。

package condition;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Account {
    // 显式定义Lock对象
     private final Lock lock=new ReentrantLock();
    // 获得指定Lock对象对应的Condition
     private final Condition cond=lock.newCondition();
    // 封装账户编号、账户余额两个Field
    private String accountNo;
    private double balance;
    //标识账户中是否已有存款的旗标
    private boolean flag=false;
    public Account(){}
    // 构造器
    public Account(String accountNo , double balance) {
        this.accountNo=accountNo;
        this.balance=balance;
    }

    // 省略accountNo的setter和getter方法
    public String getAccountNo() {
        return this.accountNo;
    }

    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }
    // 因为账户余额不允许随便修改,所以只为balance提供getter方法
    public double getBalance() {
        return this.balance;
    }
    public void draw(double drawAmount) {
        // 加锁
         lock.lock();
         try {
            // 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
            if (!flag) {cond.await();}
            else {
                // 执行取钱操作
                System.out.println(Thread.currentThread().getName()
                        + " 取钱:" +  drawAmount);
                balance -=drawAmount;
                System.out.println("账户余额为:" + balance);
                // 将标识账户是否已有存款的旗标设为false
                flag=false;
                // 唤醒其他线程
                 cond.signalAll();      
                }
            }catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            // 使用finally块来释放锁
            finally{lock.unlock();}
    }
    public void deposit(double depositAmount) {
        lock.lock();
        try {
            // 如果flag为真,表明账户中已有人存钱进去,存钱方法阻塞
            if (flag) {          //①
                cond.await();
            } else {
                // 执行存款操作
                System.out.println(Thread.currentThread().getName()
                        + " 存款:" + depositAmount);
                balance += depositAmount;
                System.out.println("账户余额为:" + balance);
                // 将表示账户是否已有存款的旗标设为true
                flag = true;
                // 唤醒其他线程
                cond.signalAll();      }
            }catch(InterruptedException ex) {
                ex.printStackTrace();
            }
        // 使用finally块来释放锁
         finally{lock.unlock();}    
    }
    //此处省略了hashCode()和equals()方法
    public int hashCode() {
        return accountNo.hashCode();
    }

    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj != null && obj.getClass() == Account.class) {
            Account target = (Account)obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
}

显式地使用Lock对象来充当同步监视器,则需要使用Condition对象来暂停、唤醒指定线程。

1.3 使用阻塞队列(BlockingQueue)控制线程通信

Java 5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途并不是作为容器,而是作为线程同步的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。
程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。
BlockingQueue提供如下两个支持阻塞的方法。

  • put(E e):尝试把E元素放入BlockingQueue中,如果该队列的元素已满,则阻塞该线程。
  • take():尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。

BlockingQueue继承了Queue接口,当然也可使用Queue接口中的方法。这些方法归纳起来可分为如下3组。

  • 在队列尾部插入元素。包括add(E e)、offer(E e)和put(E e)方法,当该队列已满时,这3个方法分别会抛出异常、返回false、阻塞队列。
  • 在队列头部删除并返回删除的元素。包括remove()、poll()和take()方法。当该队列已空时,这3个方法分别会抛出异常、返回false、阻塞队列。
  • 在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回false。

BlockingQueue包含如下5个实现类:

  • ArrayBlockingQueue:基于数组实现的BlockingQueue队列。
  • LinkedBlockingQueue:基于链表实现的BlockingQueue队列。
  • PriorityBlockingQueue:它并不是标准的阻塞队列。与前面介绍的PriorityQueue类似,该队列调用remove()、poll()、take()等方法取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。PriorityBlockingQueue判断元素的大小即可根据元素(实现Comparable接口)的本身大小来自然排序,也可使用Comparator进行定制排序。
  • SynchronousQueue:同步队列。对该队列的存、取操作必须交替进行。
  • DelayQueue:它是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现。不过, DelayQueue要求集合元素都实现Delay接口(该接口里只有一个long getDelay()方法), DelayQueue根据集合元素的getDalay()方法的返回值进行排序。

下面以ArrayBlockingQueue为例介绍阻塞队列的功能和用法。下面先用一个最简单的程序来测试BlockingQueue的put()方法。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class BlockingQueueTest
{
    public static void main(String[] args)
            throws Exception
    {
        // 定义一个长度为2的阻塞队列
        BlockingQueue<String> bq=new ArrayBlockingQueue<>(2);
        bq.put("Java");//与bq.add("Java"、bq.offer("Java")相同
        bq.put("Java");//与bq.add("Java"、bq.offer("Java")相同
        bq.put("Java");//① 阻塞线程    
     }
}

上面程序先定义一个大小为2的BlockingQueue,程序先向该队列中放入2个元素,此时队列还没有满,两个元素都可以放入,因此使用put()、add()和offer()方法效果完全一样。当程序试图放入第三个元素时,如果使用put()方法尝试放入元素将会阻塞线程,如上面程序①号代码所示。如果使用add()方法尝试放入元素将会引发异常;如果使用offer()方法尝试放入元素则会返回false,元素不会被放入。
与此类似的是,在BlockingQueue已空的情况下,程序使用take()方法尝试取出元素将会阻塞线程;使用remove()方法尝试取出元素将引发异常;使用poll()方法尝试取出元素将返回false,元素不会被删除。
掌握了BlockingQueue阻塞队列的特性之后,下面程序就可以利用BlockingQueue来实现线程通信了:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

class Producer extends Thread {
    private BlockingQueue<String> bq;
    public Producer(BlockingQueue<String> bq) {
        this.bq=bq;
    }
    public void run() {
        String[] strArr=new String[]{
            "Java",
            "Struts",
            "Spring"
        };
        for (int i=0 ; i < 999999999 ; i++ ) {
            System.out.println(getName() + "生产者准备生产集合元素!");
            try {
                Thread.sleep(200);
                // 尝试放入元素,如果队列已满,则线程被阻塞
                 bq.put(strArr[i % 3]);
            } catch (Exception ex){ex.printStackTrace();}
                System.out.println(getName() + "生产完成:" + bq);
        }
    }
}
class Consumer extends Thread {
    private BlockingQueue<String> bq;
    public Consumer(BlockingQueue<String> bq) {
    this.bq=bq;
}
public void run() {
    while(true) {
        System.out.println(getName() + "消费者准备消费集合元素!");
        try {
            Thread.sleep(200);
            // 尝试取出元素,如果队列已空,则线程被阻塞
            bq.take();
            } catch (Exception ex) {
            ex.printStackTrace();
            }
            System.out.println(getName() + "消费完成:" + bq);
        }
    }
}
public class BlockingQueueTest2 {
    public static void main(String[] args) {
        // 创建一个容量为1的BlockingQueue
        BlockingQueue<String> bq=new ArrayBlockingQueue<>(1);
        // 启动3个生产者线程
        new Producer(bq).start();
        new Producer(bq).start();
        new Producer(bq).start();
        // 启动一个消费者线程
        new Consumer(bq).start();
    }
}

上面程序启动了3个生产者线程向BlockingQueue集合放入元素,启动了1个消费者线程从Blocking Queue集合取出元素。本程序的BlockingQueue集合容量为1,因此3个生产者线程无法连续放入元素,必须等待消费者线程取出一个元素后,3个生产者线程的其中之一才能放入一个元素。
3个生产者线程都想向BlockingQueue中放入元素,但只要其中一个线程向该队列中放入元素之后,其他生产者线程就必须等待,等待消费者线程取出BlockingQueue队列里的元素。

2.线程组合未处理的异常

Java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。对线程组的控制相当于同时控制这批线程。用户创建的所有线程都属于指定线程组,如果程序没有显式指定线程属于哪个线程组,则该线程属于默认线程组。在默认情况下,子线程和创建它的父线程处于同一个线程组内,例如A线程创建了B线程,并且没有指定B线程的线程组,则B线程属于A线程所在的线程组。
一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组。
Thread类提供了如下几个构造器来设置新创建的线程属于哪个线程组:

  • Thread(ThreadGroup group, Runnable target):以target的run()方法作为线程执行体创建新线程,属于group线程组。
  • Thread(ThreadGroup group, Runnable target, String name):以target的run()方法作为线程执行体创建新线程,该线程属于group线程组,且线程名为name。
  • Thread(ThreadGroup group, String name):创建新线程,新线程名为name,属于group线程组。

因为中途不可改变线程所属的线程组,所以Thread类没有提供setThreadGroup()方法来改变线程所属的线程组,但提供了一个getThreadGroup()方法来返回该线程所属的线程组,getThreadGroup()方法的返回值是ThreadGroup对象,表示一个线程组。ThreadGroup类提供了如下两个简单的构造器来创建实例:

  • ThreadGroup(String name):以指定的线程组名字来创建新的线程组。
  • ThreadGroup(ThreadGroup parent, String name):以指定的名字、指定的父线程组创建一个新线程组。

上面两个构造器在创建线程组实例时都必须为其制定一个名字,也就是说,线程组总会有一个字符串类型的名字,改名字可通过调用ThreadGroup的getName()方法获取,但不允许改变线程组的名字。
ThreadGroup类提供了如下几个常用的方法来操作整个线程组里的所有线程。

  • int activeCount():返回此线程组中活动线程的数目。
  • interrupt():中断此线程组中的所有线程。
  • isDaemon():判断该线程组是否是后台线程组。
  • setDaemon(boolean daemon):把该线程组设置成后台线程组。后台线程组具有一个特征——当后台线程组的最后一个线程执行结束或最后一个线程被销毁后,后台线程组将自动销毁。
  • setMaxPriority(int pri):设置线程组的最高优先级。

下面程序创建了几个线程,它们分别属于不同的线程组,程序还将一个线程组设置成后台线程组。

class MyThread extends Thread {
    // 提供指定线程名的构造器
    public MyThread(String name) {
        super(name);
    }
    // 提供指定线程名、线程组的构造器
    public MyThread(ThreadGroup group , String name) {
        super(group, name);
    }
    public void run() {
        for (int i=0; i < 20 ; i++ ) {
            System.out.println(getName() + " 线程的i变量" + i);
        }
    }
}
public class ThreadGroupTest {
    public static void main(String[] args) {
        // 获取主线程所在的线程组,这是所有线程默认的线程组
        ThreadGroup mainGroup=Thread.currentThread().getThreadGroup();
        System.out.println("主线程组的名字:"+ mainGroup.getName());
        System.out.println("主线程组是否是后台线程组:"+ mainGroup.isDaemon());
        new MyThread("主线程组的线程").start();
        ThreadGroup tg=new ThreadGroup("新线程组");
        tg.setDaemon(true);
        System.out.println("tg线程组是否是后台线程组:"+ tg.isDaemon());
        MyThread tt=new MyThread(tg , "tg组的线程甲");
        tt.start();
        new MyThread(tg , "tg组的线程乙").start();
    }
}

ThreadGroup内还定义了一个很有用的方法:void uncaughtException(Thread t, Throwable e),该方法可以处理该线程组内的任意线程所抛出的未处理异常。
从Java 5开始,Java加强了线程的异常处理,如果线程执行过程中抛出了一个未处理异常,JVM在结束该线程之前会自动查找是否有对应的Thread.UncaughtExceptionHandler对象,如果找到该处理器对象,则会调用该对象的uncaughtException(Thread t, Throwable e)方法来处理该异常。
Thread.UncaughtExceptionHandler是Thread类的一个静态内部接口,该接口内只有一个方法:void uncaughtException(Thread t, Throwable e),该方法中的t代表出现异常的线程,而e代表该线程抛出的异常。
Thread类提供了如下两个方法来设置异常处理器:

  • static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):为该线程类的所有线程实例设置默认的异常处理器。
  • setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):为指定的线程实例设置异常处理器。

ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,所以每个线程所属的线程组将会作为默认的异常处理器。当一个线程抛出未处理异常时,JVM会首先查找该异常对应的异常处理器(setUncaughtExceptionHandler()方法设置的异常处理器),如果找到该异常处理器,则将调用该异常处理器处理该异常;否则,JVM将会调用该线程所属的线程组对象的uncaughtException()方法来处理该异常。线程组处理异常的默认流程如下:

  • 如果该线程组有父线程组,则调用父线程组的uncaughtException()方法来处理该异常。
  • 如果该线程实例所属的线程类有默认的异常处理器(由setDefaultUncaughtExceptionHandler()方法设置的异常处理器),那么就调用该异常处理器来处理该异常。
  • 如果该异常对象是ThreadDeath的对象,则不做任何处理;否则,将异常跟踪栈的信息打印到System.err错误输出流,并结束该线程。

下面程序为主线程设置了异常处理器,当主线程运行抛出未处理异常时,该异常处理器将会起作用:

// 定义自己的异常处理器
class MyExHandler implements Thread.UncaughtExceptionHandler
{
    //实现uncaughtException()方法,该方法将处理线程的未处理异常
    public void uncaughtException(Thread t, Throwable e)
    {
        System.out.println(t + " 线程出现了异常:" + e);
    }
}
public class ExHandler
{
    public static void main(String[] args)
    {
        // 设置主线程的异常处理器
        Thread.currentThread().setUncaughtExceptionHandler(new MyExHandler());
        int a=5 / 0;       //①
        System.out.println("程序正常结束!");
    }
}

上面程序的主方法中粗体字代码为主线程设置了异常处理器,而①号代码处将引发一个未处理异常,则该异常处理器会负责处理该异常。运行该程序,会看到如下输出:

Thread[main,5,main] 线程出现了异常:java.lang.ArithmeticException: / by zero

从上面程序的执行结果来看,虽然程序中粗体字代码指定了异常处理器对未捕获的异常进行处理,而其该异常处理器也确实起作用了,但程序依然不会正常结束,这说明异常处理器与通过catch捕获异常是不同的——当使用catch捕获异常时,异常不会向上传递给上一级调用者;但使用异常处理器对异常进行处理之后,异常依然会传递给上一级调用者。

3.编程练习

3.1 创建自己的任务定时器

在实际程序中,我们可以调用java.util类中的Date和Timer类来实现对线程任务的定时,使其在特定的时间开始运行。本实例就是一个简单的任务定时器,它可以测定各线程任务开始的时间。

1.

新建项目TimerThread,并在其中创建一个TimerThread.java文件。在该类的主方法中创建一个定时器类Timer来完成对线程任务的定时功能:

package TimerThread;

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

public class TimerThread {
    public static void main(String[] args) {        // Java程序主入口处
        Timer timer = new Timer();                  // 创建定时器类
        TimerTask tt1 = new MyTask(1);
        timer.schedule(tt1, 200);                   // 0.2秒后执行任务
        TimerTask tt2 = new MyTask(2);
        timer.schedule(tt2, 500, 1000);        // 0.5秒后执行任务并每个1秒执行一次
        TimerTask tt3 = new MyTask(3);
        Date date = new Date(System.currentTimeMillis() + 1000);
        timer.schedule(tt3, date);                  // 在指定时间1秒后执行任务
        try {
            Thread.sleep(3000);                     // 休眠3秒
        } catch (InterruptedException e) {          // 捕获拦截异常
            System.out.println("出现错误:" + e.getMessage());
        }
        timer.cancel();                             // 终止定时器取消定时器中的任务
        System.out.println("任务定时器已经被取消");
    }
}
class MyTask extends TimerTask {                    // 继承时间任务类执行任务
    private int taskID = 0;                         // 任务编号
    public MyTask(int id) {                         // 带参数的构造方法进行初始化
        this.taskID = id;
    }
    public void run() {                             // 实现TimerTask中的方法
        System.out.println("开始执行我的第" + this.taskID + "个任务 ,执行时间为"
                + new Date().toString());
    }
}

java.util.Timer和java.util.TimerTask统称为Java计时器框架,它们使程序员可以很容易地计划简单的任务(注意这些类也可用于J2ME中)。在Java2SDK、StandardEdition、Version1.3中引入这个框架之前,开发人员必须编写自己的调度程序,这需要花费很大精力来处理线程和复杂的Object.wait()方法。不过,Java计时器框架没有足够的能力来满足许多应用程序的计划要求。甚至一项需要在每天同一时间重复执行的任务,也不能直接使用Timer来计划,因为在下令开始和结束时会出现时间跳跃。

Timer有两种执行任务的模式,最常用的是schedule,它可以以两种方式执行任务:
● 在某个时间(Data)。
● 在某个固定的时间之后(int delay)。
这两种方式都可以指定任务执行的频率。

猜你喜欢

转载自blog.csdn.net/tonydz0523/article/details/87388706