第14章 线程
线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。
线程有就绪、阻塞和运行三种基本状态。线程的周期包括新建、就绪、运行、阻塞和死亡。线程是程序中一个单一的顺序控制流程。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
本章介绍线程的启动和停止、线程的优先级、线程的互斥与协作、线程池以及运用线程制作定时器和解决线程的死锁等。
实例119 启动和停止线程
Java中实现多线程有两种方法:一种是继承Thread类;另一种是实现Runnable接口。本实例介绍运用这两种方式创建线程并启动各停止线程。
技术要点
实现启动和停止线程的技术要点如下:
• 线程类继承Thread类的语法:
(public) class类名extends Thread{
public void run(){//这里写线程的内容}}
线程类实现java.lang.Runnable类的语法:
(public) class类名implements Runnable{
public void run(){//这里写线程的内容}}
• 一般鼓励使用第二种方法,Java只允许单一继承,但允许实现多个接口。第二种方法更加灵活。
• 调用Thread类的start()方法启动一个线程,调用方法后,线程准备启动,当获得CPU资源时,start()方法将自动调用run()方法,线程才真正开始运行。
• 停止线程的方式有很多,可以使用Thread类的interrupt()方法停止线程;可以设置一个布尔类型的变量flag=true,当要结束进程时,将flag设置为false;也可以使用stop()方法,不过该方法已经废弃,但可以用在死锁。
实现步骤
(1)新建一个类名为TextThreadStartOrStop.java。
(2)代码如下所示:
package com.zf.s14; //创建一个包 import java.util.Date; //引入类 class OneThread extends Thread { //继承java.lang.Thread类定义线程 private boolean running = false; //标记线程是否需要运行 public void start() { //覆盖了父类的start方法 this.running = true; //将running置为true,表示线程需要运行 super.start(); } public void run() { int i = 0; try { while (running) { //如果running为真,说明线程还可以继续运行 System.out.println("线程 " + i++); Thread.sleep(200); //sleep方法将当前线程休眠 } } catch (Exception e) { //捕获异常 } System.out.println("线程结束..."); } public void setRunning(boolean running) { //设置线程 this.running = running; } public void startThreadA(){ //启动ThreadA线程 System.out.println("启动线程..."); this.start(); } public void stopThreadA(){ //停止ThreadA线程 System.out.println("结束线程..."); this.setRunning(false); }
} class TwoThread implements Runnable { //实现java.lang.Runnable接口定义线程 private Date runDate; //线程被运行的时刻 public void run() { System.out.println("线程启动方法..."); this.runDate = new Date(); System.out.println("启动时间:"+runDate); } } public class TextThreadStartOrStop { //操作线程启动和停止的类 public void startOne() { OneThread threadOne=new OneThread(); //创建实例 threadOne.startThreadA(); //启动ThreadA线程 try { Thread.sleep(1000); //当前线程休眠1秒 } catch (InterruptedException e) { e.printStackTrace(); } threadOne.stopThreadA(); //停止ThreadA线程 } public void startTwo() { //开始第二个 Runnable tb = new TwoThread(); //创建实例 Thread threadB = new Thread(tb); //将实例放入线程中 threadB.start(); //start方法启动线程 } public static void main(String[] args) { //Java程序主入口处 TextThreadStartOrStop test = new TextThreadStartOrStop(); //实例化对象 test.startOne(); //调用方法 test.startTwo(); } }
(3)运行结果如下所示:
启动线程... 线程 0 线程 1 线程 2 线程 3 线程 4 结束线程... 线程启动方法... 启动时间:Mon Apr 20 11:43:50 CST 2009 线程结束...
源程序解读
(1)OneThread类继承Thread类来实现线程功能,TwoThread类实现Runnable接口来实现线程功能。这两种方式实现线程功能,都需要实现run()方法。启动线程使用Thread类的start()方法,如果类是继承Thread类,则真接调用对象的start()方法;如果类实现的是Runnable()接口,需要将对象封装成Thread类,再调用封装后对象的start()方法。
(2)如果一个类需要实现多个线程的功能,只能通过实现Runnable接口的方式把自己定义为线程类。由于Java只允许单一继承,继承Thread类是不能实现这个功能的。
(3)OneThread类定义一个标识判断线程是否需要运行。当线程运用start()方法启动时,将该标识设置为真,如果为真则一直循环,如果设置为假则结束循环,线程也就停止了。因此,在需要线程停止时运用stopThreadA()方法调用setRunning()方法将标识设置为假便可。该类中的run()方法调用线程的sleep()方法是使当前线程进入休眠状态。
实例120 多线程同步方法
除了可以对代码块进行同步外,对函数也可以实现同步。当一个方法被关键字synchronized声明之后,就只允许一个线程来操作这个方法。但是值得注意的是,“一个线程”指的是一次只能让一个线程运行。这个方法就称为同步方法。
技术要点
实现多线程同步方法的技术要点如下:
• 同步方法可以控制对类成员变量的访问。每个类的实例对象都对应着一把锁,每个同步方法都必须在获得该类实例对象的锁才能执行,方法一旦被执行,就独占该锁,而其他的线程只能等待,直到从该方法返回时才将锁释放。
• 在同一类中,只要有可能访问类成员变量的方法均被声明为synchronized,这些synchronized方法可以在多个线程之间同步。当有一个线程进入了有synchronized修饰的方法时,其他线程就不能进入同一个对象使用synchronized来修饰所有的方法,直到第一个线程执行完它所进入的synchronized修饰的方法为止。
实现步骤
(1)新建一个类名为ThreadDemo.java。
(2)代码如下所示:
packagecom.zf.s14; public class ThreadDemo { public static void main(String[] args) { ThreadTest1 t1 = new ThreadTest1(); //创建ThreadTest1类的实例对象 new Thread(t1).start(); //创建线程并启动它 System.out.println(t1.call()); //调用ThreadTest1类的同步方法call() } } class ThreadTest1 implements Runnable { private int x; private int y; //定义ThreadTest1的同步方法 public synchronized void run() { //重写Runnable接口的run(),并声明成synchronized for (int i = 0; i < 4; i++) { x++; y++; try { Thread.sleep(200); //当前运行的线程休眠200毫秒 } catch (InterruptedException e) { System.out.println("线程出错了"); } System.out.println(Thread.currentThread().getName() + " x=" + x + ",y=" + y + " " + i); } } public synchronized String call() { //自定义方法,并声明成synchronized String name = Thread.currentThread().getName(); return "Hellow " + name; } }
(3)运行结果如下所示:
Thread-0 x=1,y=1 0 Thread-0 x=2,y=2 1 Thread-0 x=3,y=3 2 Thread-0 x=4,y=4 3 Thread-1 x=5,y=5 0 Thread-1 x=6,y=6 1 Thread-1 x=7,y=7 2 Thread-1 x=8,y=8 3 Hellow main
源程序解读
从这段代码的结果中我们可以看出,Thread-0和Thread-1都完整地访问了共享资源,从代码的打印顺序上可以看出Thread-0访问完run()后,Thread-1才进入synchronized run()进行数据操作。在Thread-0休眠的时间内,Thread-1也没有进入运行状态。直到Thread-0从synchronized run()方法中返回把锁交给Thread-1。
而synchronized call()创建的线程没有访问它,是在main()即程序的主线程中调用。所以在没在获得锁的情况下,也只是等待。这就说明了线程的同步很好地解决了多个线程共同访问同一片存储空间发生冲突的问题。
实例121 取钱存钱(线程同步互斥)
本实例模拟银行ATM(存)取款机,分析一个用户往ATM(存)取款机中存钱时,另一个用户在另一个地方从中取钱,存在操作前后不一致,程序采用线程同步与非同步方法解决的区别。
技术要点
运用线程的互斥实现模拟银行ATM(存)取款机的技术要点如下:
• 在类的方法声明中使用synchronized关键字可以保证在同一时刻只有一个线程能够进入该方法。
• synchronized可以出现在代码块中,表示同一时刻只有一个线程能够进入该段代码。
实现步骤
(1)创建一个类名为TextSynchronized.java。
(2)代码如下所示:
package com.zf.s14; //创建一个包 class Bank{ private double current_money = 500.0d; //存入银行的钱数 public void putNonSynDeposit(double putMoney){ //存钱没有采用同步机制 System.out.println("没有采用线程同步机制,钱为" + this.current_money + "; 存钱: " + putMoney); System.out.println("等待中......"); //存钱时先等待300毫秒 try { Thread.sleep(300); //线程休眠 } catch (Exception e) { //捕获异常 e.printStackTrace(); } System.out.println("等待完毕进行存钱操作..."); this.current_money = this.current_money + putMoney; System.out.println("没有采用线程同步机制,当前钱数为" + this.current_money+"元"); } public void takeNonSynWithdraw(double takeMoney){ //取钱没有同步机制 System.out.println("没有采用线程同步机制,钱为" + this.current_money + "; 取钱: " + takeMoney); System.out.println("等待中......"); //取钱时先等待500毫秒 try { Thread.sleep(500); //线程休眠 } catch (Exception e) { //捕获异常 e.printStackTrace(); } System.out.println("等待完毕进行取钱操作..."); this.current_money = this.current_money - takeMoney; System.out.println("没有采用线程同步机制,当前钱数为" + this.current_money+"元"); } public synchronized void putSynDeposit(double putMoney){ //存钱有同步机制 System.out.println("采用线程同步机制,钱为" + this.current_money + "; 存钱: " + putMoney); System.out.println("等待中......"); //存钱时先等待300毫秒 try { Thread.sleep(300); //线程休眠 } catch (Exception e) { //捕获异常 e.printStackTrace(); } System.out.println("等待完毕进行存钱操作..."); this.current_money = this.current_money + putMoney; System.out.println("采用线程同步机制,当前钱数为" + this.current_money+"元"); } public synchronized void takeSynWithdraw(double takeMoney){//取钱有同步机制 System.out.println("采用线程同步机制,钱为" + this.current_money + "; 取钱: " + takeMoney); System.out.println("等待中......"); //取钱时先等待500毫秒 try { Thread.sleep(500); //线程休眠 } catch (Exception e) { //捕获异常 e.printStackTrace(); } System.out.println("等待完毕进行取钱操作..."); this.current_money = this.current_money - takeMoney; System.out.println("采用线程同步机制,当前钱数为" + this.current_money+"元"); } } class AccessBank extends Thread{ //继承Thread类实现线程方法 private Bank bank = null; //待访问的账号对象 private String method = ""; //访问账号的方法 public AccessBank(Bank account, String method){ //构造方法进行初始化 this.method = method; this.bank = account; } public void run(){ //实现Thread的方法 if (method.equals("putNonSynDeposit")){ //不同参数调用不同的方法 bank.putNonSynDeposit(500.0); } else if (method.equals("takeNonSynWithdraw")){ bank.takeNonSynWithdraw(200.0); } else if (method.equals("putSynDeposit")){ bank.putSynDeposit(500.0); } else if (method.equals("takeSynWithdraw")){ bank.takeSynWithdraw(200.0); } } } public class TextSynchronized { //操作展示线程同步与非同步区别的类 public static void main(String[] args) { //Java程序主入口处 Bank bank = new Bank();//实例化对象 System.out.println("1.未采用同步机制时..."); //存钱没有采用同步机制 Thread putMoney = new AccessBank(bank, "putNonSynDeposit"); //取钱没有采用同步机制 Thread takeMoney = new AccessBank(bank, "takeNonSynWithdraw"); putMoney.start(); //启动线程 takeMoney.start(); //启动线程 try { putMoney.join(); //等待两线程运行结束 takeMoney.join(); } catch (Exception e) { //捕获异常 System.out.println("两线程运行出错:"+e.getMessage()); } System.out.println(); bank = new Bank(); System.out.println("2.采用同步机制时..."); putMoney = new AccessBank(bank, "putSynDeposit"); //存钱有同步机制 takeMoney =new AccessBank(bank,"takeSynWithdraw"); //取钱有同步机制 putMoney.start(); //启动线程 takeMoney.start(); //启动线程 } }
(3)运行结果如下所示:
1.未采用同步机制时... 没有采用线程同步机制,钱为500.0; 存钱: 500.0 等待中...... 没有采用线程同步机制,钱为500.0; 取钱: 200.0 等待中...... 等待完毕进行存钱操作... 没有采用线程同步机制,当前钱数为1000.0元 等待完毕进行取钱操作... 没有采用线程同步机制,当前钱数为800.0元 2.采用同步机制时... 采用线程同步机制,钱为500.0; 存钱: 500.0 等待中...... 等待完毕进行存钱操作... 采用线程同步机制,当前钱数为1000.0元 采用线程同步机制,钱为1000.0; 取钱: 200.0 等待中...... 等待完毕进行取钱操作... 采用线程同步机制,当前钱数为800.0元
源程序解读
(1)Java中的线程同步可以通过synchronized关键字来实现,synchronized既可以修饰变量(必须是类对象),又可以修饰方法。被synchronized修饰的变量和方法同时只能有一个线程来操作,其他线程只能等待,以此达到线程同步的目的。synchronized结构:synchronized(obj){...//操作代码}。
(2)Bank类的putNonSynDeposit()方法实现存钱功能没有使用同步互斥技术。允许多个用户同时存钱或取钱,这样就会出错。takeNonSynWithdraw()方法实现取钱功能没有使用同步互斥技术,这样也可能由于多个用户同时操作会出错。
(3)putSynDeposit()方法实现使用同步互斥存钱的功能,即在同一时刻只允许一个用户(线程)进行操作,当该线程进入同步方法后对象被锁,其他线程因为没有对象锁而不能访问该对象的该方法,只有当持有对象锁的线程释放锁时,才有机会访问该方法。也就是当多个用户同时存钱时也都是顺序执行的,不会同时修改钱数。takeSynWithdraw()方法实现同步互斥取钱的功能。当多个用户同时取钱时是按顺序执行的,也不会同时修改钱数。
(4)从运行结果中可以看出:当没有使用同步互斥方法时,银行中钱总数为500,存入500再取钱时发现钱还是500,即后存入的钱没有入账。这是因为用户在存钱时另一用户对该账号进行取钱。当使用同步互斥方法后,用户向银行存入钱时,取钱的用户只能够等待,当存钱结束后,另一用户才能取钱,这样保证了钱数前后的一致。
实例122 谁唤醒了我(线程沉睡和唤醒)
线程沉睡(sleep)并不会让线程释放它所持有的同步锁,而且在这期间也不会阻碍其他线程的运行。唤醒(interrupt)可以将沉睡或阻塞的线程唤醒。本实例介绍运用线程的沉睡和唤醒判断来设置线程的暂停和运行。
技术要点
运用线程沉睡和唤醒实现谁唤醒我的技术要点如下:
• 线程唤醒可以使当前线程沉睡一段时间,在这段时间内不会有时间轮流到该线程上,直到过完这段时间,线程又重新运行。
• 线程唤醒可以使执行了sleep操作的线程或执行了wait操作或者join操作的线程唤醒。线程沉睡要指定沉睡的时间,如果对该线程执行interrupt操作,线程可以提早继续运行。
实现步骤
(1)创建一个类名为TextSleepAndInterrupt.java。
(2)代码如下所示:
package com.zf.s14; //创建一个包 import java.text.DateFormat; //引入类 import java.text.SimpleDateFormat; import java.util.Date; public class TextSleepAndInterrupt extends Thread { //操作线程沉睡与唤醒的类 private DateFormat dateFormat = new SimpleDateFormat("HH-mm-ss:SSSS"); public void run() { System.out .println(dateFormat.format(new Date()) + getName() + " 沉睡3秒钟"); try { sleep(3000); //线程休眠3秒 } catch (InterruptedException e) { //捕获唤醒异常 System.out.println(getName() + dateFormat.format(new Date()) + getName() + " 唤醒异常:" + e.getMessage()); } System.out.print(dateFormat.format(new Date()) + " 沉睡期间是否唤醒?"); try { sleep(2000); //线程休眠2秒 } catch (InterruptedException e) { //捕获唤醒异常 System.out.println(getName() + dateFormat.format(new Date()) + getName() + " 唤醒异常:" + e.getMessage()); } System.out.println(!isAlive()); //线程是否激活,false表示不激活 interrupt();//唤醒线程 System.out.print(dateFormat.format(new Date()) + "沉睡的我,是否唤醒?"); System.out.println(isAlive()); //线程是否激活 } public void getUp(){ Thread.currentThread().interrupt(); //唤醒当前线程 while (true) { if (Thread.currentThread().isInterrupted()) { //判断当前线程是否被唤醒 System.out.println(dateFormat.format(new Date())+"当前我是否被唤醒?" + Thread.currentThread().isInterrupted()); try { Thread.currentThread().sleep(2000); //线程休眠2秒 } catch (InterruptedException e) { //捕获唤醒异常 System.out.println(getName() + dateFormat.format(new Date()) + getName() + " 唤醒异常:" + e.getMessage()); } System.out.println(dateFormat.format(new Date())+"沉睡后是否被唤醒?" + Thread.currentThread().isInterrupted()); } } } public static void main(String[] args) { //Java程序主入口处 TextSleepAndInterrupt text=new TextSleepAndInterrupt();//实例化对象 text.start(); //启动线程 try { text.join(); //等待线程运行结束 } catch (InterruptedException e) { //捕获唤醒异常 System.out.println(" 唤醒异常:" + e.getMessage()); } text.getUp(); //调用方法判断是否唤醒 } }
(3)运行结果如下所示:
10-26-08:0218Thread-0 沉睡3秒钟 10-26-11:0218 沉睡期间是否唤醒?false 10-26-13:0218 沉睡的我,是否唤醒?true 10-26-13:0218 当前我是否被唤醒?true Thread-010-26-13:0218Thread-0 唤醒异常:sleep interrupted 10-26-13:0218 沉睡后是否被唤醒?false
源程序解读
(1)TextSleepAndInterrupt类继承Thread类必须要实现run()方法。在run()方法中调用Thread类的sleep()方法让线程休眠(暂停)指定的时间,如果出现异常则该线程被唤醒没有休眠。isAlive()方法表示线程是否是激活的,如果是激活的则返回真,否则返回假。
(2)getUp()方法通过currentThread()方法获得当前线程,interrupt()方法唤醒该线程, isInterrupted()方法判断线程是否被唤醒。当线程被唤醒时如果调用休眠sleep()方法则会抛出唤醒异常。如果线程调用休眠sleep()方法没有在指定时间内休眠完毕就调用interrupt()方法,则该线程提前运行。
(3)在main()主方法中线程的join()方法是等待前一个或几个线程运行结束后再运行getUp()方法。
实例123 让步传文件(线程让步)
线程让步(yield)让当前运行的线程放弃其所占用的CPU时间片,以便让其他线程运行,用线程让步的目的是让线程能适当地轮转,但并不能保证达到此效果。因为即使当前线程放弃时间片,还有可能再次被Java虚拟机(JVM)选中。本实例介绍运用线程让步实现让步传文件。
技术要点
运用线程让步实现让步传文件的技术要点如下:
• 执行一次让步操作只让步一个时间片,当下次时间片轮到该线程时,该线程会继续运行。由于一个时间片非常短,最多几个毫秒,因此一次线程让步操作的效果微乎其微,甚至觉察不出。
• 线程让步适用于同级别的线程让步,且让步后不是转入阻塞状态,则是就绪状态,不抛出异常。
实现步骤
(1)创建一个类名为TextThreadYield.java。
(2)代码如下所示:
package com.zf.s14; //创建一个包 import java.text.DateFormat; //引入类 import java.text.SimpleDateFormat; import java.util.Date; import java.util.Vector; public class TextThreadYield { //操作线程让步的类 private Vector vector = new Vector(); //创建向量集合 //创建日期格式 private DateFormat dateFormat = new SimpleDateFormat("HH-mm-ss:SSSS"); private boolean isFlag = false; private class Yield extends Thread { //让步接收文件类 public Yield() { this.setName("接收文件"); //设置线程名称 this.setDaemon(true); //如果SendFile线程结束,则该线程自动结束 } public void run() { while (!isFlag) { //标识为真进行循环 try { Thread.sleep(100); //休眠100毫秒 }catch(InterruptedException e){ //捕获唤醒异常 System.out.println("唤醒异常:"+e.getMessage()); } if (vector.size() == 0) { //判断向量集合大小 System.out.println(dateFormat.format(new Date())+ "\t向量集 合中没有文件,执行yield操作"); Thread.yield(); //调用线程让步 } else { //移除文件获得对象 String ss = (String) vector.remove(0); System.out.println(dateFormat.format(newDate())+"\t取到文件,名为" + ss); } } } } private class SendFile extends Thread { //发送文件类 private String[] files = new String[] { "新闻文件", "国内旅游向导", "山水名 画欣赏", "发家致富说明" }; public SendFile() { this.setName("发送文件"); } public void run() { try { for (int i=0;i < files.length;i++){ //循环使线程休眠 Thread.sleep(201); //线程休眠 vector.add(files[i]); //添加文件 } Thread.sleep(100); //线程休眠 } catch (InterruptedException e) { //捕获唤醒异常 System.out.println("唤醒异常:"+e.getMessage()); } } } public static void main(String []args){ //Java程序主入口处 TextThreadYield text=new TextThreadYield(); //实例化对象 text.new Yield().start(); //实例对象启动线程 text.new SendFile().start(); } }
(3)运行结果如下所示:
11-15-03:0796 向量集合中没有文件,执行yield操作 11-15-03:0906 向量集合中没有文件,执行yield操作 11-15-04:0000 取到文件,名为新闻文件 11-15-04:0109 取到文件,名为国内旅游向导 11-15-04:0203 向量集合中没有文件,执行yield操作 11-15-04:0312 向量集合中没有文件,执行yield操作 11-15-04:0406 取到文件,名为山水名画欣赏 11-15-04:0500 向量集合中没有文件,执行yield操作 11-15-04:0625 取到文件,名为发家致富说明
源程序解读
(1)TextThreadYield类创建向量集合操作传送的文件,创建日期格式类对获得的时间格式化,声明一标识变量控制循环。其Yield内部类的构造方法设置线程的名称并设置其线程为守护线程。功能是如果SendFile类线程结束,则其线程也自动结束。类的run()方法判断向量集合中是否有元素,如果有则获取移除的对象并显示,否则调用线程的yield()让步方法,进行该线程的让步。
(2)SendFile类继承Thread类,在其构造方法中设置该线程的名称。run()方法利用循环使线程休眠并向向量集合中添加文件元素。设置线程的休眠时间以使Yield类线程进行时间让步。
(3)一般而言,线程的yield操作效果是非常小的,读者可以将Thread.yield();一行去掉或注释掉,看一下不用yield与用yield有什么区别。
实例124 爱子(守护线程)
Java虚拟机(JVM)的垃圾回收线程可以称为守护线程。守护线程是一种特殊的线程,它是否运行结束并不仅仅依赖于自己run()方法内的程序,还依赖于其他非守护线程。本实例介绍如何实现守护线程,并判断其何时停止。
技术要点
运用守护线程实现爱子的技术要点如下:
• 普通线程可以通过Thread类的setDaemon(布尔类型参数)方法设置为守护线程,参数为true时表示该线程为守护线程。任何线程都可以通过Thread类的isDaemon()方法判断是否为守护线程。
• 线程被运行后,Thread类的setDaemon(布尔类型参数)方法无效,即必须在调用方法start()之前调用setDaemon(布尔类型参数)方法,才能设置该线程为守护线程。
• 如果所有的非守护线程运行结束,无论守护线程有没有运行结束,都将不再运行。如果一个程序没有任何守护线程,则所有非守护线程运行结束后就退出。程序中启动的线程默认为非守护线程,但在守护线程中启动的线程都是守护线程。
实现步骤
(1)创建一个类名为TextThreadDaemon.java。
(2)代码如下所示:
package com.zf.s14; //创建一个包 class FatherThread extends Thread { //操作守护线程的类 public void run() { System.out.println("遇到危险时,父亲是否被保护 ? " + this.isDaemon()); System.out.println("在危险时,父亲要保护儿子"); Thread child = new ChildThread(); child.setDaemon(true); //设置子类为守护线程 child.start(); //启动线程 try { Thread.sleep(1000); //休眠1秒 } catch (InterruptedException e) { //捕获唤醒异常 System.out.println("唤醒异常:"+e.getMessage()); } finally { //内容总执行 System.out.println("在危险时,父亲没有被保护,但要保护儿子"); } System.out.println("父亲爱儿子..."); } } class ChildThread extends Thread { public void run() { System.out.println("儿子是否被保护?" + this.isDaemon()); System.out.println("一共有几个儿子"); int i = 0; try { while (i < 5) { //进行5次循环 System.out.println("儿子" + i++); Thread.sleep(200); //休眠0.2秒 } } catch (InterruptedException e) { //捕获唤醒异常 System.out.println("唤醒异常:"+e.getMessage()); } finally { //内容总执行 System.out.println("父亲已经把儿子保护好了..."); } System.out.println("危险结束"); } } public class TextThreadDaemon { //操作守护线程的类 public static void main(String[] args) { //Java程序主入口处 Thread father = new FatherThread(); //默认情况下父类是普通线程 father.start(); //启动父类线程 try { Thread.sleep(500); } catch (InterruptedException e) { //捕获唤醒异常 System.out.println("唤醒异常:"+e.getMessage()); } } }
(3)运行结果如下所示:
遇到危险时,父亲是否被保护? false 在危险时,父亲要保护儿子 儿子是否被保护?true 一共有几个儿子 儿子0 儿子1 儿子2 儿子3 儿子4 在危险时,父亲没有被保护,但要保护儿子 父亲爱儿子... 父亲已经把儿子保护好了... 危险结束
源程序解读
(1)FatherThread类继承Thread类必须实现Thread类的run()方法。在run()方法中获得ChildThread子类对象,将子类对象设置为守护线程并启动子类线程。线程休眠1秒。
(2)ChildThread类继承Thread类必须实现Thread类的run()方法。run()方法判断该类线程是否是守护线程,并根据条件循环5次让线程休眠。
(3)在main()主方法中创建FatherThread父类对象,默认情况下其为普通线程类,当调用start()方法启动线程时使子类变为守护线程类。
实例125 有始有终(线程连接)
线程连接(join)是线程间运行调度的一种操作,即线程需要等待其他线程运行结束后才能够开始,否则继续等待。本实例介绍在多个线程之间等待。当线程的前一个或几个线程执行结束后,该线程才继续执行。
技术要点
运用线程连接实现有始有终的技术要点如下:
• 线程连接有两种形式:join(参数)和join(),在join(参数)方法中参数表示等待的时间,单位是毫秒。若超出这个时间,则无论前线程是否执行完毕,该线程都会继续运行。如果时间为0,则表示无期限地等待。join()方法相当于join(0),表示无期限地等待。
• join操作与sleep操作一样都可以被interrupt操作唤醒。
实现步骤
(1)创建一个类名为TextThreadJoin.java。
(2)代码如下所示:
package com.zf.s14; //创建一个包 class Marry extends Thread { //测试是否能结婚的类 private String personName; //人员名称 private String personObj; //结婚对象 private boolean isMarry=false; //是否结婚 public Marry(String personName,String personObj) { //带参数构造方法进行初始化 this.personName=personName; this.personObj=personObj; } public void run() { int i = 0; try { while (i < 5) { int person=(int)Math.floor((Math.random()*10+1)); //获得随机数 if(person %2==0){ isMarry=true; //设置标识 }else{ isMarry=false; } if(!isMarry){ System.out.println(personName+"可以与"+personObj+person+"结婚"); }else{ System.out.println(personObj+person+"已经结婚,要有始有终"); } Thread.sleep(200); //线程休眠200毫秒 i++; } } catch (InterruptedException e) { //捕获唤醒异常 System.out.println("唤醒异常:"+e.getMessage()); } } } public class TextThreadJoin { //操作线程连接(结合)的类 public static void main(String[] args) { //Java程序主入口处 Thread m1 = new Marry("TOM","SUSAN"); //实例化对象 Thread m2 = new Marry("JIM", "LINGDA"); Thread m3 = new Marry("SUN","LILI"); System.out.println("测试是否能结婚,要保证有始有终"); m1.start(); //启动线程 try { m1.join(); //等待线程运行结束 } catch (InterruptedException e) { //捕获唤醒异常 System.out.println("唤醒异常:"+e.getMessage()); } m2.start(); try { m2.join(); //等待线程运行结束 } catch (InterruptedException e) { //捕获唤醒异常 System.out.println("唤醒异常:"+e.getMessage()); } m3.start(); try { m3.join(); //等待线程运行结束 } catch (InterruptedException e) { //捕获唤醒异常 System.out.println("唤醒异常:"+e.getMessage()); } System.out.println("匹配结束..."); } }
(3)运行结果如下所示:
测试是否能结婚,要保证有始有终 SUSAN1已经结婚,要有始有终 TOM可以与SUSAN6结婚 TOM可以与SUSAN8结婚 TOM可以与SUSAN6结婚 SUSAN1已经结婚,要有始有终 JIM可以与LINGDA6结婚 LINGDA9已经结婚,要有始有终 JIM可以与LINGDA2结婚 LINGDA9已经结婚,要有始有终 JIM可以与LINGDA10结婚 SUN可以与LILI10结婚 SUN可以与LILI10结婚 SUN可以与LILI6结婚 LILI7已经结婚,要有始有终 LILI3已经结婚,要有始有终 匹配结束...
源程序解读
(1)Marry类构造方法带参数初始化人员名称与要匹配的对象。其run()方法根据条件循环5次,获得随机整数并判断此整数是否是偶数,如果是则将标识变量设置为真表示已经结婚,否则为假表示未婚。
(2)在main()主方法中创建三个Marry线程类对象,并依次启动这三个线程类对象,在每个线程类对象启动后调用join()方法,用来等待该线程类运行结束。当运行结束后下一个线程对象才启动运行。
实例126 模拟下载文件(线程等待和通报)
wait与notify是Object类而非Thread类的两个操作。这两个操作只能在线程同步的时候有效,并且只能出现在synchronize()方法或者synchronize块中。本实例介绍运用wait与notify模拟实现下载文件并将下载的文件展示出来。
技术要点
运用wait与notify模拟实现下载文件的技术要点如下:
• 将显示文件的线程处于等待状态,如果下载线程下载了新的文件,就通报显示线程,显示线程再将文件显示出来。
• wait方法有两种常用的形式:wait(长整型参数)和wait()。前者是线程等待指定的毫秒数,超过则继续进行,如果参数为0则表示无限等待;wait()方法相当于wait(0),表示无限等待。
• notify方法也有两种形式:notify()和notifyAll()。notify()方法只通知一个等待该对象的线程,如果有多个,则根据线程调度挑选一个;notifyAll()方法通知所有等待该对象的线程。
实现步骤
(1)新建一个类名为TextWaitAndNotify.java。
(2)代码如下所示:
package com.zf.s14;//创建一个包 import java.text.DateFormat;//引入类 import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Random; public class TextWaitAndNotify {//操作wait()与notify()方法的类 private static boolean isDone=false;//标识线程是否停止 private static List fileList=new ArrayList();//创建集合列表对象 private static DateFormat dateFormat=new SimpleDateFormat("HH-mm-ss"); private class DownLoadFile extends Thread{//创建模拟下载文件类 private String[]file=new String[]{"time.txt","axis.jar","ajax in action. rar","post.zip"}; public void run(){//实现线程类的方法 for(int i=0;i<file.length;i++){ try { int time=new Random().nextInt(100)*5;//获得随机生成的秒数 System.out.println(dateFormat.format(new Date())+" 下载文件 "+file[i]+"用时"+time+" 毫秒. "); Thread.sleep(time);//线程休眠等待 } catch (Exception e) {//捕获异常 System.out.println("下载文件出错:"+e.getMessage()); } synchronized (fileList) {//实现同步 System.out.println(dateFormat.format(new Date())+" 下载线程 已下载文件"+file[i]); fileList.add(file[i]);//将文件添加到集合列表中 fileList.notify();//通报所有等待的fileList的线程 } } isDone=true;//重新设置标识 System.out.println(dateFormat.format(new Date())+" 下载线程 退出"); } } private class ShowFile extends Thread{ public void run(){//实现线程类的方法 while(!isDone){ synchronized (fileList) {//实现同步 try { fileList.wait(1000);//集合列表等待,1秒超时 } catch (Exception e) {//捕获异常 System.out.println("线程等待出现错误:"+e.getMessage()); } while(fileList.size()>0){//如果集合中存在文件 System.out.println(dateFormat.format(new Date())+" 显 示线程 显示文件"+fileList.remove(0)); } } } System.out.println(dateFormat.format(new Date())+" 显示线程 退出"); } } public static void main(String [Jargs){//Java程序主入口处 TextWaitAndNotify text=new TextWaitAndNotify();//实例化对象 text.new DownLoadFile().start();//实例化内部类并启动线程 text.new ShowFile().start(); } }
(3)运行结果如下所示:
16-22-36 下载文件time.txt用时30 毫秒. 16-22-36 下载线程 已下载文件time.txt 16-22-36 下载文件axis.jar用时220 毫秒. 16-22-36 显示线程 显示文件time.txt 16-22-36 下载线程 已下载文件axis.jar 16-22-36 下载文件ajax in action.rar用时265 毫秒. 16-22-36 显示线程 显示文件axis.jar 16-22-36 下载线程 已下载文件ajax in action.rar 16-22-36 下载文件post.zip用时330 毫秒. 16-22-36 显示线程 显示文件ajax in action.rar 16-22-37 下载线程 已下载文件post.zip 16-22-37 显示线程 显示文件post.zip 16-22-37 下载线程 退出 16-22-38 显示线程 退出
源程序解读
(1)TextWaitAndNotify类的内部类DownLoadFile用于模拟文件下载。其声明一个字符串数组来模拟存储的文件。run()方法根据字符串数组的长度进行循环获得随机生成的下载秒数并输出下载文件的信息,设置线程的休眠时间为随机生成的下载秒数。运用同步方式将字符串文件添加到集合列表中并调用集合列表的notify()方法通报所有等待集合列表的线程。
(2)ShowFile类的run()方法根据标识为真进行循环,对集合列表对象实现同步,其他线程在此时不能操作该集合列表。调用该集合列表的wait()方法,线程会释放集合列表所持有的同步资源(不会释放其他对象的同步锁),这时其他线程可以访问该集合列表。再通过判断集合列表中是否有元素输出文件信息。
实例127 家族等级(线程优先级)
每个线程都有一个“优先级”,优先级可以用整数表示,取值范围为0~10,0为最低优先级,10为最高优先级,当决定哪个线程需要调度时,首先查看是否存在优先级高的可调度线程,如果存在,就从中选择进行调度。当该线程的时间片到达之后,系统查看是否存在另一个优先级为比较高的可调度线程,如果存在就调度。这样依次进行判断调度线程。本实例利用三种方式实现线程的优先级排列家族的等级。
技术要点
实现线程优先级的技术要点如下:
• Thread类有三个优先级静态常量:MAX_PRIORITY取值是10,为线程最高优先级;MIN_PRIORITY取值是1,为线程最低优先级;NORM_PRIORITY取值是5。为线程中间位置的优先级。默认情况下,线程的优先级为NORM_PRIORITY。
• Java中的线程在同等情况下,对于两个同时启动的线程,优先级高的线程先获取CPU资源,先被真正运行,优先级低的线程后获取CPU资源,后被执行。
• 对于两个同时启动的线程,大多数情况下优先级高的线程会比优先级低的线程先运行,但也有例外情况,完全取决于Java虚拟机的调度。
实现步骤
(1)新建一个类名为TextPriorityLevel.java。
(2)代码如下所示:
package com.zf.s14; //创建一个包 class OneFamilyThread implements Runnable { //线程实现接口Runnable Thread thread; //声明一个线程 OneFamilyThread(){} //默认构造方法 OneFamilyThread(String name) { //构造方法初始一个线程 thread = new Thread(this, name); } public void run() { System.out.println("家族人员:"+thread.getName()); //获得线程的名称 } public void startThreadByLevel(){ //按优先级执行线程 OneFamilyThread o1=new OneFamilyThread("爷爷");//创建线程对象并命名 OneFamilyThread o2= new OneFamilyThread("奶奶"); OneFamilyThread o3 = new OneFamilyThread("爸爸"); OneFamilyThread o4 = new OneFamilyThread("妈妈"); OneFamilyThread o5 = new OneFamilyThread("我"); o1.thread.setPriority(Thread.MAX_PRIORITY); //MAX_PRIORITY优先级最大 o2.thread.setPriority(Thread.MAX_PRIORITY - 1); //较次之 o3.thread.setPriority(Thread.NORM_PRIORITY);//NORM_PRIORITY处于中间位置 o4.thread.setPriority(Thread.NORM_PRIORITY - 1); o5.thread.setPriority(Thread.MIN_PRIORITY); //MIN_PRIORITY优先级最小 o1.thread.start(); //启动线程 o2.thread.start(); o3.thread.start(); o4.thread.start(); o5.thread.start(); try { o5.thread.join(); //等待线程运行结束 } catch (InterruptedException e) { //捕获拦截异常 System.out.println("等待线程结束出错:"+e.getMessage()); } } } class TwoFamilyThread implements Runnable { Thread thread; public TwoFamilyThread() {} //默认构造方法 public void run() { System.out.println("家族人员:"+thread.getName()); //获得线程的名称 } public void startThreadByLevel(){ //按优先级执行线程 TwoFamilyThread t1 = new TwoFamilyThread(); //创建线程类 TwoFamilyThread t2 = new TwoFamilyThread(); TwoFamilyThread t3 = new TwoFamilyThread(); TwoFamilyThread t4 = new TwoFamilyThread(); TwoFamilyThread t5 = new TwoFamilyThread(); t1.thread = new Thread(t1, "爷爷"); //线程类赋名称 t2.thread = new Thread(t2, "奶奶"); t3.thread = new Thread(t3, "爸爸"); t4.thread = new Thread(t4, "妈妈"); t5.thread = new Thread(t5, "我"); t1.thread.setPriority(10); //线程优先级10为优先级最高 t2.thread.setPriority(9); t3.thread.setPriority(8); t4.thread.setPriority(7); t5.thread.setPriority(6); t1.thread.start(); //启动线程 t2.thread.start(); t3.thread.start(); t4.thread.start(); t5.thread.start(); try { t5.thread.join(); //等待线程运行结束 } catch (InterruptedException e) { //捕获拦截异常 System.out.println("等待线程结束出错:"+e.getMessage()); } } } class ThreeFamilyThread extends Thread { Thread thread; public ThreeFamilyThread(){} //默认构造方法 public ThreeFamilyThread(String name) { //带参数构造方法初始化一个线程 thread = new Thread(this, name); } public void run() { System.out.println("家族人员:"+thread.getName()); //获得线程的名称 } public void startThreadByLevel(){ //按优先级执行线程 ThreeFamilyThread e1=new ThreeFamilyThread("爷爷"); //创建线程对象并命名 ThreeFamilyThread e2 = new ThreeFamilyThread("奶奶"); ThreeFamilyThread e3 = new ThreeFamilyThread("爸爸"); ThreeFamilyThread e4 = new ThreeFamilyThread("妈妈"); ThreeFamilyThread e5 = new ThreeFamilyThread("我"); e1.thread.setPriority(Thread.MAX_PRIORITY); //MAX_PRIORITY优先级最大 e2.thread.setPriority(Thread.MAX_PRIORITY - 1); //较次之 e3.thread.setPriority(Thread.NORM_PRIORITY);//NORM_PRIORITY处于中间位置 e4.thread.setPriority(Thread.NORM_PRIORITY - 1); e5.thread.setPriority(Thread.MIN_PRIORITY); //MIN_PRIORITY优先级最小 e1.thread.start(); //启动线程 e2.thread.start(); e3.thread.start(); e4.thread.start(); e5.thread.start(); try { e5.thread.join(); //等待线程运行结束 } catch (InterruptedException e) { //捕获拦截异常 System.out.println("等待线程结束出错:"+e.getMessage()); } } } public class TextPriorityLevel extends Thread { //操作运用优先级实现家族等级的类 public static void main(String []args){ //Java程序主入口处 System.out.println("继承Thread类,根据数字从高到低实现线程优先级"); new TwoFamilyThread().startThreadByLevel(); System.out.println("继承Thread类,根据静态等级常量实现线程优先级"); new ThreeFamilyThread().startThreadByLevel(); System.out.println("实现Runnable接口,根据静态等级常量实现线程优先级"); new OneFamilyThread().startThreadByLevel(); } }
(3)运行结果如下所示:
继承Thread类,根据数字从高到低实现线程优先级 家族人员:爷爷 家族人员:妈妈 家族人员:爸爸 家族人员:奶奶 家族人员:我 继承Thread类,根据静态等级常量实现线程优先级 家族人员:爷爷 家族人员:奶奶 家族人员:爸爸 家族人员:妈妈 家族人员:我 实现Runnable接口,根据静态等级常量实现线程优先级 家族人员:爷爷 家族人员:奶奶 家族人员:妈妈 家族人员:爸爸 家族人员:我
源程序解读
(1)OneFamilyThread类实现Runnable接口可看作是一线程类,实现该接口必须实现run()方法,该类的startThreadByLevel()方法创建5个线程,并对每个线程设置优先级。其中Thread类的MAX_PRIORITY表示优先级最高,NORM_PRIORITY表示优先级居中,MIN_PRIORITY表示优先级最小。启动线程,线程是按照优先级的不同依次启动运行。
(2)TwoFamilyThread类实现Runnable接口可看作是一线程类,实现该接口必须实现run()方法,类中的startThreadByLevel()方法创建5个线程,每个线程设置优先级。其中setPriority (整数)方法是以数字设置优先级,10表示优先级最高,优先级依次类推,1表示优先级最低。启动线程,线程是按照优先级的不同依次启动运行。
(3)ThreeFamilyThread类继承Thread线程类,扩展run()方法。startThreadByLevel()方法创建5个线程类,并设置5个线程的优先级,方式与OneFamilyThread类的该方法相同。启动线程,线程是按照优先级的不同依次启动运行。
(4)每个类中都含有join()方法,该方法是等待前面的线程运行结束才运行该方法后的线程。
实例128 定时器(Timer)
在开发过程中往往会用到Timer类来定时执行任务,如每天下午指定时间查看个人邮件信息,或者定时生成一些索引文件供查询信息所用。本实例介绍如何定时执行指定的任务,包括在指定的时间点执行任务、每隔某个时间段重复执行任务等。
技术要点
实现定时器Timer的技术要点如下:
• 类java.util.Timer和java.util.TimerTask可以实现定时执行任务。Timer类的schedule系列方法能够在指定的时间执行TimerTask类型的任务。
• 可以用线程来实现每隔某个时间段重复执行任务:当线程处理完任务后,进入休眠sleep状态中,等一段时间之后,再执行任务。
实现步骤
(1)新建一个类名为TextThreadTimer.java。
(2)代码如下所示:
package com.zf.s14; //创建一个包 import java.util.Date; //引入类 import java.util.Timer; import java.util.TimerTask; class WorkTask extends TimerTask { //继承时间任务类执行任务 private int taskID = 0; //任务编号 public WorkTask(int id) { //带参数的构造方法进行初始化 this.taskID = id; } public void run() { //实现TimerTask中的方法 System.out.println("执行工作任务-" + this.taskID + ",执行时间-" + new Date().getTime()); } } public class TextThreadTimer { //操作运用线程实现定时器的类 public static void main(String[] args) { //Java程序主入口处 Timer timer = new Timer(); //创建定时器类 TimerTask task1 = new WorkTask(1); timer.schedule(task1, 100); //0.1秒后执行任务 TimerTask task2 = new WorkTask(2); timer.schedule(task2, 300, 1000); //0.3秒后执行任务并每隔1秒执行一次 TimerTask task3 = new WorkTask(3); Date date = new Date(System.currentTimeMillis() + 1000); timer.schedule(task3, date); //在指定时间1秒后执行任务 try { Thread.sleep(5000); //休眠5秒 } catch (InterruptedException e) { //捕获拦截异常 System.out.println("出现错误:"+e.getMessage()); } timer.cancel(); //终止定时器取消定时器中的任务 System.out.println("定时器取消..."); } }
(3)运行结果如下所示:
执行工作任务-1, 执行时间-2009-4-21 15:30:50 执行工作任务-2, 执行时间-2009-4-21 15:30:50 执行工作任务-3, 执行时间-2009-4-21 15:30:51 执行工作任务-2, 执行时间-2009-4-21 15:30:51 执行工作任务-2, 执行时间-2009-4-21 15:30:52 执行工作任务-2, 执行时间-2009-4-21 15:30:53 执行工作任务-2, 执行时间-2009-4-21 15:30:54 定时器取消...
源程序解读
(1)WorkTask类继承TimerTask类实现定时器执行指定任务,TimeTask类提供了一个抽象方法run(),所有需要用定时器执行的任务类都必须继承TimerTask并实现run()方法。
(2)Timer类的schedule()方法用于调度任务。schedule(任务,时间)方法表示在过该时间后执行任务,schedule(任务,时间1,时间2)方法表示在过时间1后执行任务并以后每隔时间2执行一次任务,schedule(任务,日期)方法表示在过该日期后执行任务。
实例129 没法吃饭(死锁)
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们将无法推进下去,此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。本实例模拟两个人就餐在等待一双筷子时引起的死锁现象。
技术要点
模拟等待筷子没法吃饭引起的死锁现象的技术要点如下:
• 产生死锁的原因主要有:因为系统资源不足;进程运行推进的顺序不合适;资源分配不相等。如果系统的资源充足,进程的资源请求都能够得到满足,死锁出现的可以性就很低,否则就会因争夺有限资源而陷入死锁。另外,进程运行推进顺序与速度不同,也可能产生死锁。
• 产生死锁的四个必要条件:一是互斥条件,一个资源每次只能被一个进程使用;二是请求与保持条件,一个进程因请求资源而阻塞时,对已获得的资源保持不放;三是不剥夺条件,进程已获得的资源,在未使用完之前,不能强行剥夺;四是循环等待条件,若干进程之间形成一种头尾相接的循环等待资源关系。这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
实现步骤
(1)新建一个类名为TextDeadLock.java。
(2)代码如下所示:
package com.zf.s14; //创建一个包 public class TextDeadLock { //操作线程死锁的类 static String[] chopsticks=new String []{"筷子1","筷子2"}; static class OneThread extends Thread{ //静态内部类 public void run(){ synchronized(chopsticks[0]){ //在同一时间只能有一个类访问 System.out.println("甲拿起了"+chopsticks[0]+",在等待另一"+chopsticks[1]); try { Thread.sleep(100); //线程休眠 } catch (Exception e) { //捕获异常 System.out.println("线程休眠出错:"+e.getMessage()); } synchronized(chopsticks[1]){ System.out.println("甲又拿起了"+chopsticks[1]); } } } } static class TwoThread extends Thread{ //静态内部类 public void run(){ synchronized(chopsticks[1]){ System.out.println("乙拿起了"+chopsticks[1]+",在等待另一"+chopsticks[0]); try { Thread.sleep(100); //线程休眠 } catch (Exception e) { //捕获异常 System.out.println("线程休眠出错:"+e.getMessage()); } synchronized(chopsticks[0]){ System.out.println("乙又拿起了"+chopsticks[1]); } } } } static class DaemonThread extends Thread{ //静态守护线程类 public DaemonThread(){ this.setDaemon(true); //线程设置守护 } public void run(){ while(true){ try { Thread.sleep(1000); //线程休眠 }catch (Exception e) { //捕获异常 System.out.println("线程休眠出错:"+e.getMessage()); } System.out.println("守护线程:程序正在运行..."); } } } public static void main(String []args){ //Java程序主入口处 OneThread one=new OneThread(); //实例化对象 TwoThread two=new TwoThread(); DaemonThread daemon=new DaemonThread(); one.start(); //启动线程 two.start(); daemon.start(); } }
(3)运行结果如下所示:
甲拿起了筷子1,在等待另一筷子2 乙拿起了筷子2,在等待另一筷子1 守护线程:程序正在运行... 守护线程:程序正在运行... 守护线程:程序正在运行... 守护线程:程序正在运行... ......
源程序解读
(1)TextDeadLock类创建静态字符串数组,用来存放所需的筷子。创建静态内部类OneThread继承Thread线程类,实现其run()方法。在run()方法中运用同步块synchronized()来实现在同一时间只能有一个线程访问。在其同步块中再使用同步块。甲在拿起“筷子1”时在等待“筷子2”。静态内部类TwoThread继承Thread线程类,实现其run()方法。在run()方法中运用同步块synchronized()方法使乙拿起“筷子2”在等待“筷子1”,这样就造成了死锁问题。
(2)静态内部类DaemonThread类继承Thread线程类,在其构造方法中通过方法setDaemon()设置该类为守护线程类。在run()方法中根据标识为真进行循环,使线程在每1秒休眠。
实例130 方便吃饭(解决死锁)
死锁有一系列的方法可以避免,也有很多的判断和解决办法,在上一个实例中遇到的没法吃饭的死锁问题,解决方法有很多。例如,再增加一只筷子,甲或者乙可以拿到新增的筷子开始吃饭,吃完饭后将筷子资源释放,然后另一个人可以开始吃饭。本实例介绍一个避免死锁的方法,甲和乙都先拿起筷子1后拿起筷子2就不会有死锁问题。
技术要点
解决死锁问题方便吃饭的技术要点如下:
• 死锁排除的方法:一是撤销陷入死锁的全部进程;二是逐个撤销陷于死锁的进程,直到死锁不存在;三是从陷于死锁的进程中逐个强迫放弃所占用的资源,直至死锁消失;四是从另外一些进程那里强行剥夺足够数量的资源分配给死锁进程,以解除死锁状态。
• 预防死锁需要防止进程在处于等待状态的情况下占用资源,在系统运行过程中,对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,若分配后系统可能发生死锁,则不予分配,否则予以分配。因此,对资源的分配要给予合理的规划。
实现步骤
(1)新建一个类名为TextSolveDeadLock.java。
(2)代码如下所示:
package com.zf.s14; //创建一个包 public class TextSolveDeadLock{ //操作解决线程死锁的类 static String[] chopsticks=new String []{"筷子1","筷子2"}; static class OneThread extends Thread{ //静态内部类 public void run(){ synchronized(chopsticks[0]){ //在同一时间只能有一个类访问 System.out.println("甲拿起了"+chopsticks[0]+",在等待另一"+chopsticks[1]); try { Thread.sleep(100); //线程休眠 } catch (Exception e) { //捕获异常 System.out.println("线程休眠出错:"+e.getMessage()); } synchronized(chopsticks[1]){ System.out.println("甲又拿起了"+chopsticks[1]); } } } } static class TwoThread extends Thread{ //静态内部类 public void run(){ synchronized(chopsticks[0]){ System.out.println("乙拿起了"+chopsticks[0]+",在等待另一"+chopsticks[1]); try { Thread.sleep(100); //线程休眠 } catch (Exception e) { //捕获异常 System.out.println("线程休眠出错:"+e.getMessage()); } synchronized(chopsticks[1]){ System.out.println("乙又拿起了"+chopsticks[1]); } } } } static class DaemonThread extends Thread{ //静态守护线程类 public DaemonThread(){ this.setDaemon(true); //线程设置守护 } public void run(){ while(true){ try { Thread.sleep(1000); //线程休眠 }catch (Exception e) { //捕获异常 System.out.println("线程休眠出错:"+e.getMessage()); } System.out.println("守护线程:程序正在运行......"); } } } public static void main(String []args){ //Java程序主入口处 OneThread one=new OneThread(); //实例化对象 TwoThread two=new TwoThread(); DaemonThread daemon=new DaemonThread(); one.start(); //启动线程 two.start(); daemon.start(); } }
(3)运行结果如下所示:
甲拿起了筷子1,在等待另一筷子2 甲又拿起了筷子2 乙拿起了筷子1,在等待另一筷子2 乙又拿起了筷子2
源程序解读
TextSolveDeadLock类中的静态内部类OneThread继承Thread线程类,扩展run()方法实现甲拿起“筷子1”在等待“筷子2”。静态内部类TwoThread类的run()方法实现乙拿起“筷子1”在等待“筷子2”。这样就解决了死锁问题。
实例131 查看JVM中所有的线程和线程组
本实例介绍如何以树状结构查看Java虚拟机中所有的线程和线程组以及线程的优先级等信息。
技术要点
查看Java虚拟机(JVM)中所有的线程和线程组的技术要点如下:
• 虚拟机中的任何线程都处于线程组ThreadGroup中,线程组也可以包含子线程组。
• Thread的currentThread静态方法能够获得当前线程的引用。
• Thread的getThreadGroup实例方法能够获得当前线程所属的线程组。
• ThreadGroup的getParent实例方法能够获得当前线程组的父线程组,根线程组的父线程组为null。
实现步骤
(1)新建一个类名为TextRunningThread.java。
(2)代码如下所示:
package com.zf.s14; //创建一个包 public class TextRunningThread {/ /操作查看JVM虚拟机中所的线程和线程组的类 //显示线程信息 private static void showThread(Thread thread, String index) { if (thread == null) return; System.out.println(index + "线程名称- " + thread.getName() + " 线程优先级- " + thread.getPriority() + (thread.isDaemon() ? " 守护" : "") + (thread.isAlive() ? "" : " 不活动")); } //显示线程组信息 private static void showThreadGroup(ThreadGroup group, String index) { if (group == null)return; //判断线程组 int count = group.activeCount(); //获得活动的线程数 //获得活动的线程组数 int countGroup = group.activeGroupCount(); //根据活动的线程数创建指定个数的线程数组 Thread[] threads = new Thread[count]; //根据活动的线程组数创建指定个数的线程组数组 ThreadGroup[] groups = new ThreadGroup[countGroup]; group.enumerate(threads, false); //把所有活动子组的引用复制到指定数组中,false表示不包括对子组的所有活动子组的引用 group.enumerate(groups, false); System.out.println(index + "线程组的名称-" + group.getName() + "最高优先级- " + group.getMaxPriority() + (group.isDaemon() ? " 守护" : " ")); //循环显示当前活动的线程信息 for (int i = 0; i < count; i++) showThread(threads[i], index + " "); for (int i = 0; i < countGroup; i++) //循环显示当前活动的线程组信息 showThreadGroup(groups[i], index + " ");//递归调用方法 } public static void listAllThreads() { //找到根线程组并列出它递归的信息 ThreadGroup currentThreadGroup; //当前线程组 ThreadGroup rootThreadGroup; //根线程组 ThreadGroup parent; //获得当前活动的线程组 currentThreadGroup = Thread.currentThread().getThreadGroup(); rootThreadGroup = currentThreadGroup; //获得根线程组 parent = rootThreadGroup.getParent(); //获得根线程 while (parent != null) { //循环对根线程组重新赋值 rootThreadGroup = parent; parent = parent.getParent(); } showThreadGroup(rootThreadGroup, ""); //显示根线程组 } public static void main(String[] args) { //Java程序主入口处 TextRunningThread.listAllThreads(); //调用方法显示所有线程的信息 } }
(3)运行结果如下所示:
线程组的名称-system最高优先级- 10 线程名称- Reference Handler 线程优先级- 10 守护 线程名称- Finalizer 线程优先级- 8 守护 线程名称- Signal Dispatcher 线程优先级- 9 守护 线程名称- Attach Listener 线程优先级- 5 守护 线程组的名称-main最高优先级- 10 线程名称- main 线程优先级- 5
源程序解读
(1)TextRunningThread类的showThread()静态方法是显示线程的信息,包括线程的名称、优先级、是否是守护线程以及线程是否处在活动状态。
(2)showThreadGroup()静态方法显示线程组的信息,ThreadGroup类activeCount()方法获得线程组中活动的线程数,activeGroupCount()方法获得活动的线程组数。根据线程数和线程组数分别创建线程数组和线程组数组。enumerate()方法把所有活动的子组的引用复制到指定数组中,false表示不包括对子组的所有活动子组的引用。运用循环调用showThread()方法显示每个线程的信息,调用showThreadGroup()方法递归显示线程组中线程的信息。
(3)listAllThreads()方法找到根线程组并列出它通过递归显示的信息。Thread类的currentThread()方法是获得当前线程,getThreadGroup()方法是获得当前活动的线程组。ThreadGroup类的getParent()方法获得根线程。运用根线程不为空条件进行循环,获得每个线程的根线程,再调用showThreadGroup()方法以树状结构显示线程信息。
实例132 执行任务(线程池)
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程,每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。本实例介绍如何运用线程池实现任务的执行。
技术要点
运用线程池实现任务的执行的技术要点如下:
• 如果某个线程在托管代码中空闲,则线程池将插入另一个辅助线程来使所有处理器保持繁忙;如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程,但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但它们要等到其他线程完成后才启动。
• 任务放在LinkedList中,由于LinkedList不支持同步,所以在添加任务和获取任务的方法声明中必须使用synchronized关键字。
• 关闭线程池时,通过ThreadGroup线程组获得池中所有活动线程的引用,依次调用Thread类的join()方法等待活动线程执行完毕。当所有线程运行结束时,线程池才算被关闭。
实现步骤
(1)新建一个类名为TextThreadPool.java。
(2)代码如下所示:
package com.zf.s14; //创建一个包 import java.util.LinkedList; //引入类 class ThreadPool extends ThreadGroup{ //继承线程组实现线程池功能 private boolean isClosed = false; //线程池是否关闭 private LinkedList taskQueue; //工作任务队列 private static int threadPool_ID = 1; //线程池的编号
private class TaskThread extends Thread { //负责从工作队列中取出任务并执行的内部类 private int id; //任务编号 public TaskThread(int id) { //构造方法进行初始化 super(ThreadPool.this,id+""); //将线程加入到当前线程组中 this.id =id; } public void run() { while(! isInterrupted()) { //判断线程是否被中断 Runnable task = null; task = getTask(id); //取出任务
//如果getTask()返回null或者线程执行getTask()时被中断,则结束此线程 if(task == null) return;
try { task.run(); //运行任务 }catch(Throwable t) { t.printStackTrace(); } } } } public ThreadPool(int poolSize) { //构造方法传入线程池中的工作线程的数量 super(threadPool_ID + ""); //指定线程组名称 setDaemon(true); //继承线程组的方法用来设置是否守护线程池 taskQueue = new LinkedList(); //创建工作任务队列 for(int i = 0; i < poolSize; i++) { //循环创建任务线程 new TaskThread(i).start(); //根据线程池数据创建任务线程并启动线程 } } public synchronized void executeTask(Runnable task){//添加新任务并执行任务 if(isClosed) { //判断标识 throw new IllegalStateException(); //抛出不合理状态异常 } if(task != null) { taskQueue.add(task); //向任务队列中加入一个任务 notify(); //唤醒等待任务的工作任务线程 } } private synchronized Runnable getTask(int id){ //取出任务 try { while(taskQueue.size() == 0) { //循环使线程等待任务 if(isClosed) return null; System.out.println("工作线程"+id+"等待任务⋯⋯"); wait(); //如果任务队列中没有任务,就等待任务 } } catch (InterruptedException e) { //捕获拦截异常 System.out.println("等待任务出现错误:"+e.getMessage()); } System.out.println("工作线程"+id+"开始执行任务⋯⋯"); return (Runnable)taskQueue.removeFirst();//返回第一个任务并从队列中删除 } public synchronized void closeThreadPool(){ //关闭线程池 if(!isClosed) { //判断标识 waitTaskFinish(); //等待任务线程执行完毕 isClosed = true; //标识为真 taskQueue.clear(); //任务队列清空 interrupt(); //唤醒线程池中所有的工作线程 } } public void waitTaskFinish() { //等待任务线程把所有任务执行完毕 synchronized (this) { isClosed = true; //标识为真 notifyAll(); //唤醒等待任务的工作任务线程 } Thread[] threads=new Thread[activeCount()];//创建线程组中活动的线程组 int count = enumerate(threads); //获得线程组中当前所有活动的工作线程 for(int i =0; i < count; i++) { //循环等待所有工作线程结束 try { threads[i].join(); //等待工作线程结束 }catch(InterruptedException e) { //捕获拦截异常 System.out.println("任务执行出错:"+e.getMessage()); } } } } public class TextThreadPool { //操作线程池执行任务的类 private static Runnable createTask(final int taskID) { //创建任务方法 return new Runnable() { public void run() { //创建任务 System.out.println("任务开始,编号为"+taskID); System.out.println("start task"); System.out.println("任务结束,编号为"+taskID); } }; } public static void main(String[] args){ //Java程序主入口处 ThreadPool threadPool = new ThreadPool(3); //创建一个有个3任务线程的线程池 try {//休眠600毫秒,让线程池中的任务线程全部运行 Thread.sleep(600); } catch (InterruptedException e) { //捕获拦截异常 System.out.println("线程休眠出错:"+e.getMessage()); } for (int i = 0; i < 5 ; i++) { //循环创建并执行任务 threadPool.executeTask(createTask(i)); } threadPool.waitTaskFinish(); //等待所有任务执行完毕 threadPool.closeThreadPool(); //关闭线程池 } }
(3)运行结果如下所示:
工作线程0等待任务⋯⋯ 工作线程1等待任务⋯⋯ 工作线程2等待任务⋯⋯ 工作线程0开始执行任务⋯⋯ 任务开始,编号为0 start task 任务结束,编号为0 工作线程0开始执行任务⋯⋯ 任务开始,编号为1 start task 任务结束,编号为1 工作线程2开始执行任务⋯⋯ 任务开始,编号为2 start task 工作线程1开始执行任务⋯⋯ 任务开始,编号为3 start task 任务结束,编号为3 任务结束,编号为2 工作线程0开始执行任务⋯⋯ 任务开始,编号为4 start task 任务结束,编号为4
源程序解读
(1)内部类ThreadPool继承线程组类ThreadGroup实现线程池的功能。其私有内部类TaskThread继承Thread线程类,扩展run()方法。在run()方法中根据标识为真进行循环。根据Thread类的getTask()方法获得Runnable任务对象,调用任务对象的run()方法执行任务。
(2)ThreadPool类的构造方法传入线程池中工作线程的数量,设置该类为守护线程类,并创建双向链表。运用循环创建任务线程并启动线程。executeTask()方法是往双向链表中添加任务对象并唤醒等待任务的工作任务线程。getTask()方法根据编号获得指定的任务,并移除双向链表中的第一个任务线程。closeThreadPool()方法判断标识,如果标识为真,则调用waitTaskFinish()方法将等待的任务线程的所有的任务执行完毕。若设置标识为假,清空双向链表中的任务,唤醒线程池中的所有工作线程。
(3)waitTaskFinish()方法是等待任务线程执行所有的任务。在其同步块中唤醒等待任务的工作任务线程并设置标识为真。根据活动的线程数创建线程数组,根据活动的线程获得线程组中当前所有活动的工作线程,再运用循环通过join()方法等待所有工作线程结束。
(4)TextThreadPool类的createTask()方法根据任务编号执行指定的任务。在类的main()主方法中实例化三个ThreadPool对象,Thread类的sleep()方法使线程休眠0.6秒,运用循环执行创建的任务,调用waitTashFinish()方法等待所有的任务执行完毕再关闭线程池。
实例133 碰撞的球(多线程)
本实例运用多线程控制球的运动,球在遇到障碍或边界后会折回。适合初学者对线程的进一步掌握和了解与运用。
技术要点
运用多线程实现碰撞的球的技术要点如下:
• 多线程是为了使得多个线程并行地工作以完成多项任务,以提高系统的效率。线程是在同一时间需要完成多项任务的时候被实现的。
• 碰撞的小球运用多线程实现,球的运动速度随着时间的变动可能加快,这也是体现多线程的好处之一。从左右两侧出现的球是运用两个线程分别控制的。当碰到面板的边缘时则折回。
实现步骤
(1)新建一个类名为TextCollisionBall.java。
(2)代码如下所示:
package com.zf.s14; //创建一个包 import java.awt.*; //引入类 import java.awt.event.*; import javax.swing.*; import javax.swing.event.*; class BallFrame extends JFrame implements ActionListener, ChangeListener { private static final long serialVersionUID = 1L; JPanel panel; //面板 JPanel preview; //预览面板 JSlider red; //红色滑块 JSlider green; //绿色滑块 JSlider blue; //蓝色滑块 JSlider JS_SIZE; //滑块大小 int x = 45, y = 45; //方位 int BALL_SIZE = 30; //球的大小 public BallFrame() { //构造方法进行初始化 super("collision ball"); panel = new JPanel(); //创建面板 panel.setBounds(20, 0, 450, 200); //设置面板的位置以及大小 panel.setBackground(Color.WHITE); //面板背景色为白色 preview = new JPanel(); //创建预览球的面板 preview.setBounds(350, 220, 120, 120); preview.setBackground(Color.white); //设置状态栏 JTextField status = new JTextField("请选择球的颜色、大小然后单击按钮"); status.setBounds(1, 404, 492, 20); //设置状态栏的位置以及大小 status.setEditable(false); //初始不能编辑 JLabel redLabel = new JLabel("红"); //创建红色标签 redLabel.setBounds(20, 215, 30, 20); //设置红标签的位置以及大小 JLabel greenLabel = new JLabel("绿"); //创建绿色标签 greenLabel.setBounds(20, 260, 30, 20); //设置绿标签的位置以及大小 JLabel blueLabel = new JLabel("蓝"); //创建蓝色标签 blueLabel.setBounds(20, 305, 30, 20); //设置蓝标签的位置以及大小 JLabel sizeLabel = new JLabel("大小"); //创建大小标签 sizeLabel.setBounds(20, 350, 30, 20); //设置大小标签的位置以及大小 red = new JSlider(SwingConstants.HORIZONTAL, 0, 255, 127);//创建红色滑块 red.setBounds(50, 210, 250, 45); //设置滑块的位置以及大小 red.putClientProperty("JSlider.isFilled", Boolean.TRUE);//填充滑块 red.setPaintTicks(true); //绘制勾号标记 red.setMajorTickSpacing(50); //主要的勾号标记大小 red.setMinorTickSpacing(25); //次要的勾号标记大小 red.setPaintLabels(true); //显示主要刻度的数字标记 red.addChangeListener(this); //添加监听者 blue = new JSlider(SwingConstants.HORIZONTAL, 0, 255, 127); //创建蓝色滑块 blue.setBounds(50, 300, 250, 45); blue.putClientProperty("JSlider.isFilled", Boolean.TRUE); blue.setPaintTicks(true); blue.setMajorTickSpacing(50); blue.setMinorTickSpacing(25); blue.setPaintLabels(true); blue.addChangeListener(this); green = new JSlider(SwingConstants.HORIZONTAL, 0, 255, 127); //创建绿色滑块 green.setBounds(50, 255, 250, 45); green.putClientProperty("JSlider.isFilled", Boolean.TRUE); green.setPaintTicks(true); green.setMajorTickSpacing(50); green.setMinorTickSpacing(25); green.setPaintLabels(true); green.addChangeListener(this); //创建球大小的滑块 JS_SIZE = new JSlider(SwingConstants.HORIZONTAL, 10, 50, 30); JS_SIZE.setBounds(50, 345, 250, 45); JS_SIZE.putClientProperty("JSlider.isFilled", Boolean.TRUE); JS_SIZE.setPaintTicks(true); JS_SIZE.setMajorTickSpacing(10); JS_SIZE.setMinorTickSpacing(5); JS_SIZE.setPaintLabels(true); JS_SIZE.addChangeListener(this); JButton jb = new JButton("注入球"); //创建球按钮单击 jb.setBounds(350, 360, 120, 30); jb.addActionListener(this); Container c = this.getContentPane(); //将所有对象添加到窗体 c.setLayout(null); //布局置空(不使用布局) c.add(panel); c.add(preview); c.add(redLabel); c.add(blueLabel); c.add(greenLabel); c.add(sizeLabel); c.add(red); c.add(green); c.add(blue); c.add(JS_SIZE); c.add(jb); c.add(status); this.setBounds(100, 50, 500, 450); //设置窗体的位置和大小 this.setResizable(false); this.setVisible(true);//可视 this.addWindowListener(new WindowAdapter() { //对窗口添加监听事件 public void windowClosing(WindowEvent e) { //当窗口关闭时 System.exit(0); //安全退出 } }); } public void actionPerformed(ActionEvent e) { //实现ActionListener接口的方法 Color ball_color = new Color(red.getValue(), green.getValue(), blue.getValue()); //获取当前球的颜色 //实例化右边的球对象 RightBall r = new RightBall(panel, JS_SIZE.getValue(), ball_color); r.start(); //启动线程 //实例化左边球对象 LeftBall ball = new LeftBall(panel, JS_SIZE.getValue(), ball_color); ball.start(); //启动线程 } public void stateChanged(ChangeEvent e) { //实现ChangeListener接口的方法 Graphics g = preview.getGraphics(); //获得preview画笔并在预览面板中心画个圆 g.setColor(Color.white); //图形为白色 g.fillOval(x, y, BALL_SIZE, BALL_SIZE); //填充外接指定矩形框的圆 x = 60 - JS_SIZE.getValue() / 2; y = 60 - JS_SIZE.getValue() / 2; BALL_SIZE = JS_SIZE.getValue(); g.setColor(new Color(red.getValue(), green.getValue(), blue .getValue())); g.fillOval(x, y, BALL_SIZE, BALL_SIZE); //填充外接指定矩形框的圆 g.dispose();//显示 } } class LeftBall extends Thread { //继承Thread类实现从窗口左面出现的球 JPanel LEFTPANEL; //从窗口左面出现的球的面板 int BALL_SIZE; //球的大小 Color BALL_COLOR; //球的颜色 public LeftBall(JPanel panel,int size,Color color){//构造方法进行初始化 this.LEFTPANEL = panel; //获得画板的句柄 this.BALL_SIZE = size; //获得球的大小 this.BALL_COLOR = color; //获得球的颜色 } public void run() { //继承Thread类实现的方法 Graphics g = LEFTPANEL.getGraphics(); //获得图形 int x = 0, y = 0; int LEFT_X = 450 - BALL_SIZE; //计算画球时X轴的最大坐标 int LEFT_Y = 200 - BALL_SIZE; //计算画球时Y轴的最大坐标 int x_increase = 5, y_increase = 5; //球移动的增量 while (true) { //循环移动球 g.setColor(Color.white); //将上一次画的球擦掉 g.fillOval(x, y, BALL_SIZE, BALL_SIZE);//填充外接指定矩形框的圆 g.setColor(BALL_COLOR); //设置球的颜色 x = x + x_increase; //球每次X轴的位置 y = y + y_increase; //球每次Y轴的位置 g.fillOval(x,y,BALL_SIZE,BALL_SIZE);//填充外接指定矩形框的圆 if (x <= 0 || x >= LEFT_X) //判断球是否到达了边界,若到达了则转向 x_increase = -x_increase; if (y <= 0 || y >= LEFT_Y) y_increase = -y_increase; try { Thread.sleep(30); //休眠一段时间 } catch (Exception e) { //捕获异常 } } } } class RightBall extends Thread { //继承Thread类实现从窗口右面出现的球 JPanel RIGHTPANEL; //从窗口右面出现的球的面板 int BALL_SIZE; //球的大小 Color BALL_COLOR; //球的颜色
public RightBall(JPanel panel, int size, Color color) {//构造方法进行初始化 this.RIGHTPANEL = panel; //获得画板的句柄 this.BALL_SIZE = size; //获得球的大小 this.BALL_COLOR = color; //获得球的颜色 } public void run() { Graphics g = RIGHTPANEL.getGraphics();//获得图形 int x = 450 - BALL_SIZE, y = 0; int RIGHT_X = x; int RIGHT_Y = 200 - BALL_SIZE; int x_increase = -5, y_increase = 5; //球移动的增量 while (true) { //循环移动球 g.setColor(Color.white); //将上一次画的球擦掉 g.fillOval(x, y, BALL_SIZE, BALL_SIZE); //填充外接指定矩形框的圆 g.setColor(BALL_COLOR); //设置球的颜色 x = x + x_increase; //球每次X轴的位置 y = y + y_increase; //球每次Y轴的位置 g.fillOval(x,y,BALL_SIZE,BALL_SIZE); //填充外接指定矩形框的圆 if (x <= 0 || x >= RIGHT_X) //判断球是否到达了边界,若到达了则转向 x_increase = -x_increase; if (y <= 0 || y >= RIGHT_Y) y_increase = -y_increase; try { Thread.sleep(60); //休眠一段时间 } catch (Exception e) { //捕获异常 } } } } public class TextCollisionBall { //操作运用线程实现碰撞的球的类 public static void main(String args[]) { //Java程序主入口处 new BallFrame(); //实例化对象 } }
(3)运行结果如下所示:
源程序解读
(1)BallFrame类继承JFrame类实现ActionListener与ChangeListener接口。实现ActionListener接口必须实现actionPerformed()方法,实现ChangeListener接口必须实现stateChanged()方法。在BallFrame类的构造方法中创建面板并设置面板的大小、位置、背景颜色;创建预览面板并设置预览面板的大小、位置与背景颜色;创建文本框作为状态栏并设置其大小、位置,初始不可编辑;创建红绿蓝和球大小的四个标签,并设置标签的位置;创建红绿蓝以及球大小四个滑块并设置滑块的大小、位置、填充滑块、绘制勾号标记、设置主要的勾号标记大小、设置次要的勾号标记大小、设置主要刻度的数字标记以及添加监听器。创建注入球按钮,当单击该按钮时触发事件在面板中导入两个运动的球。创建窗体容器并设置其布局为空,这样可以在窗体容器中随意摆放组件,再将相关的组件添加到窗体容器中,设置窗体容器的位置大小以及可视化,当关闭窗体时调用系统的exit(0)方法安全退出程序。
(2)BallFrame类的actionPerformed()方法根据红绿蓝滑块中指定的颜色获取当前球的颜色。根据面板、球的大小以及球的颜色创建RightBall和LeftBall对象,并启动线程。这样球就可以注入到面板中运动了。
(3)BallFrame类的stateChanged()方法获得预览面板中的画笔,在预览面板的中心画一个圆,并设置该图形的颜色为白色和填充外接指定矩形框的圆。根据设置球大小的滑块值设置圆的半径。重新设置图形的颜色和填充外接指定矩形框的圆,Graphics类的dispose()方法将所画的圆显示出来。
(4)LeftBall类和RightBall类继承Thread类扩展run()方法,其构造方法获得球的面板、大小以及颜色。run()方法根据面板的getGraphics()方法获得球的图形,并根据获得的球的大小计算画球时横纵坐标的最大坐标。运用条件始终为真进行循环,球每移动一步,将上一步画的球的颜色设置为白色来将球擦掉,并获得新的球的横纵坐标。如果球运动到面板的边缘则球转向。设置左球与右球每隔30毫秒移动一次。
实例134 钟表(多线程)
Applet是在Web浏览器中运行的Java小应用程序,它能够嵌入到HTML页面中,并可以通过Web浏览器下载和执行。Applet程序中并不需要主运行方法,由Java虚拟机调用执行。本实例介绍运用Applet与多线程制作一个简单的钟表。
技术要点
运用Applet与多线程制作一个简单的钟表的技术要点如下:
• Applet程序中不需要主运行函数,取之的则是使用init()、start()、stop()和destroy()方法。这四个方法组成了Applet的生命周期。
• Applet类位于java.Applet包中,其继承自Pod,所以也是一个容器,可以包含AWT组件。当Applet嵌入到Web页面时,如果页面转到其他页面,浏览器会自动调用Applet类的stop()方法,因此让Applet暂停的代码要写在这个方法中。
实现步骤
(1)创建一个类名为TextClock.java。
(2)代码如下所示:
package com.zf.s14; //创建一个包 import java.awt.*; //引入类 import java.applet.*; import java.util.*; public class TextClock extends Applet implements Runnable { //操作运用多线程实现一钟表的类 Thread HOUR_THREAD = null; //表示时针的线程 Thread MINUTE_THREAD = null; //表示分针的线程 Thread SECOND_THREAD = null; //表示秒针的线程 int hour_a,hour_b,minute_a,minute_b,second_a,second_b; //表示时、分、秒针端点整型变量 int current_hour = 0; //获取当前的时整数变量 int current_minute = 0; //获取当前的分整数变量 int current_second = 0; //获取当前的秒整数变量 Graphics second_graphics = null; //绘制秒针的Graphics对象 Graphics minute_graphics = null; //绘制分针的Graphics对象 Graphics hour_graphics = null; //绘制时针的Graphics对象 double point_x[] = new double[61]; //存放表盘刻度的X轴数组 double point_y[] = new double[61]; //存放表盘刻度的Y轴数组 double scan_x[] = new double[61]; //供绘制表盘使用 double scan_y[] = new double[61]; int isStart = 0; //判断是否重新开始 public void init() { //数据初始化 hour_graphics = this.getGraphics(); //实例化时针Graphics对象 hour_graphics.setColor(Color.black); //设置时针的颜色 hour_graphics.translate(200, 200); //进行坐标系统变换,原点设在(200,200)处 minute_graphics = this.getGraphics(); //实例化分针Graphics对象 minute_graphics.setColor(Color.blue); //设置分针的颜色 minute_graphics.translate(200, 200); //进行坐标系统变换,原点设在(200,200)处 second_graphics = this.getGraphics(); //实例化秒针Graphics对象 second_graphics.setColor(Color.RED); //设置秒针的颜色 second_graphics.translate(200, 200); //进行坐标系统变换,原点设在(200,200)处 point_x[0] = 0; //各个时针12点处的位置坐标(按新坐标系的坐标) point_y[0] = -120; scan_x[0] = 0; //12点处的刻度位置坐标(按新坐标系的坐标) scan_y[0] = -140; double jiaodu = 6 * Math.PI / 180; //表盘分割成60分,将分割点的坐标存放在数组中 for (int i = 0; i < 60; i++) { point_x[i + 1] = point_x[i] * Math.cos(jiaodu) - Math.sin(jiaodu) * point_y[i]; point_y[i + 1] = point_y[i] * Math.cos(jiaodu) + point_x[i] * Math.sin(jiaodu); } point_x[60] = 0; point_y[60] = -120; //表盘分割成60份,将分割点的坐标存放在绘制数组中 for (int i = 0; i < 60; i++) { scan_x[i + 1] = scan_x[i] * Math.cos(jiaodu) - Math.sin(jiaodu) * scan_y[i]; scan_y[i + 1] = scan_y[i] * Math.cos(jiaodu) + Math.sin(jiaodu) * scan_x[i]; } scan_x[60] = 0; scan_y[60] = -140; } public void start() { if (isStart >= 1) { SECOND_THREAD.interrupt(); //唤醒线程 MINUTE_THREAD.interrupt(); HOUR_THREAD.interrupt(); } HOUR_THREAD = new Thread(this); //创建时针线程 MINUTE_THREAD = new Thread(this); //创建分针线程 SECOND_THREAD = new Thread(this); //创建秒针线程 SECOND_THREAD.start(); //启动秒针线程 MINUTE_THREAD.start(); //启动分针线程 HOUR_THREAD.start(); //启动时针线程 isStart++; if (isStart >= 2) isStart = 1; } public void stop() { SECOND_THREAD.interrupt(); //唤醒线程 MINUTE_THREAD.interrupt(); HOUR_THREAD.interrupt(); } public void paint(Graphics g) { //绘制图形 this.start(); g.drawOval(50, 50, 300, 300); //表盘的外圈 g.translate(200, 200); //进行坐标系统变换 for (int i = 0; i < 60; i++) { //绘制表盘的小刻度和大刻度 if (i % 5 == 0) { g.setColor(Color.BLACK); //设置颜色 g.fillOval((int) scan_x[i], (int) scan_y[i], 10, 10); } else g.fillOval((int) scan_x[i], (int) scan_y[i], 5, 5); } } public void run() { //实现Thread的方法,开始线程 Date date = new Date(); //获取本地时间 String s = date.toString(); current_hour = Integer.parseInt(s.substring(11, 13));//获得当前时间的小时 current_minute=Integer.parseInt(s.substring(14,16));//获取当前时间的分钟 current_second=Integer.parseInt(s.substring(17,19));//获取当前时间的秒钟 if (Thread.currentThread() == SECOND_THREAD){ //如果当前线程是秒线程 second_a = (int) point_x[current_second]; //秒针初始化 second_b = (int) point_x[current_second]; //用背景色清除前一秒的秒针 second_graphics.drawLine(0, 0, second_a, second_b); second_graphics.drawString("秒", second_a, second_b); int i = current_second; while (true) { try { SECOND_THREAD.sleep(1000); //每隔1秒休眠 Color c = getBackground(); //获取背景颜色 second_graphics.setColor(c); //设置秒针的颜色 //用背景色清除前一秒的秒针 second_graphics.drawLine(0, 0, second_a, second_b); second_graphics.drawString("秒", second_a, second_b); //秒针与分针重合,恢复分针显示 if ((second_a == minute_a) && (second_b == minute_b)) { //用背景色清除前一分的分针 minute_graphics.drawLine(0, 0, minute_a, minute_b); minute_graphics.drawString("分", minute_a, minute_b); } //秒针与时针重合,恢复时针的显示 if ((second_a == hour_a) && (second_b == hour_b)) { //用背景色清除前一时的时针 hour_graphics.drawLine(0, 0, hour_a, hour_b); hour_graphics.drawString("时", hour_a, hour_b); } } catch (InterruptedException e) { //捕获异常 Color c = getBackground(); //获取背景颜色 second_graphics.setColor(c); //设置秒针的颜色 //用背景色清除秒针 second_graphics.drawLine(0, 0, second_a, second_b); second_graphics.drawString("秒", second_a, second_b); return; } second_a = (int) point_x[(i + 1) % 60]; //秒针向前走一个单位 second_b = (int) point_y[(i + 1) % 60]; //每一秒走6度(一个单位格) second_graphics.setColor(Color.red); //绘制秒针的颜色 //用背景色清除前一秒的秒针 second_graphics.drawLine(0, 0, second_a, second_b); second_graphics.drawString("秒", second_a, second_b); i++; } } if (Thread.currentThread() == MINUTE_THREAD) { //如果当前线程是分线程 minute_a = (int) point_x[current_minute]; minute_b = (int) point_y[current_minute]; minute_graphics.drawLine(0, 0, minute_a, minute_b); int i = current_minute; //获取当前分钟 while (true) { try { //第一次过60秒就前进一分钟,以后每隔60秒前进一分钟 MINUTE_THREAD.sleep(1000 * 60 - current_second * 1000); current_second = 0; Color c = getBackground(); //获取背景颜色 minute_graphics.setColor(c); //设置分针的颜色 minute_graphics.drawLine(0, 0, minute_a, minute_b); minute_graphics.drawString("分", minute_a, minute_b); //如果时针和分针重合 if ((hour_a == minute_a) && (hour_b == minute_b)) { hour_graphics.drawLine(0, 0, minute_a, minute_b); hour_graphics.drawString("时", hour_a, hour_b); } } catch (InterruptedException e) { return; } minute_a = (int) point_x[(i + 1) % 60]; //分针向前走一个单位 minute_b = (int) point_y[(i + 1) % 60]; //每一分走6度(一个单位格) minute_graphics.setColor(Color.BLUE); //绘制分针的颜色 minute_graphics.drawLine(0, 0, minute_a, minute_b); minute_graphics.drawString("分", minute_a, minute_b); i++; current_second = 0; } } if (Thread.currentThread() == HOUR_THREAD) { //如果当前线程是时线程 int h = current_hour % 12; hour_a = (int) point_x[h * 5 + current_minute / 12]; hour_b = (int) point_y[h * 5 + current_minute / 12]; int i = h * 5 + current_minute / 12; hour_graphics.drawLine(0, 0, hour_a, hour_b); hour_graphics.drawString("时", hour_a, hour_b); while (true) { try { //第一次过12-minute%12分钟就前进一个刻度,以后每隔12分钟前进一刻度 HOUR_THREAD.sleep(1000 * 60 * 12 - 1000 * 60 * (current_minute % 12) - current_second * 1000); current_minute = 0; Color c = getBackground(); hour_graphics.setColor(c); hour_graphics.drawLine(0, 0, hour_a, hour_b); hour_graphics.drawString("时", hour_a, hour_b); } catch (InterruptedException e) { return; } hour_a = (int) point_x[(i + 1) % 60]; hour_b = (int) point_y[(i + 1) % 60]; hour_graphics.setColor(Color.BLACK); hour_graphics.drawLine(0, 0, hour_a, hour_b); hour_graphics.drawString("时", hour_a, hour_b); i++; current_minute = 0; } } } }
(3)运行结果如下所示:
源程序解读
(1)TextClock类继承Applet类实现Runnable接口,继承Applet类需要扩展init()、start()、stop()以及destroy()方法,实现接口必须实现run()方法。类中声明存放表盘刻度的横纵坐标数组和供绘制表盘用的横纵坐标数组以及其他变量。
(2)init()方法创建时针、分针以及秒针图形对象,并设置时针图形颜色为黑色,分针图形颜色为蓝色,秒针图形颜色为红色。Translate()方法进行坐标系统变换,将原点设置在(200,200处)。设置表盘刻度的横纵坐标与供绘制表盘用的横纵坐标的初始位置。运用循环将表盘分割成60等份,并将分割好的坐标存放在数组中。再将绘制表盘分割成60等份,分割好的坐标放在绘制表盘的数组中。
(3)start()方法判断标识是否大于1来唤醒秒针、分针与时针的线程。创建时针、分针与秒针线程对象并启动线程。stop()方法唤醒秒针、分针与时针线程。
(4)paint()方法用来绘制图形。drawOval()方法用来绘制表盘的外圈,translate()方法进行坐标系统的变换。运用循环绘制表盘的大刻度与小刻度以及表盘的颜色。
(5)run()方法获得本地时间并将时间转成字符串,根据字符串的substring()方法对时间字符串进行截取,截取出本地的时分秒。如果当前的线程为秒线程,则将当前的秒数放在存放秒的数组中,并用背景色清除前一秒的秒针,这样就形成了秒针的移动。秒针每隔1秒移动一次,如果秒针运动的坐标与分针重合,则恢复分针的显示;如果秒针运动的坐标与时针重合,则恢复时针的显示。秒针向前走一个单位,每一秒走的角度为6度,每走一步则用背景色清除前一秒的秒针。如果当前的线程为分线程,则将当前的分数放在存放分的数组中,并用背景色清除前一分钟的分针,这样就形成了分针的移动。分针第一次过60秒数就前进一次,以后每隔60秒移动一次,如果分针运动的坐标与时针重合,则恢复时针的显示。如果当前的线程为时线程,则将当前的时数放在存放时的数组中,并用背景色清除前一时的时针,这样就形成了时针的移动。时针第一次过(12-分数%12)便前进一个刻度,以后每隔12分钟就移动一次。
实例135 模拟生产者与消费者
生产者与消费者是运用多线程协作,其中生产者不断生产产品,将这些产品放入指定的仓库(或大的容器);消费者是从仓库中取得产品。当仓库中产品放满时,生产者则需要停止生产;当仓库中没有产品时,消费者则需要停止消费,除非仓库中有产品。本实例介绍生产者不断采集鲜花放入花篮中,消费者不断从花篮中取出鲜花。
技术要点
运用多线程实现模拟生产者与消费者的技术要点如下:
• 有synchronized的地方不一定有wait()与notify()方法,有wait()与notify()方法的地方必有synchronized,这是因为wait()方法和notify()方法不是属于线程类,而是每一个对象都具有的方法,而且,这两个方法都和对象锁有关,有锁的地方,必有synchronized。
• 调用wait()方法前的判断最好用while,而不用if;while可以实现被唤醒的线程再次作条件判断,而if则只能判断一次。
• 当仓库中产品数量为0时,调用wait()方法,使得当前消费者线程进入等待状态;当有新的产品时,调用notify()方法,唤醒等待的消费者线程;当仓库中的产品放满时,调用wait()方法,使得当前的生产者线程进入等待状态;当消费者取得产品时,调用notify()方法,唤醒等待的生产者线程。
实现步骤
(1)创建一个类名为TextProductAndConsume.java。
(2)代码如下所示:
package com.zf.s14; //创建一个包 import java.util.Random; class Flower { //缓存数据,用于存与取数据的类(花篮) private final String[] data; private int tail; //下一个放put的地方 private int head; //下一个放flower的地方 private int count; //缓存内的鲜花数 public Flower(int count) { //构造方法进行初始化 this.data = new String[count]; //创建字符串数组 this.head = 0; this.tail = 0; this.count = 0; } public synchronized void put(String flower) { //放置鲜花 System.out.println("当前线程" + Thread.currentThread().getName() + "放置鲜花" + flower); try { while (count >= data.length) { wait(); //线程等待 } data[tail] = flower; //放置鲜花于数组 tail = (tail + 1) % data.length; count++; notifyAll(); } catch (Exception e) { //捕获异常 System.out.println("放置鲜花出现错误:"+e.getMessage()); } } public synchronized String take() { //取出鲜花 String flower = null; try { while (count <= 0) { wait(); //线程等待 } flower = data[head]; //取出指定的鲜花 head = (head + 1) % data.length; count--; //数组个数减1 notifyAll(); } catch (Exception e) { //捕获异常 System.out.println("取出鲜花出现错误:"+e.getMessage()); } System.out.println("当前线程"+Thread.currentThread().getName() + "取出鲜花" + flower); return flower; } } class ProductThread extends Thread { //生产者线程类 private final Random random; private final Flower flower; private static int id = 0; //鲜花的流水号 public ProductThread(String name, Flower flower, long seed) {//构造方法进行初始化 super(name); this.flower = flower; this.random = new Random(seed); } public void run() { //实现Thread类的方法,启动线程 try { while (true) { Thread.sleep(random.nextInt(1000));//随机休眠 String flowerID = "鲜花流水号" + nextId() ; flower.put(flowerID); //放置鲜花 } } catch (Exception e) { //捕获异常 } } private static synchronized int nextId() { return id++; } } class ConsumeThread extends Thread { //消费者线程类 private final Random random; private final Flower flower; //构造方法进行初始化 public ConsumeThread(String name, Flower flower, long seed) { super(name); this.flower = flower; this.random = new Random(seed); //创建随机对象 } public void run() { //实现Thread类的方法,启动线程 try { while (true) { String flower = this.flower.take(); Thread.sleep(random.nextInt(1000)); } } catch (Exception e) { //捕获异常 System.out.println("消费者取出鲜花出错:"+e.getMessage()); } } } public class TextProductAndConsume { //操作用多线程实现生产者与消费者的类 public static void main(String[] args) { //Java程序主入口处 Flower flower=new Flower(5); //创建可以放置5朵鲜花的花篮 new ProductThread("ProductThread-1", flower, 001).start();//创建实例并启动线程 new ProductThread("ProductThread-2", flower, 002).start(); new ProductThread("ProductThread-3", flower, 003).start(); new ProductThread("ProductThread-4", flower, 004).start(); new ProductThread("ProductThread-5", flower, 005).start(); new ConsumeThread("ConsumeThread-1", flower, 101).start(); new ConsumeThread("ConsumeThread-2", flower, 102).start(); new ConsumeThread("ConsumeThread-3", flower, 103).start(); new ConsumeThread("ConsumeThread-4", flower, 104).start(); new ConsumeThread("ConsumeThread-5", flower, 105).start(); } }
(3)运行结果如下所示:
当前线程ProductThread-2放置鲜花,鲜花流水号0 当前线程ConsumeThread-5取出鲜花,鲜花流水号0 当前线程ProductThread-2放置鲜花,鲜花流水号1 当前线程ConsumeThread-5取出鲜花,鲜花流水号1 当前线程ProductThread-5放置鲜花,鲜花流水号2 当前线程ConsumeThread-4取出鲜花,鲜花流水号2 当前线程ProductThread-2放置鲜花,鲜花流水号3 当前线程ConsumeThread-1取出鲜花,鲜花流水号3 当前线程ProductThread-5放置鲜花,鲜花流水号4 当前线程ConsumeThread-3取出鲜花,鲜花流水号4 当前线程ProductThread-2放置鲜花,鲜花流水号5 当前线程ConsumeThread-2取出鲜花,鲜花流水号5 当前线程ProductThread-3放置鲜花,鲜花流水号6 当前线程ProductThread-4放置鲜花,鲜花流水号7 当前线程ConsumeThread-3取出鲜花,鲜花流水号6 当前线程ConsumeThread-5取出鲜花,鲜花流水号7 当前线程ProductThread-2放置鲜花,鲜花流水号8 ......
源程序解读
(1)生产者线程ProductThread类在其构造方法中设置花篮和创建随机对象,run()方法根据为真的条件进行循环,每隔随机生成的毫秒后便生产一个鲜花产品,并调用put()方法将产品存入到花篮中。消费者线程ConsumeThread类在其构造方法中设置花篮和创建随机对象, run()方法根据为真条件进行循环,每隔随机生成的毫秒数调用take()方法从花篮中取走一个鲜花产品。
(2)Flower类负责存入与取走产品。put()方法负责存入产品。当花篮中的鲜花满时,当前线程进入等待状态,即当生产者线程在调用put()方法来存入产品时,如果发现篮中鲜花已满便不生产鲜花,生产者线程进入等待状态;如果篮中鲜花未满,当向篮中放鲜花时,调用notify()方法,唤醒等待的消费者线程。take()方法用来取走鲜花产品。如果发现篮中没有鲜花,则当前消费者线程进入等待状态;如果篮中有鲜花,还有可以放鲜花的地方,则在取走鲜花时,调用notify()方法,唤醒等待的生产者线程。
(3)在类的main()主方法中建立一个大花篮,并为花篮关联了5个生产者线程和5个消费者线程,启动这些线程,便可以模拟生产者消费者。
实例136 仿迅雷下载文件
很多人喜欢用下载工具下载电影或其他音乐,其中迅雷就是一款很好的多线程下载工具。本实例介绍运用多线程制作一个简单的仿迅雷的下载工具来下载文件。
技术要点
制作一仿迅雷下载工具来下载文件的技术要点如下:
• 多线程下载工具,可以并发下载同一个文件,每个线程下载文件的一部分,最后再组合成一个完整的文件。在实现多线程下载时,在下载、线程启动时记录或给予一定的日志信息或提示。
• 在下载工具中设置目标文件的路径和下载文件的另存路径,设置一个区域显示下载文件时线程的信息以及其他相关内容。
实现步骤
(1)创建三个类,名称分别为FileSplit.java、DownLoadFile.java和TextXunleiFrame.java。
(2)代码如下所示:
/**--------文件名:FileSplit.java-------------*/ package com.zf.s14; //创建一个包 import javax.swing.JTextArea; //引入类 import java.net.*; import java.io.*; public class FileSplit extends Thread { String downloadURL; //下载文件的地址 long startPosition; //线程的开始位置 long endPosition; //线程的结束位置 int threadID; JTextArea textArea = new JTextArea(); //创建文本域 boolean isDone = false; //是否下载完毕 RandomAccessFile random; public FileSplit(String downloadURL, String saveAs, long nStart, long nEnd, int id, JTextArea textArea) { this.downloadURL = downloadURL; this.startPosition = nStart; this.endPosition = nEnd; this.threadID = id; this.textArea = textArea; try { random=new RandomAccessFile(saveAs,"rw"); //创建随机访问对象,以读/写方式 random.seek(startPosition); //定位文件指针到startPosition位置 } catch (Exception e) { //捕获异常 System.out.println("创建随机访问对象出错:"+e.getMessage()); } } public void run() { //实现Thread类的方法 try { URL url = new URL(downloadURL); //根据网址创建URL对象 HttpURLConnection httpConnection = (HttpURLConnection) url .openConnection(); //创建远程对象连接对象 String sProperty = "bytes=" + startPosition + "-"; httpConnection.setRequestProperty("RANGE", sProperty); textArea.append("\n 线程" + threadID + "下载文件! 请等待..."); InputStream input = httpConnection.getInputStream();//获得输入流对象 byte[] buf = new byte[1024]; //创建字节数据存储文件的数据 int splitSpace; splitSpace =(int)endPosition-(int)startPosition; //获得每个线程的间隔 if (splitSpace > 1024) splitSpace = 1024; //读取文件信息 w hile (input.read(buf, 0, splitSpace) > 0 && startPosition < endPosition){ splitSpace = (int) endPosition - (int) startPosition; if (splitSpace > 1024) splitSpace = 1024; textArea.append("\n线程: " + threadID + " 开始位置: " + startPosition + ", 间隔长度: " + splitSpace); random.write(buf, 0, splitSpace); //写入文件 startPosition += splitSpace; //开始位置改变 } textArea.append("\n 线程" + threadID + "下载完毕!!"); random.close(); //释放资源 input.close(); isDone = true; } catch (Exception e) { //捕获异常 System.out.println("多线程下载文件出错:"+e.getMessage()); } } } /**--------文件名:DownLoadFile.java-------------*/ package com.zf.s14; //创建一个包 import javax.swing.JTextArea; //引入类 import java.net.*; public class DownLoadFile extends Thread { //分析下载的文件并启动下载进程 String downloadURL; //下载文件的地址 String saveFileAs; //文件另存为 int threadCount; //线程总数 String log = new String(); //下载过程的日志记录 JTextArea textArea = new JTextArea(); //创建文本域 long[] position; long[] startPosition; //每个线程开始位置 long[] endPosition; //每个线程结束位置 FileSplit[] FileSplitt; //子线程对象 long fileLength; //下载的文件的长度 public DownLoadFile(String downloadURL, String saveFileAs, int threadCount, JTextArea textArea){ //构造方法进行初始化 this.downloadURL = downloadURL; this.saveFileAs = saveFileAs; this.threadCount = threadCount; this.textArea = textArea; startPosition = new long[threadCount]; endPosition = new long[threadCount]; } public void run() { //实现Thread类的方法 log = "目标文件:" + downloadURL; textArea.append("\n" + log); //日志写入文本域 log = "\n 线程总数:" + threadCount; textArea.append("\n" + log); try { fileLength = getFileSize(); //获得文件长度 if (fileLength == -1) { //不可获取文件长度或没找到资源 textArea.append("\n 不可知的文件长度!请重试!!"); } else { if (fileLength == -2) { //无法获取文件或没有找到资源 textArea.append("\n 文件无法获取,没有找到指定资源,请重试!!"); } else { //循环对每个线程的开始位置赋值 for (int i = 0; i < startPosition.length; i++) { startPosition[i] = (long)(i * (fileLength / startPosition.length)); } for (int i= 0;i<endPosition.length -1;i++) //循环对每个线程的结束位置赋值 endPosition[i] = startPosition[i + 1]; //最后一线程结束位置是文件长度 endPosition[endPosition.length - 1] = fileLength; for(int i=0;i<startPosition.length;i++){ //循环显示每线程开始和结束位置 log = "线程:" + i + "下载范围:" + startPosition[i] + "--" + endPosition[i]; textArea.append("\n" + log); } FileSplitt = new FileSplit[startPosition.length]; for(int i=0;i<startPosition.length;i++){ //启动一组子线程 FileSplitt[i] = new FileSplit(downloadURL, saveFileAs, startPosition[i], endPosition[i], i, textArea); log = "线程 " + i + "启动"; textArea.append("\n" + log); FileSplitt[i].start(); //启动线程
} boolean breakWhile = true; while (breakWhile) { //当条件始终为true时进行循环 Thread.sleep(500); //线程休眠 breakWhile = false; for (int i = 0; i < FileSplitt.length; i++) { if (!FileSplitt[i].isDone) { //循环判断每个线程是否结束 breakWhile = true; break; } } } textArea.append("\n文件传输结束!");//文件传输结束 } } } catch (Exception ex) { //捕获异常 ex.printStackTrace(); } } public long getFileSize() { //获得文件的长度的方法 int fileLength = -1; try { URL url = new URL(downloadURL); //根据网址创建URL对象 HttpURLConnection httpConnection = (HttpURLConnection) (url .openConnection()); //创建远程对象连接对象 int responseCode = httpConnection.getResponseCode(); if (responseCode >= 400) { //没有获得响应信息 System.out.println("Web服务器响应错误"); return -2; //Web服务器响应错误 } String sHeader; for (int i = 1;; i++) { //查标识文件长度文件头获文件长度 sHeader = httpConnection.getHeaderFieldKey(i); if (sHeader != null) { if (sHeader.equals("Content-Length")){ //查找标识文件长度的文件头 fileLength = Integer.parseInt(httpConnection .getHeaderField(sHeader)); break; } } else { break; } } } catch (Exception e) { //捕获异常 System.out.println("无法获得文件长度:"+e.getMessage()); } return fileLength; } } /**--------文件名:TextXunleiFrame.java-------------*/ package com.zf.s14; //创建一个包 import javax.swing.*; //引入类 import java.awt.*; import java.awt.event.*; public class TextXunleiFrame extends JFrame { //模拟迅雷下载的界面面板 private JPanel contentPane; //迅雷面板 private JTextField webField = new JTextField(); //下载地址的文本框 private JTextField localFile = new JTextField(); //下载到本地的文本框 private JButton button = new JButton(); //下载按钮 private JLabel webLabel = new JLabel(); //目标标签 private JLabel localLabel = new JLabel(); //下载到本地标签 private JTextArea textArea = new JTextArea(); //显示下载记录的文本域 private String downloadURL = new String(); //下载地址 private String saveFileAs = new String(); //另存为 public TextXunleiFrame() { //构造方法进行初始化 enableEvents(AWTEvent.WINDOW_EVENT_MASK); try { toInit(); //调用方法初始化面板 } catch (Exception ex) { ex.printStackTrace(); } } private void toInit() throws Exception { //初始化面板 contentPane = (JPanel) this.getContentPane(); //创建面板 contentPane.setLayout(null); //没有设置面板布局 this.setSize(new Dimension(380, 320)); //面板的大小 this.setLocation(100, 100); //面板位置 this.setTitle("仿迅雷多线程下载"); //面板标题 webField.setBounds(new Rectangle(150,200,200,20)); //设置文本框的位置 //设置默认下载路径 webField.setText("http://zhanjia.javaeye.com/topics/download/d3682cdf- 04be-3a57-89a4-93cbef5ae038"); localFile.setBounds(new Rectangle(150, 240, 120, 20)); //设置文本框的位置 localFile.setText("d:\\try.rar");//设置默认另存为 webLabel.setBounds(new Rectangle(20, 200, 120, 20)); //标签的位置 webLabel.setText("下载的目标文件为:"); localLabel.setBounds(new Rectangle(20, 240, 120, 20)); //标签的位置 localLabel.setText("下载的文件另存为:"); button.setBounds(new Rectangle(280, 240, 60, 20)); //按钮的位置 button.setText("下载"); button.addActionListener(new ActionListener() { //按钮添加监听事件 public void actionPerformed(ActionEvent e) { button_actionPerformed(e); //调用事件 } }); //创建有滑动条的面板将文本域放在上面 JScrollPane scrollPane = new JScrollPane(textArea); scrollPane.setBounds(new Rectangle(20, 20, 330, 170)); //面板的位置 textArea.setEditable(false); //不可编辑 contentPane.add(webField, null); //将文本框添加到面板中 contentPane.add(localFile, null); //将文本框添加到面板中 contentPane.add(webLabel, null); //将标签添加到面板中 contentPane.add(localLabel, null); //将标签添加到面板中 contentPane.add(button, null); //将按钮添加到面板中 contentPane.add(scrollPane, null); //将滑动条添加到面板中 downloadURL = webField.getText(); //获得文本框中的文本 saveFileAs = localFile.getText(); this.setDefaultCloseOperation(EXIT_ON_CLOSE); //设置默认关闭操作 } //单击事件触发方法,启动分析下载文件的进程 public void button_actionPerformed(ActionEvent e) { downloadURL = webField.getText(); //获得目标文件的网址 saveFileAs = localFile.getText(); //获得另存为的地址 if (downloadURL.compareTo("") == 0) textArea.setText("请输入要下载的文件完整地址"); else if(saveFileAs.compareTo("") == 0) { textArea.setText("请输入保存文件完整地址"); }else { try { DownLoadFile downFile = new DownLoadFile(downloadURL, saveFileAs, 5, textArea); //传参数实例化下载文件对象 downFile.start(); //启动下载文件的线程 textArea.append("主线程启动⋯⋯"); } catch (Exception ec) { //捕获异常 System.out.println("下载文件出错:"+ec.getMessage()); } } } public static void main(String[] args) { //Java程序主入口处 TextXunleiFrame frame=new TextXunleiFrame(); //实例化对象进行初始化 frame.setVisible(true); //设置窗口可视 } }
(3)运行结果如下所示:
源程序解读
(1)FileSplit类继承Thread线程类扩展run()方法。类的构造方法中获得目标文件的路径、线程的开始和结束位置以及获得线程的编号和文本域。以读/写的方式创建随机访问对象并设置读取文件的指针位置。
(2)FileSplit类的run()方法根据目标文件的路径创建URL对象,其openConnection()方法创建远程连接对象。setRequestProperty()方法设置断点续传的开始位置。getInputStream()方法获得输入流。创建字节数组用来存储文件的数据信息。根据输入流中读取的数据大于0以及开始位置小于结束位置进行循环,在文本域中添加线程的处理信息,并根据随机访问对象的write()方法将读取的内容写入文件。写入完毕后释放相关流资源。
(3)DownLoadFile类继承Thread类扩展run()方法。在其构造方法中获得目标文件的路径、下载文件的另存为路径、线程的个数、文本域以及创建线程开始与结束位置数组。
(4)DownLoadFile类的run()方法,调用getFileSize()方法获得目标文件的长度。如果长度为-1,则表明文件的长度不可知;如果长度为-2,则表明无法获得资源文件。根据开始与结束位置的数组长度循环对每个线程的开始与结束位置进行赋值,最后一个线程的结束位置的长度为文件的长度。再运用循环显示每个线程的开始与结束位置,并将这些信息写入文本域中。根据线程开始位置的数组的长度创建FileSplit对象数组。循环对对象数组的每个子元素进行实例化,其每个子元素实际上是一个线程,启动线程。利用标识为真进行循环,查看每个子线程是否执行完毕,如果子线程执行完毕则跳出循环。
(5)DownLoadFile类的getFileSize()方法用来获得目标文件的长度。根据目标文件路径创建URL对象。其openConnection()方法创建远程连接对象。getResponseCode()方法获得响应的信息,如果响应的值超过400则不能获得Web服务器的信息,即不能获得目标文件资源。getHeaderFieldKey()方法获取文件头部信息。如果文件头部信息为Content-Length,则根据getHeaderField()方法获得相应文件的长度。
(6)TextXunleiFrame类继承JFrame类实现仿迅雷的界面。其构造方法中enableEvents()方法启用由传递给此组件的指定事件屏蔽参数所定义的事件。WINDOW_EVENT_MASK参数用于选择窗口事件的事件掩码。调用toInit()方法初始化界面面板。
(7)TextXunleiFrame类的toInit()方法创建面板并设置面板的布局为空,这样可以在面板中随意地摆放组件。设置默认的下载地址与默认的另存的文件路径。创建下载按钮并对按钮添加监听器。当单击按钮时会触发button_actionPerformed()方法。创建滑动条面板并将文本域放置在滑动面板上,当文本域中的内容超过设置的行数时会出现滑动条。文本域设置为不可编辑。
(8)button_actionPerformed()方法获得目标文件的路径和另存为的路径并判断它们是否为空或空字符串。如果都不是空或空字符串,则根据目标文件路径与另存为路径以及相关参数创建DownLoadFile对象,由于DownLoadFile继承Thread线程类,则其为一个线程,启动线程开始下载文件。文本域中显示文件下载过程的信息。