JAVA的多线程【转】
当计算机处于DOS时代时,程序几乎是没有界面的,而且由于计算机运行速度等原因,那个时代的计算机只能启动一个程序,只有当该程序退出以后才可以执行其它的程序。但是随着计算机性能的提高,以及软件的丰富,如果计算机还只能同时执行一个程序的话,那么计算机恐怕是很多人都不能接受的。
这种在任何一个时间点,可以有多个程序同时执行,或者有多个程序逻辑同时执行的能力,成为并发执行。
现在计算机早已进入到并发执行的时代,对于程序编程来说,进行并发执行的程序编写也就被称作并发编程,在Java语言中,同一个程序内部的并发处理由线程这个概念来实现。
12.1 多线程简介
从小时候开始,老师就教育大家——“一心不可二用”,这是指做一件事情的时候一定要专注,不能够分心。但是在程序编程的领域却早已经需要做到“一心二用”甚至“一心多用”了。下面来看一下线程的概念吧!
12.1.1 进程和线程
在介绍线程的概念以前,首先介绍一下进程的概念。
进程(Process)指操作系统中一个独立运行的程序。例如在计算机中,同时运行着QQ、Word、MSN等,那么QQ程序是一个进程,MSN程序也是一个进程。在Windows操作系统中的任务管理器中,就可以清晰的看到当前操作系统中正在运行的进程信息。
进 程,也称任务,所以支持多个进程同时执行的操作系统就被称作多进程操作系统或多任务操作系统,现在主流的操作系统都属于这种类型。在操作系统中,每个进程 拥有独立的内存空间等系统资源,进程和进程之间的系统资源不互用,所以进程之间的通信比较麻烦。通过在操作系统上同时运行多个进程,可以充分发挥计算机的 硬件能力,更方便用户使用,也使得各种各样的程序大量出现。
对于只有一个CPU的计算机来说,是如何实现同时执行多个进程的呢?其实CPU采用的原理就是分时执行,每个进程处于操作系统的进程队列中。然后每个进程依次获得一个时间片进入CPU进行执行,在该时间片执行完成以后,该进程保存自身状态,退出CPU,然后其它的进程进入CPU继续执行。由于时间片的时间很短,例如Windows操作系统的时间片是20ms,所以在计算机用户看来程序就是同时执行的,而实际的执行方式是穿插依次执行的。而对于多CPU的计算机来说,只是排队的队列增加了几个而已,每个队列的实现方式和上面的介绍类似。
但是进程的概念相对比较大,而且需要成为一个独立的程序,这样对于编程来说比较麻烦,所以在程序开发中设计了另外一个概念——线程。
线程(Thread)指同一个程序(进程)内部每个单独执行的流程。在前面的程序中每个程序内部都只包含一个系统流程,该流程从main方法开始,随着方法的调用进入到每个方法的内部,在方法调用完成以后返回到调用的位置,直到main方法结束以后则该流程结束,这个流程就是前面程序中的系统线程。Java语言对于线程的概念提供了良好的支持,在编程中实际使用线程也显得比其它语言要简单一些。
而在实际实现时,Java语言支持在一个程序内部同时执行多个流程,其中每个单独的流程就是一个线程。例如在QQ程 序中,系统的线程负责响应用户的按键操作,在后台可以启动网络通讯的线程执行数据的发送和接收,这样两个流程之间同时执行,并协调进行工作。而在服务器端 程序中,每个和服务器进行通讯的客户端,在服务器端都会启动一个对应的线程进行通讯,这样每个客户端才显得同时和服务器端进行通讯。
在很多地方,线程被看作是一种“轻量级进程”,因为使用线程和进程的改变比较类似,而且使用线程时对于系统资源,如内存、CPU等,的占用要比进程小很多,也就是有更小的系统开销。另外,同一个程序中的线程之间变量是共享的,线程之间的数据交换要比进程之间的数据交换简单一些。
总之,无论是进程的概念还是线程的概念,都使编程从串行编程(依次执行)进入到并行编程(同时执行)的领域,而在CPU内部实现的原理都是按照时间片进行切换。
12.1.2 多线程优势
线程的概念增加了编程的难度,也增加了程序的复杂度,但是该概念还是在程序内部大量进行使用,这主要因为多线程程序的优势。
多线程程序主要的优势有两个:
1、 提高界面程序响应速度
通 过使用线程,可以将需要大量时间完成的流程在后台完成,例如现在常见的网络程序,在进行网络通讯时都需要使用单独的流程进行,也就是启动一个单独的线程进 行,这样不会阻塞系统线程的执行,也就是不会阻塞对于界面的操作。另外,如果需要大量操作数据或进行数据变换的程序,也需要在后台启动单独的线程来提高前 台界面的响应速度。
通过将程序逻辑独立成一个单独的线程,使得控制界面的系统线程和逻辑线程同时执行,避免了逻辑操作需要大量的时间阻塞系统的线程执行,从而大幅度提高界面程序的响应速度。
2、 充分利用系统资源
通过在一个程序内部同时执行多个流程,可以充分利用CPU等系统资源,从而最大限度的发挥硬件的吸能。就像一个人同时承担多份工作一样,这样可以使这个人的时间获得比较充分的使用。
当然,多线程程序也有一些不足,例如当程序中的线程数量比较多时,系统将花费大量的时间进行线程的切换,这反而会降低程序的执行效率。
但是,相对于优势来说,劣势还是很有限的,所以在现在的项目开发中,多线程编程技术获得了广泛的使用。
12.1.3 线程生命周期
线程作为一个全新的概念,主要由系统进行管理,但是熟悉线程概念的各个阶段,是控制线程程序执行的基础,和以后学习的其它Java技术类似,线程在程序中从出现到消亡的各个阶段,在程序中统称为线程的生命周期。
在Java语言中线程的概念由java.lang.Thread类实现,在该类中封装线程的概念,并且将线程控制的相关方法包含在该类的内部。后续介绍中如果没有特别说明,则提到的方法均是Thread类内部的方法。
线程的生命周期中包含如下阶段:
1、 新建状态(New)
该状态指线程已经初始化完成,但是还没有启动。具体点说,也就是线程对象已经创建,准备工作已经完成。
2、 运行状态(Run)
运行状态是指线程的正常执行状态,处于该状态的线程在CPU内部执行程序,也就是线程正常运行时的状态。
3、 阻塞状态(Block)
阻塞状态指线程处于执行状态,但是由于没有获得CPU的执行时间,而处于CPU外部等待线程执行的状态。
4、 死亡状态(Dead)
死亡状态指线程执行结束,释放线程占用的系统资源,结束线程执行的状态。
在实际使用线程时,首先需要创建一个线程对象,在线程对象创建完成以后,该线程就处于新建状态了,在新建状态下的线程,已经初始化完成,但是还没有启动,也就是不会获得CPU的执行时间。在新建状态下,一般可以通过调用线程对象中的start方法,使线程进入到运行状态,start方法不阻塞程序的执行,在调用完成以后立刻就返回了。一旦线程进入运行状态,则开始排队进入CPU执行,根据系统的调度,线程就在运行状态和阻塞状态之间进行切换,这就是线程的执行状态。当线程执行完成或需要结束该流程时,则需要将线程切换到死亡状态,释放线程占用的资源,结束线程的执行。
另外在线程执行的过程中也可以根据需要调用Thread类中对应的方法改变线程的状态。例如使用线程对象的interrupt中断线程的执行,使线程进入到死亡状态;使用yield方法使当前正在执行的线程从运行状态切换到阻塞状态。
而具体线程编程的实现方式、线程的控制以及线程编程时需要注意的问题,则将在下面进行详细的介绍。
多线程实现方式
线程的概念虽然比较复杂,但是在Java语言中实现线程却比较简单,只需要按照Java语言中对于线程的规定进行编程即可。
在实现线程编程时,首先需要让一个类具备多线程的能力,继承Thread类或实现Runnable接口的类具备多线程的能力,然后创建线程对象,调用对应的启动线程方法开始执行即可实现多线程编程。
在一个程序中可以实现多个线程,多线程编程指在同一个程序中启动了两个或两个以上的编程形式。当启动的线程数量比较多时,对于系统资源的要求比较多,所以程序支持的最大线程数量和计算机的硬件配置相关。
在实际实现线程时,Java语言提供了三种实现方式:
1、 继承Thread类
2、 实现Runnable接口
3、 使用Timer和TimerTask组合
下面依次介绍每种实现方式的代码编写,以及各种实现之间的区别比较。
12.2.1 继承Thread类
如果一个类继承了Thread类,则该类就具备了多线程的能力,则该类则可以以多线程的方式进行执行。
但是由于Java语言中类的继承是单重继承,所以该方式受到比较大的限制。
下面以一个简单的示例介绍该种多线程实现方式的使用以及启动线程的方式。示例代码如下所示:
/**
* 以继承Thread的方式实现线程
*/
public class FirstThread extends Thread{
public static void main(String[] args) {
//初始化线程
FirstThread ft = new FirstThread();
//启动线程
ft.start();
try{
for(int i = 0;i < 10;i++){
//延时1秒
Thread.sleep(1000);
System.out.println("main:" + i);
}
}catch(Exception e){}
}
public void run(){
try{
for(int i = 0;i < 10;i++){
//延时1秒
Thread.sleep(1000);
System.out.println("run:" + i);
}
}catch(Exception e){}
}
}
在该程序中,通过使FirstThread继承Thread类,则FirstThread类具备了多线程的能力,按照Java语言线程编程的规定,线程的代码必须书写在run方法内部或者在run方法内部进行调用,在示例的代码中的run方法实现的代码作用是每隔1秒输出一行文字。换句话说,run方法内部的代码就是自定义线程代码,或者说,自定义线程的代码必须书写在run方法的内部。
在执行FirstThread类时,和前面的执行流程一样。当执行FirstThread类时,Java虚拟机将开启一个系统线程来执行该类的main方法,main方法的内部代码按照顺序结构进行执行,首先执行线程对象的初始化,然后执行调用start方法。该行代码的作用是启动线程,在执行start方法时,不阻塞程序的执行,start方法的调用立刻返回,Java虚拟机以自己的方式启动多线程,开始执行该线程对象的run方法。同时系统线程的执行流程继续按照顺序执行main方法后续的代码,执行main方法内部的输出。
这样,在FirstThread执行时,就有了两个同时执行的流程:main流程和自定义run方法流程,换句专业点的话来说,就是该程序在执行时有两个线程:系统线程和自定义线程。这个同时执行可以从该程序的执行结果中获得更加直接的证明。
该程序的执行结果为:
run:0
main:0
main:1
run:1
main:2
run:2
main:3
run:3
main:4
run:4
main:5
run:5
main:6
run:6
main:7
run:7
main:8
run:8
main:9
run:9
从执行结果可以看到两个线程在同时执行,这将使我们进入多线程编程的时代,进入并发编程的领域,体会神奇的多线程编程的魔力。
由于两个线程中的延迟时间——1秒,是比较长的,所以看到的结果是线程规律执行的,其实真正的线程执行顺序是不能直接保证的,系统在执行多线程程序时只保证线程是交替执行的,至于那个线程先执行那个线程后执行,则无法获得保证,需要书写专门的代码才可以保证执行的顺序。
其实,上面的代码可以简化,简化以后的代码为:
/**
* 以继承Thread的方式实现线程2
* 使用方法简化代码
*/
public class SecondThread extends Thread{
public static void main(String[] args) {
//初始化线程
SecondThread ft = new SecondThread();
//启动线程
ft.start();
print("main:");
}
public void run(){
print("run:");
}
private static void print(String s){
try{
for(int i = 0;i < 10;i++){
//延时1秒
Thread.sleep(1000);
System.out.println(s + i);
}
}catch(Exception e){}
}
}
在该示例代码中,将重复的代码组织称print方法,分别在main方法和run方法内部调用该方法。需要特别强调的是,在run方法内部调用的方法,也会以多线程多线程的方式被系统执行,这样更加方便代码的组织。
其实在实际实现时,还可以把线程以单独类的形式出现,这样实现的代码如下所示:
/**
* 测试类
*/
public class Test {
public static void main(String[] args) {
//初始化线程
ThirdThread ft = new ThirdThread();
//启动线程
ft.start();
try{
for(int i = 0;i < 10;i++){
//延时1秒
Thread.sleep(1000);
System.out.println("main:" + i);
}
}catch(Exception e){}
}
}
/**
* 以继承Thread类的方式实现多线程3
* 以单独类的实现组织代码
*/
public class ThirdThread extends Thread {
public void run(){
try{
for(int i = 0;i < 10;i++){
//延时1秒
Thread.sleep(1000);
System.out.println("run:" + i);
}
}catch(Exception e){}
}
}
在该示例代码中,ThirdThread类是一个单独的线程类,在该类的run方法内部实现线程的逻辑,使用该种结构符合面向对象组织代码的方式。需要启动该线程时,和前面启动的方式一致。
一个类具备了多线程的能力以后,可以在程序中需要的位置进行启动,而不仅仅是在main方法内部启动。
对于同一个线程类,也可以启动多个相同的线程,例如以ThirdThread类为例,启动两次的代码为:
ThirdThread t1 = new ThirdThread();
t1.start();
ThirdThread t2 = new ThirdThread();
t2.start();
而下面的代码是错误的
ThirdThread t1 = new ThirdThread();
t1.start();
t1.start(); //同一个线程不能启动两次
当自定义线程中的run方法执行完成以后,则自定义线程将自然死亡。而对于系统线程来说,只有当main方法执行结束,而且启动的其它线程都结束以后,才会结束。当系统线程执行结束以后,则程序的执行才真正结束。
总之,继承Thread类可以使该类具备多线程的能力,需要启动该线程时,只需要创建该类的对象,然后调用该对象中的start方法,则系统将自动以多线程的发那个是执行该对象中的run方法了。
虽然该种方式受到Java语法中类的单重继承的限制,但是在实际的项目中还是获得了比较广泛的使用,是一种最基本的实现线程的方式。
12.2.2实现Runnable接口
一个类如果需要具备多线程的能力,也可以通过实现java.lang.Runnable接口进行实现。按照Java语言的语法,一个类可以实现任意多个接口,所以该种实现方式在实际实现时的通用性要比前面介绍的方式好一些。
使用实现Runnable接口实现多线程的示例代码如下:
/**
* 测试类
*/
public class Test2 {
public static void main(String[] args) {
//创建对象
MyRunnable mr = new MyRunnable();
Thread t = new Thread(mr);
//启动
t.start();
try{
for(int i = 0;i < 10;i++){
Thread.sleep(1000);
System.out.println("main:" + i);
}
}catch(Exception e){}
}
}
/**
* 使用实现Runnable接口的方式实现多线程
*/
public class MyRunnable implements Runnable {
public void run() {
try{
for(int i = 0;i < 10;i++){
Thread.sleep(1000);
System.out.println("run:" + i);
}
}catch(Exception e){}
}
}
该示例代码实现的功能和前面实现的功能相同。在使用该方式实现时,使需要实现多线程的类实现Runnable,实现该接口需要覆盖run方法,然后将需要以多线程方式执行的代码书写在run方法内部或在run方法内部进行调用。
在需要启动线程的地方,首先创建MyRunnable类型的对象,然后再以该对象为基础创建Thread类的对象,最后调用Thread对象的start方法即可启动线程。代码如下:
//创建对象
MyRunnable mr = new MyRunnable();
Thread t = new Thread(mr);
//启动
t.start();
在这种实现方式中,大部分和前面介绍的方式类似,启动的代码稍微麻烦一些。这种方式也是实现线程的一种主要方式。
12.2.3使用Timer和TimerTask组合
最后一种实现多线程的方式,就是使用java.util包中的Timer和TimerTask类实现多线程,使用这种方式也可以比较方便的实现线程。
在这种实现方式中,Timer类实现的是类似闹钟的功能,也就是定时或者每隔一定时间触发一次线程。其实,Timer类本身实现的就是一个线程,只是这个线程是用来实现调用其它线程的。而TimerTask类是一个抽象类,该类实现了Runnable接口,所以按照前面的介绍,该类具备多线程的能力。
在这种实现方式中,通过继承TimerTask使该类获得多线程的能力,将需要多线程执行的代码书写在run方法内部,然后通过Timer类启动线程的执行。
在实际使用时,一个Timer可以启动任意多个TimerTask实现的线程,但是多个线程之间会存在阻塞。所以如果多个线程之间如果需要完全独立运行的话,最好还是一个Timer启动一个TimerTask实现。
使用该种实现方式实现的多线程示例代码如下:
import java.util.*;
/**
* 测试类
*/
public class Test3 {
public static void main(String[] args) {
//创建Timer
Timer t = new Timer();
//创建TimerTask
MyTimerTask mtt1 = new MyTimerTask("线程1:");
//启动线程
t.schedule(mtt1, 0);
}
}
import java.util.TimerTask;
/**
* 以继承TimerTask类的方式实现多线程
*/
public class MyTimerTask extends TimerTask {
String s;
public MyTimerTask(String s){
this.s = s;
}
public void run() {
try{
for(int i = 0;i < 10;i++){
Thread.sleep(1000);
System.out.println(s + i);
}
}catch(Exception e){}
}
}
在该示例中,MyTimerTask类实现了多线程,以多线程方式执行的代码书写在该类的run方法内部,该类的功能和前面的多线程的代码实现类似。
而在该代码中,启动线程时需要首先创建一个Timer类的对象,以及一个MyTimerTask线程类的兑现,然后使用Timer对象的schedule方法实现,启动线程的代码为:
//创建Timer
Timer t = new Timer();
//创建TimerTask
MyTimerTask mtt1 = new MyTimerTask("线程1:");
//启动线程
t.schedule(mtt1, 0);
其中schedule方法中的第一个参数mtt1代表需要启动的线程对象,而第二个参数0则代表延迟0毫秒启动该线程,也就是立刻启动。
由于schedule方法比较重要,下面详细介绍一下Timer类中的四个schedule方法:
1、 public void schedule(TimerTask task,Date time)
该方法的作用是在到达time指定的时间或已经超过该时间时执行线程task。例如假设t是Timer对象,task是需要启动的TimerTask线程对象,后续示例也采用这种约定实现,则启动线程的示例代码如下:
Date d = new Date(2009-1900,10-1,1,10,0,0);
t. schedule(task,d);
则该示例代码的作用是在时间达到d指定的时间或超过该时间(例如2009年10月2号)时,启动线程task。
2、 public void schedule(TimerTask task, Date firstTime, long period)
该方法的作用是在时间到达firstTime开始,每隔period毫秒就启动一次task指定的线程。示例代码如下:
Date d = new Date(2009-1900,10-1,1,10,0,0);
t. schedule(task,d,20000);
该示例代码的作用是当时间达到或超过d指定的时间以后,每隔20000毫秒就启动一次线程task,这种方式会重复触发线程。
3、 public void schedule(TimerTask task,long delay)
该方法和第一个方法类似,作用是在执行schedule方法以后delay毫秒以后启动线程task。示例代码如下:
t. schedule(task,1000);
该示例代码的作用是在执行该行启动代码1000毫秒以后启动一次线程task。
4、 public void schedule(TimerTask task,long delay,long period)
该方法和第二个方法类似,作用是在执行schedule方法以后delay毫秒以后启动线程task,然后每隔period毫秒重复启动线程task。
例外需要说明的是Timer类中启动线程还包含两个scheduleAtFixedRate方法,这两个方法的参数和上面的第二个和第四个一致,其作用是实现重复启动线程时的精确延时。对于schedule方法来说,如果重复的时间间隔是1000毫秒,则实际的延迟时间是1000毫秒加上系统执行时消耗的时间,例如为5毫秒,则实际每轮的时间间隔为1005毫秒。而对于scheduleAtFixedRate方法来说,如果设置的重复时间间隔为1000毫秒,系统执行时消耗的时间为5毫秒,则延迟时间就会变成995毫秒,从而保证每轮间隔为1000毫秒。
介绍完了schedule方法以后,让我们再来看一下前面的示例代码,如果在测试类中启动两个MyTimerTask线程,一种实现的代码为:
import java.util.Timer;
/**
* 测试类
*/
public class Test4 {
public static void main(String[] args) {
//创建Timer
Timer t = new Timer();
//创建TimerTask
MyTimerTask mtt1 = new MyTimerTask("线程1:");
MyTimerTask mtt2 = new MyTimerTask("线程2:");
//启动线程
System.out.println("开始启动");
t.schedule(mtt1, 1000);
System.out.println("启动线程1");
t.schedule(mtt2, 1000);
System.out.println("启动线程2");
}
}
在该示例代码中,使用一个Timer对象t依次启动了两个MyTimerTask类型的对象mtt1和mtt2。而程序的执行结果是:
开始启动
启动线程1
启动线程2
线程1:0
线程1:1
线程1:2
线程1:3
线程1:4
线程1:5
线程1:6
线程1:7
线程1:8
线程1:9
线程2:0
线程2:1
线程2:2
线程2:3
线程2:4
线程2:5
线程2:6
线程2:7
线程2:8
线程2:9
从程序的执行结果可以看出,在Test4类中mtt1和mtt2都被启动,按照前面的schedule方法介绍,这两个线程均会在线程启动以后1000毫秒后获得执行。但是从实际执行效果却可以看出这两个线程不是同时执行的,而是依次执行,这主要是因为一个Timer启动的多个TimerTask之间会存在影响,当上一个线程未执行完成时,会阻塞后续线程的执行,所以当线程1执行完成以后线程2才获得了执行。
如果需要线程1和线程2获得同时执行,则只需要分别使用两个Timer启动TimerTask线程即可,启动的示例代码如下:
import java.util.Timer;
/**
* 测试类
*/
public class Test5 {
public static void main(String[] args) {
//创建Timer
Timer t1 = new Timer();
Timer t2 = new Timer();
//创建TimerTask
MyTimerTask mtt1 = new MyTimerTask("线程1:");
MyTimerTask mtt2 = new MyTimerTask("线程2:");
//启动线程
System.out.println("开始启动");
t1.schedule(mtt1, 1000);
System.out.println("启动线程1");
t2.schedule(mtt2, 1000);
System.out.println("启动线程2");
}
}
在该示例中,分别使用两个Timer对象t1和t2,启动两个TimerTask线程对象mtt1和mtt2,两者之间不互相干扰,所以达到了同时执行的目的。
在使用上面的示例进行运行时,由于Timer自身的线程没有结束,所以在程序输出完成以后程序还没有结束,需要手动结束程序的执行。例如在Eclipse中可以点击控制台上面的红色“Teminate”按钮结束程序。
12.2.4 小结
关于线程的三种实现方式,就简单的介绍这么多。其实无论那种实现方式,都可以实现多线程,在语法允许的前提下,可以使用任何一种方式实现。比较而言,实现Runnable接口方式要通用一些。
只是从语法角度介绍线程的实现方式,还是无法体会到线程实现的奥妙,下面将通过几个简单的示例来体会线程功能的强大,并体会并发编程的神奇,从而能够进入并发编程的领域发挥技术的优势。
2.3 多线程使用示例
多线程技术对于初学者来说,是编程思维的一种跳跃,在实际学习时,一定要熟悉线程的基础知识,掌握线程的实现方式,然后就是开始大量的进行实践,从实践中领悟线程编程的奥妙以及实现的原理。
下面通过几个常见的例子演示多线程的基本使用。
12.3.1 定时炸弹
定时炸弹是在电影中常见的一种装置,在该部分就使用多线程技术模拟该功能。实现的功能为:在程序启动以后进行倒计时,当60秒以后程序结束,在程序运行时可以在控制台输入quit控制线程(炸弹)的暂停。
在该示例程序中,开启了一个系统线程(main方法所在的线程),该线程的作用是启动模拟定时炸弹的线程,并且在控制台接受用户的输入,并判断输入的内容是否为quit,如果是则结束模拟定时炸弹的线程,程序结束。
首先来看一下使用继承Thread类的方式实现多线程时的代码示例,代码如下:
package example1;
import java.io.*;
/**
* 模拟定时炸弹线程
*/
public class TestTimeBomb1 {
public static void main(String[] args) {
//创建线程和启动线程
TimeBombThread tbt = new TimeBombThread();
//接受控制台输入
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in));
String line;
try{
while(true){
System.out.println("输入quit结束线程:");
//获得控制台输入
line = br.readLine();
//判断是否是quit
if(line.equals("quit")){
tbt.stopThread(); //结束线程
break; //结束循环
}
}
}catch(Exception e){}
}
}
package example1;
/**
* 使用继承Thread类的方式模拟定时炸弹逻辑
*/
public class TimeBombThread extends Thread {
int n;
boolean isRun;
public TimeBombThread(){
n = 60;
isRun = true;
start();//启动线程
}
public void run(){
try{
while(isRun){
Thread.sleep(1000); //延迟1秒
System.out.println("剩余时间:" + n);
if(n <= 0){
isRun = false; //结束线程
System.out.println("炸弹爆炸!");
break;
}
n--; //时间减少1
}
}catch(Exception e){}
}
public void stopThread(){
isRun = false;
}
}
在该示例代码中,TestTimeBomb1类中包含的是系统线程,在系统线程中启动模拟定时炸弹的TimeBombThread线程,然后在TestTimeBomb1中接收用户的控制台输入,如果输入的内容是quit则结束线程,程序结束,否则忽略用户的输入,继续等待用户输入。按照前面介绍的IO知识,在接收控制台输入时readLine是阻塞方法,也就是该方法在未获得用户输入时会阻塞系统线程的执行,使系统线程进入到等待状态,等待用户输入。而TimeBombThread实现的逻辑是每隔1秒钟减少一次数值,并输出剩余时间,当剩余时间为零时,结束TimeBombThread线程。这样两个线程就同时工作了,系统线程等待用户输入的同时,模拟定时炸弹的线程继续执行,这样程序中就包含了两个同时执行的流程。
在这里需要特别说明的是,如何控制线程的结束?在本程序中,使用的是让线程自然死亡的方式,在实际控制线程时,当线程的run方法执行结束则线程自然死亡,所以在本程序中通过控制isRun变量使得线程可以自然结束,从而释放线程占用的资源。
同样的功能也可以使用Timer和TimerTask组合的方式实现,实现的代码如下所示:
package example1;
import java.io.*;
/**
* 模拟定时炸弹线程
*/
public class TestTimeBomb2 {
public static void main(String[] args) {
//创建线程和启动线程
TimeBombTimerTask tbtt = new TimeBombTimerTask();
//接受控制台输入
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in));
String line;
try{
while(true){
System.out.println("输入quit结束线程:");
//获得控制台输入
line = br.readLine();
//判断是否是quit
if(line.equals("quit")){
tbtt.stopThread(); //结束线程
break; //结束循环
}
}
}catch(Exception e){}
}
}
package example1;
import java.util.*;
/**
* 使用Timer和TimerTask组合模拟定时炸弹
*/
public class TimeBombTimerTask extends TimerTask {
int n;
Timer t;
boolean isRun;
public TimeBombTimerTask(){
n = 60;
isRun = true;
t = new Timer();
t.schedule(this, 0); //启动线程
}
public void run() {
try{
while(isRun){
Thread.sleep(1000); //延迟1秒
System.out.println("剩余时间:" + n);
if(n <= 0){
stopThread(); //结束线程
System.out.println("炸弹爆炸!");
break; //结束循环
}
n--; //时间减少1
}
}catch(Exception e){}
}
public void stopThread(){
isRun = false;
t.cancel();
}
}
在该示例代码中,实现的原理和前面的类似,TestTimeBomb2类实现系统线程,功能是启动模拟定时炸弹的线程,并接收用户的控制台输入。而TimeBombTimerTask类实现模拟定时炸弹的线程,在该类内部包含启动线程的Timer对象,当构造该类的对象时,不仅完成该类的初始化,而且启动线程。
在控制Timer启动的线程结束时,首先结束当前的TimerTask线程,然后再调用Timer对象的cancel方法结束Timer对象的线程,这样才可以真正停止这种方式启动的线程。
至于使用实现Runnable方式实现线程的方式,和继承Thread类的实现几乎一致,读者可以根据第一种方式的实现独自进行实现,这里就不再重复实现了。
2.3.2 模拟网络数据发送
在实际的网络程序开发中,由于网络通讯一般都需要消耗时间,所以网络通讯的内容一般都启动专门的线程进行处理。
这样,在一个最简单的网络程序程序中,至少就包含了两个线程:处理界面绘制和接收用户输入的系统线程,以及至少一个网络通讯线程。
下面以一个简单的模拟程序,实现模拟网络数据的发送功能,关于更详细的网络编程中线程的使用,可以参看后续的网络编程章节。
在该示例代码中,用户在控制台输入需要发送的内容,程序接收到用户的输入以后,启动一个单独的线程进行网络通讯,然后用户可以继续在控制台进行输入。示例代码如下所示:
package example2;
import java.io.*;
/**
* 模拟网络数据发送的测试类
*/
public class TestNet {
public static void main(String[] args) {
BufferedReader br = null;
String input;
try{
//初始化输入流
br = new BufferedReader(
new InputStreamReader(System.in));
//循环接收输入
while(true){
System.out.println("请输入内容(quit代表退出程序):");
//读取控制台输入
input = br.readLine();
//判断是否是结束
if(input.equals("quit")){
break; //结束程序
}
//模拟发送
NetDemoThread ndt = new NetDemoThread(input);
}
}catch(Exception e){
}finally{
try {
br.close();
} catch (Exception e) {}
}
}
}
package example2;
/**
* 通过继承Thread类的方式模拟网络通讯线程
*/
public class NetDemoThread extends Thread {
String data;
public NetDemoThread(String data){
this.data = data;
start();
}
public void run(){
try{
System.out.println("开始发送");
Thread.sleep(10000); //模拟网络发送的延迟
System.out.println("发送完成,发送的内容是:" + data);
}catch(Exception e){}
}
}
在该示例中,TestNet类实现接收控制台输入,并在接收到用户输入以后,启动网络通讯线程发送数据,当用户在控制台输入quit时,结束程序。NetDemoThread类实现模拟网络通讯线程,在需要发送网络数据时,创建一个NetDemoThread类型的线程对象,并将需要发送的内容作为参数传入到该对象的内容,在run方法中,输出线程的状态,并使用一个延迟10秒,比实际的延迟要夸大很多,的代码模拟发送时的线程延迟。由于这里的延迟比较大,所以如果用户输入的数据速度比较快的话,会存在多个网络通讯的线程同时运行。
下面是程序的运行结果:
请输入内容(quit代表退出程序):
abc
请输入内容(quit代表退出程序):
开始发送
123
请输入内容(quit代表退出程序):
开始发送
tbc
请输入内容(quit代表退出程序):
开始发送
faga
请输入内容(quit代表退出程序):
开始发送
发送完成,发送的内容是:abc
hfsd
请输入内容(quit代表退出程序):
开始发送
发送完成,发送的内容是:123
发送完成,发送的内容是:tbc
发送完成,发送的内容是:faga
发送完成,发送的内容是:hfsd
quit
在该次运行中,用户依次输入了:123、tbc、faga和hfsd,当用户输入完成以后,模拟网络通讯的线程就被启动,这个可以从输出“开始发送”语句看出,当内容发送完成以后线程自然结束。最后输入quit指令结束程序。
当然,该程序会在用户输入的内容不同时出现很多不同的结果,这些结果能够使你体会到两点:
1、 多个网络通讯的线程在同时工作,互不干扰。
2、 当输入quit以后,如果还有网络通讯的线程没有结束,则程序会等待到网络通讯的线程结束以后才真正结束。
当然,这两个简单的例子只能够使你熟悉基本的多线程编程的使用,还没有进入到多线程编程的核心。
其实,当多线程一起运行时,除了带来一系列的优势以外,还会带来一系列的问题。例如现实社会中,一个儿子继承遗产时就很简单,但是当有多个儿子呢?所以,下面来深入线程的概念,理解多线程编程存在的问题以及解决办法。
以上是 JAVA的多线程【转】 的全部内容, 来源链接: utcz.com/z/392906.html