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

2024-04-17 20:48

本文主要是介绍《疯狂java讲义》学习(45):线程通信线程组,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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);// 将标识设为falseflag = 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);// 标识账户已经有存款旗标设为trueflag = 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对象对应的Conditionprivate final Condition cond=lock.newCondition();// 封装账户编号、账户余额两个Fieldprivate 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);// 将标识账户是否已有存款的旗标设为falseflag=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);// 将表示账户是否已有存款的旗标设为trueflag = 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的BlockingQueueBlockingQueue<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)。
这两种方式都可以指定任务执行的频率。

这篇关于《疯狂java讲义》学习(45):线程通信线程组的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/912815

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06