Java编程——多线程基础

第十二章多线程

        当计算机处于DOS时代时,程序几乎是没有界面的,而且由于计算机运行速度等原因,那个时代的计算机只能启动一个程序,只有当该程序退出以后才可以执行其它的程序。但是随着计算机性能的提高,以及软件的丰富,如果计算机还只能同时执行一个程序的话,那么计算机恐怕是很多人都不能接受的。

        这种在任何一个时间点,可以有多个程序同时执行,或者有多个程序逻辑同时执行的能力,成为并发执行。

        现在计算机早已进入到并发执行的时代,对于程序编程来说,进行并发执行的程序编写也就被称作并发编程,在Java语言中,同一个程序内部的并发处理由线程这个概念来实现。

12.1多线程简介

        从小时候开始,老师就教育大家——“一心不可二用”,这是指做一件事情的时候一定要专注,不能够分心。但是在程序编程的领域却早已经需要做到“一心二用”甚至“一心多用”了。下面来看一下线程的概念吧!

12.1.1进程和线程

        在介绍线程的概念以前,首先介绍一下进程的概念。

        进程(Process)指操作系统中一个独立运行的程序。例如在计算机中,同时运行着QQWordMSN等,那么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方法使当前正在执行的线程从运行状态切换到阻塞状态。

        而具体线程编程的实现方式、线程的控制以及线程编程时需要注意的问题,则将在下面进行详细的介绍。

12.2 多线程实现方式

         线程的概念虽然比较复杂,但是在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语法中类的单重继承的限制,但是在实际的项目中还是获得了比较广泛的使用,是一种最基本的实现线程的方式。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值