JAVA编程实践1

上传人:无*** 文档编号:127307003 上传时间:2022-07-29 格式:DOC 页数:23 大小:371.50KB
收藏 版权申诉 举报 下载
JAVA编程实践1_第1页
第1页 / 共23页
JAVA编程实践1_第2页
第2页 / 共23页
JAVA编程实践1_第3页
第3页 / 共23页
资源描述:

《JAVA编程实践1》由会员分享,可在线阅读,更多相关《JAVA编程实践1(23页珍藏版)》请在装配图网上搜索。

1、JAVA编程实践1 引言本培训的主要目的是帮助被培训者了解如何使用JAVA语言构造出高效的、不易出错的、可维护的程序。同传统的程序设计语言相比,JAVA语言通过语法上的精心设计,例如通过引用替换指针,已经避免了很多使用C+或者其他语言容易导致错误的地方。但是,程序设计语言本身的语法本身并不能够保证程序正确无误,我们发现,即使程序开发人员已经熟悉了JAVA语言的语法和函数库,要写出健壮的、高效的程序,也需要经过长时间的经验积累。使用同样的JAVA,要完成一个程序,可能会有10种编码方法,但是可能有7种都是低效的、笨拙的、可读性差、难以维护的编码方法。这是因为程序开发人员尽管已经掌握了语法规则和函

2、数库,但是没有掌握正确的、高效的方式来编写代码。更为重要的是,开发人员有时候根本不知道什么是好的程序,什么是坏的程序,错误的使用一些技巧,使得程序复杂而且难以维护。我们通过在统一网管平台开发的实践,收集了一些最容易出错的编码问题,从中总结出一些编码的方式,这就是本次培训的目的。通过本次培训,我们希望能够帮助被培训者了解一些编码中最可能出现错误的地方,写出简单的、高效的程序,使得程序更不容易出错,使得其他人能够更好的理解这些程序,也使得在发生错误的时候,你能够更快的找到错误的原因,在需要进行功能增强的时候,更容易的修改他们的程序。2 异常处理2.1 为什么要使用异常对于一个大型的软件系统,特别是

3、那些需要连续运行几个月的服务器软件,错误处理通常会消耗程序员极大的精力。一个中等水平的程序员,一般说来都应该能够正确完成基本的事件处理流程,而对于错误处理,就很难考虑周全。我们经常看到,程序员能够很快的完成一个所谓演示系统,或者原型系统,或者他报告说已经完成了全部功能。但是,真正的要得到一个健壮的、稳定的商用软件,还需要花费程序员很长的时间和精力。对于错误处理的估计不足,也是导致软件项目计划延期的重要原因。因此,有必要对错误处理加以特别的重视。在C、C+或者其他早期的语言中,通常采用返回值或者设置标志位的方式来处理错误。典型情况下,错误的发现函数设置一个错误码返回值/标志位,调用者检查这些返回

4、值/标志位,判断发生的具体情况,并进行不同的处理流程。许多标准的C库函数也是采用这种方式来处理错误。这种方式使用了很多年,实际应用中发现了很多问题,其中最大的问题是,对于返回值的检查,不是依赖于语法来保证的,而是依赖于程序员的个人素质来保证的。程序员可以不检查这些错误的返回值/标志位,而在编译和运行期间,没有任何语法的措施来发现这一点。通常,正确的处理流程只有一个,而发生错误的机会却非常之多,程序员也很难对全部的错误情况考虑周全。一种情况下,错误非常的隐蔽,程序员可能考虑不到。另一种情况下,错误非常的低级,程序员会产生麻痹情绪,这种错误如此愚蠢,怎么可能发生呢?所以就没有进行条件检查。即使程序

5、员非常有经验,水平很高。但是,当函数调用层次很深的时候,如果在最下层的函数中发生了一个错误,上层的每一级调用者必须层层检查这些返回值。这样导致代码非常的庞大,也难以阅读,降低了软件的可维护性。有时候,一个服务器程序中,错误处理的代码要占到50%以上。我们发现,采用这种方式开发一个大型的、稳定的、又易于维护的系统,是一件非常困难的事情。因此,JAVA中提供了异常处理机制,专门用于错误的处理。这种机制,将错误处理的方式作为程序设计语言的一部分,强制程序员进行错误处理。当发生异常的时候,程序员必须停下来,捕获该异常并进行处理,否则编译器就会报错:未捕获的异常。这样,就通过编译器保证了程序员必须处理错

6、误。另一方面,异常处理机制使得错误处理的代码大大简化。编译器保证了一定会有一个地方来处理异常,这样,程序员不必层层检查返回值/标志位,而是只需要在“应该处理”的地方来处理错误。处理错误的代码和正常处理逻辑的代码很好的分离开,也有助于代码更加有条理易于维护。规则:使用异常处理机制来处理错误,而不是使用返回值或者标志位。2.2 基本语法异常是一个较新的语法,很多程序员,特别是原来的C/C+程序员,没有完全掌握异常的语法,因此有必要在这里复习一下语法。2.2.1 throwthrow关键字用于“抛出”一个异常。使用该语句通常存在于两种情况:一种是抛出一个新的异常,一种是抛出一个已经存在的异常。如:2

7、.2.2 throws对于一个提供其他方法调用的方法,需要告诉调用者该方法会抛出什么异常,这样,调用者才能够有针对性地进行错误控制。因此,JAVA引入了一个关键字:throws。该关键字用于方法声明,在该关键字后跟随所有的可能抛出的异常类型。假如一个方法中调用了另一个可能抛出异常的方法,那么一般情况下,该方法要么捕获这些异常,并加以处理;要么也需要在自己的声明中throws这个异常。否则,编译器会报告一个错误。前面提到,异常处理机制和返回值/标志位处理方式的不同在于异常处理机制从语法上强制了程序员必须进行错误处理。这实际上表现为两个方面:首先,一个方法会发生什么错误,是在该方法的声明中通过th

8、rows语句告诉程序员的,而不是通过在注释中告诉程序员的。如果一个方法抛出了一个异常,又不在方法声明中写明,那么编译就无法通过。而编译器是无法控制是否在注释中写清楚返回值的含义的。其次,对于一个调用一个方法的程序员,他必须处理该异常,要么捕获,要么在继续向外抛出,否则编译也无法通过。2.2.3 try、catch & finallytry、catch、finally关键字用于捕获并处理异常。这里需要特别提到的是finally关键字。从语法上,在finally关键字作用范围之内的语句是正常和异常的处理流程中都需要执行的。一般情况下,这是指资源的释放。当申请一个资源之后,不论是否处理正确,处理完成

9、之后,都必须释放该资源。将这些语句集中到finally语句块之中,有助于增加程序的可读性,并减少不释放资源的危险。在关于资源的培训中,将会有关于这一点的详细说明。2.3 异常分类2.3.1 java.lang.Throwable所有能够“throw”出来的对象的虚基类。实际应用中,应用不能够直接使用Throwable,也不能够直接从它继承,而应该继承它的两个子类。2.3.2 java.lang.ErrorError表明严重的错误,通常当发生了这种错误的时候,程序已经无法在运行下去,只能够中断运行退出。应用程序不应该捕获并处理Error,这些工作应该由虚拟机完成。除了非常底层的程序之外,一般应用

10、程序不需要继承Error,或者抛出Error。在公司的JAVA编程规范中,禁止直接从Error继承(可以从Error的子类继承)。2.3.3 java.lang.ExceptionException表明普通的错误,应用程序可以在程序中捕获这些异常并进行处理,保证程序能够继续运行。应用程序自定义的异常,都是Exception的子类。我们在通常的情况下,处理的也都是这种类型的异常。2.3.4 java.lang.RuntimeExceptionRuntimeException是Exception的一种特殊子类,其特殊性在于:编译器不强制进行RuntimeException的处理。回顾2.2.2节,

11、如果TestException是一个RuntimeException的子类,那么2.2节中的错误例子也能够通过编译器的检查。前面提到,异常处理机制的好处在于强制程序员进行异常处理。而如果使用了RuntimeException,则程序员可以完全不处理这些异常。这里JAVA为了编程的方便而提供了一些灵活性。那么RuntimeException一般应用于什么情况下呢?根据我的理解,RuntimeException用于表示这样一种异常:该异常只在调试期间产生,而在程序交付使用之后,根本不会出现的异常(好像名字正好取反了?)。导致该错误发生的原因是程序员的编程错误,而不是其他诸如通讯中断、磁盘错误、用户

12、输入错误等。通常,一些公共函数库会抛出这类异常。看一个例子,假设有一个公共函数对于一个数组进行排序,参数为一个数组引用,如果传入的引用为一个空引用,那么该方法会抛出一个NullPointerException。这种错误会在什么时候产生呢?实际只有一种情况:调用该函数的程序写错了。也就是说,当调用程序调试正确之后,该异常永远不会被抛出。上述的程序能够通过编译器的检查,但是会发生了一个NullPoinrtException,应用程序不进行处理,则这个异常会由JVM进行处理,从而中断正常的处理流程。这正好给程序员一个强烈的提示,此种情况表示了一个编程错误。当程序员的程序调试正确之后,这种情况就永远不

13、会发生。注意,不需要自己检查NullPointerException。如以下的代码:2.4 finally的特别说明2.4.1 什么都逃不过finally的掌心我们看以下的代码:在代码中的3种情况,无论是自己抛出一个异常,还是直接返回,或者是产生一个NullPointerException,都逃不出finally语句。在代码中发现一种情况,少数代码认为只要写finally,就必须写catch,结果导致了很多不必要的catch语句块。2.4.2 finally语句中不要抛出异常如果在finally语句中抛出异常,会掩盖真正需要抛出的异常,编程的时候特别需要注意这一点。假如在catch语句块和fi

14、nally语句块中都抛出了异常,那么程序将不会抛出catch语句块中的异常,而是会抛出finally语句块中的异常。我们来看下面的例子:在以上的代码中,如果读写文件正常结束,执行到关闭文件的时候出了错,那么就会抛出异常,但是这种情况下,功能应该都完成了的,这会给客户端错误的信息。如果读写文件中发生异常,那么程序的原意是关闭文件,将读写文件中发生的异常抛出。但是如果在关闭文件中,也发生了异常,那么最终抛出的是关闭文件的异常,而不是读写文件的异常。这就会给客户端程序错误的信息。而且调用者的到这个关闭异常之后,也无法判断文件的读写是否已经正常完成。2.5 Exception的特别说明2.5.1 是否

15、需要捕获Exception在代码中常常发现,有些人为了确保程序正确,往往写一个catch(Exception ex),用这种方法来确保程序能够继续运行。其实这是不对的,因为这样实际上意味着写程序的人并没有认真地考虑可能发生的异常情况,写一个大而宽泛的catch(Exception ex),看起来很保险,实际上可能会掩盖很多编程上的问题。因为我们在前面已经提过,实际上在Exception的所有子类中,除了RuntimeException之外,其他所有可能抛出的异常,如果你没有页:7catch,编译器都会发现并报错。而对于RuntimeException,我们认为是编程的错误,就是要让它暴露出来。

16、当然,这也有一些例外情况,一个是在finally语句块中,因为不允许抛出异常,所以我们允许直接catch(Exception ex)。另一种情况是,为了确保程序在真正运行的时候不出问题,在线程的run方法中要捕获所有的异常进行处理,防止这个线程退出运行。因为实际上所有的代码都在线程中运行,只要控制住了这一点,就不会发生这些异常影响程序运行的现象发生。问题:出现异常的情况下,我需要释放资源,如果不捕获RuntimeException,不会出现资源不被释放的情况发生吗?2.5.2 不要在函数声明中throws Exception前面提到,不要捕获Exception,那么如果在函数的声明中写thro

17、ws Exception,就强迫了使用者要catch Exception。所以,在函数声明中,一定要写清楚throws的具体的异常类。2.6 如何定义异常2.6.1 异常的层次和异常链如果高层方法调用了低层方法,在低层方法中抛出了一个异常,如果高层方法中不加处理的抛出该异常,那么对于该方法的使用者来说,可能会感到无法理解。因为高层方法的使用者为了理解这个异常,就必须了解高层方法的实现细节。假如高层方法的实现发生改变,就有可能导致抛出的异常发生变化,从而导致需要修改高层方法的客户端程序,因为需要捕获的异常发生变化了。为了避免这种情况发生,高层方法应该自己捕获低层方法的异常,将其转换成为按照高层解

18、释的新的异常。这种方法称为异常转换。也就是说,抛出的异常语意应该和方法的语意层次相一致。例如,一个处理某项业务逻辑的方法,目前该方法实现中使用文件作数据存储,但是未来有可能改变为使用数据库做数据存储。那么该方法抛出的异常,其语意应该表明数据存储失败,而不是文件操作失败。否则,现在客户端程序捕获处理文件操作失败异常,当方法实现改为数据库时,客户端程序必须修改为捕获数据库操作失败异常。进行了异常转换之后,为了能够在调试程序的时候,能够追根溯源到异常的原始发生地,那么需要在高层异常中保留低层异常。高层异常中保留了低层异常,低层异常中又保留了更低层的异常,这种情况称为异常链。JAVA的Exceptio

19、n类提供了对于异常链的支持:Constructor SummaryException() Constructs a new exception with null as its detail message.Exception(Stringmessage) Constructs a new exception with the specified detail message.Exception(Stringmessage, Throwablecause) Constructs a new exception with the specified detail message and caus

20、e.Exception(Throwablecause) Constructs a new exception with the specified cause and a detail message of (cause=null ? null : cause.toString() (which typically contains the class and detail message of cause).使用了以上3、4的构造方法的时候,当打印异常堆栈的时候,会把cause也一起打印出来,如:java.lang.Exception: aaaat zq.sample.TestExcepti

21、onChain.openFile2(TestExceptionChain.java:40)at zq.sample.TestExceptionChain.main(TestExceptionChain.java:47)Caused by: java.io.FileNotFoundException: aaa (系统找不到指定的文件。)at java.io.FileInputStream.open(Native Method)at java.io.FileInputStream.(FileInputStream.java:103)at java.io.FileInputStream.(FileI

22、nputStream.java:66)at zq.sample.TestExceptionChain.openFile1(TestExceptionChain.java:22)at zq.sample.TestExceptionChain.openFile2(TestExceptionChain.java:37)2.6.2 我们系统的情况对于统一网管平台,我们将程序分为两类:公共函数,和应用程序。这两类程序的层次不同,抛出的异常类型所属的层次不一样。在平台中,一般只需要两层异常层次就可以了,低层异常由公共函数抛出,我们称为原始异常;高层异常由处理业务逻辑的应用程序抛出,称为应用异常。应用程序捕

23、获原始异常,将其转换成为应用异常。公共函数提供一系列函数库,供其它程序使用。对于这一类函数,抛出的原始异常包括两类:RuntimeException。这些异常主要是由于调用者的程序书写不当造成的。由于JDK已经定义了绝大多数由编程错误导致的异常,所以写函数库的程序员一般情况下不需要自定义异常类型,只需要直接使用JAVA已经定义的异常类型即可。普通的异常。例如:处理IO的函数,由于磁盘硬件错误导致的异常。程序应当继承Exception,或者继承Exception的子类,定义一些特殊的异常类来表示。如果JDK中存在可用的异常类型,也可以直接使用。对于应用程序,推荐首先整理所有可能发生的错误类型,使

24、用一种通用的异常类型来表示所有的错误。所有可能发生的错误类型,都通过这个异常的属性来表示。例如:通过一个错误代码和一个表示详细描述的字符串,来表示不同的错误。例如:2.7 抛出和捕获异常的时机2.7.1 理论对于函数库,在发现错误的时刻抛出异常。对于应用程序,相对复杂一些。如果应用程序能够直接检测到错误,那么直接抛出异常即可。捕获异常的时机比较复杂。有两种做法,一种使用较小的try语句块,精确定位所捕获的异常,另一种使用很长的try语句块,在最后捕获所有的异常。这两种做法各有优缺点,前者代码比较长,但是有助于精确定位,使程序员保持对于错误的敏感。后者代码比较简洁,属于偷懒的做法。我习惯的做法是

25、:对于原始异常,要立刻检测,检测到之后,将其转换成应用异常抛出。对于应用异常,可以忽略,直到需要捕获的时候再处理。这里所谓需要捕获的时候,通常会有这么几种类型:输出到用户界面的时候。忽略该次错误,继续进行以下的处理时。其他需要采取错误处理措施的时候,如断链重连等。其他情况下,应用程序几乎不需要做任何事情,只需要在方法声明的throws关键字后增加AppException即可。有些程序员喜欢在每一个方法中都使用一个大的try语句,并在最后捕获一个通用的Exception。特别是在程序不稳定的情况下,以为这样可以增加程序的稳定性。这样,不仅丧失了异常处理机制使程序简洁的优点,也容易隐藏真正的异常发

26、源地和产生原因。异常处理机制的一个优点就是,使得程序员可以一直考虑正常的处理流程,在发生错误的时候,只需要抛出异常即可。总之,可以把握一个原则,如果程序员捕获了一个异常,那么他一定需要对这个异常做一些处理,例如:一些异常处理措施(如一个告警池满之后,清除老的告警)、转换成应用异常抛出、在界面上输出错误提示等等。唯一的例外是,该错误可以忽略,继续进行以下的处理。这种情况下,会将其打印出来,用以提示发生了错误。那种捕获了一个异常,仅仅是将其继续向外抛出,这种做法是没有任何用处的。规则:不允许捕获异常之后,不做任何处理,仅仅将其继续外抛。如果仅将其打印,需要在注释中写明。2.7.2 我们系统的情况对

27、于统一网管平台,应该在处理业务逻辑的Bean的方法中,只允许向外抛出应用异常。在这里,必须捕获所有的原始异常,将其转换为应用异常。2.7.3 什么时候输出调试信息同捕获异常一样,有的程序员喜欢将异常输出的到处都是,经常一个错误的发生,会在调试打印的输出中看到好几个异常的堆栈信息。这样,反而使得调试者无法正确定位异常的真正发生原因。推荐的输出原则是:对于函数库,不输出异常的调试打印信息,只需要把异常往外抛就是了。对于应用程序,谁处理该异常谁输出。注意:以上是指在正常运行之后的输出原则,如果出于程序调试阶段,则可以不受此限制。3 线程和共享控制3.1 概述为了提高程序的处理效率,我们需要进行多线程

28、的编程。但是,多线程是一把双刃剑,一方面可以提供编程的极大灵活性,另一方面又非常容易导致错误,特别是在多线程的数据共享互斥和线程的执行顺序控制上,我们就曾经发现过JDK的一个Bug导致程序死锁。为了帮助程序员编写多线程的程序,JAVA函数库提供了多种函数和工具类,在我们看来,有一些函数是非常有害的,一不小心就会导致错误。我们这里介绍一下可以用于线程共享控制的函数,把它们分为几类:底层控制函数:包括Thread类和ThreadGroup类。低级函数:如Mutex、semephone、Condition等。高级函数:如读写锁、channel、Excecutor、Barriar等。3.2 底层控制函

29、数关于线程控制方面最古老的函数是线程类Thread的一些方法:destroy、interrupt、jion、yield、suspend、resume以及线程的优先级和各类属性的设置等方法。使用这些函数会导致其大的危险,一般的开发人员要花很长的时间才能够掌握这些函数。而且,即使你掌握了这些函数的正确用法,在编码的时候也要仔细计算程序的逻辑,不同线程的执行顺序等等,非常容易出错。使用这些函数编程,对于脑细胞也是极大的破坏,一小段程序就要反复斟酌。类似的还有ThreadGroup类,该类也是极不可靠的,最终执行的结果可能和你设想的有十万八千里的差异。我们在程序中禁止使用这两个类,因为所有需要使用他们

30、完成的控制,都可以通过后面介绍的高级函数来完成。3.3 低级函数低级函数包括用于数据共享保护的方法和用于线程控制的方法。在JAVA里面,提供了synchronize关键字用于共享数据保护,此外还有一些第3方的函数库提供如Mutex、semephorne、CriticalSection等类。下面分别介绍一下。3.3.1 MutexSynchronized提供了基本的共享数据保护方法。可以将Synchronized理解为一个公共类,提供两个方法,lock和unlock(或者叫做accqure和release等)。但是为什么JAVA要把这作为一个关键字而不是提供一个公共类呢?我们来看两段代码的样例:

31、 如上,假如使用工具类,必须注意调用unlock,而且注意最好在finally语句中调用,以避免异常情况下无法释放。而通过Synchronized关键字,就可以从语法上避免可能出现的不释放的问题。Mutex提供了更精细一些的互斥控制,主要是当一个线程对共享数据区的访问结束以后,操作系统究竟让哪个等待的线程来访问。一般有: 随机,即有操作系统随机的选择一个等待的线程来访问。这就等于synchronized关键字。 按顺序调度。 按线程的优先级调度。此外,Mutex还提供了当线程无法访问共享数据时候的行为控制,可以是: 无限制的等待,等于synchronized关键字 立即返回 等待一段时间当需要

32、一些精细的控制,synchronized无法满足要求的时候,可以使用Mutex。如果使用Mutex,就需要注意在finally语句中调用unlock方法。3.3.2 SemephorneSemephorne内部保存了一个stoken池,在初始化的时候有一个stoken池中的stoken数目,假如设置stoken数目为3,则可以有3个线程同时访问共享数据区,第4个就被挂起。每个线程访问共享数据库区的时候,从Semephorne的stoken池中取出一个stoken,取得就可以访问,访问完成以后将stoken放回Semephorne的stoken池中。如果初始化的设置stoken数目为1,就蜕化为

33、Mutex。Semephorne的主要用处在于对访问数量作限制。3.3.3 CriticalSection和ConditionCriticalSection,也叫做临界区,概念和Mutex类似。Condition,条件变量,作为yield、suspend、resume等方法的升级版本,用于线程调度和控制。他的基本原语有:wait、notify和notifyAll,已经作为JAVA Object的最基本方法。虽然看起来这几个原语非常简单,但是真正用起来,非常的困难,也是问题百出。所以我们也不提倡在程序中使用Condition。3.4 高级函数高级函数包括读写锁、消息队列、Excecutor、Ba

34、rriar。3.4.1 读写锁读写锁用于共享数据控制。它将对内存数据的访问分为读写两种,并遵循以下规则: 读可以同时进行。 读和写不能够同时进行。 写不能够同时进行。通过读写锁,可以提高共享数据区的效率,提高同时存在大量的读的情况。也存在多种类型的读写锁,以做一些更加精细的控制,如写优先锁等。3.4.2 消息队列和Exceutor消息队列提供了产生请求和处理请求的解偶,用于处理产生和处理效率不匹配的情况。请求的生产者产生请求后,将其写到消息队列中,而消费者则侦听这个消息队列,从中取出请求来处理。绝大多数需要使用Condition的情况,实际上都可以用消息队列来处理,不仅简单的多,而且不容易出错

35、。对于请求的处理,通常可以采取几种策略: 单线程策略:使用一个线程处理所有的请求,适用于请求的处理有顺序要求的情况。 每请求一个线程:对于每一个请求,都使用一个线程来处理,当该请求处理完毕之后,线程就消亡。这种策略不利于控制整个系统的线程数量,我们不提倡采取该策略。对于效率要求高,需要并行处理的情况,推荐使用下面的线程池策略。 线程池:使用一个线程池来处理请求。当收到一个请求后,从线程池中取出一个空闲的线程来处理,处理完毕之后,将该线程回收到线程池。如果没有空闲的线程,则请求挂起/拒绝。Exceutor封装了这几种情况的线程管理,使得我们可以简单的设置策略,就可以管理线程。Barrire使用情

36、况比较少,可以忽略,我也不懂。3.4.3 实现在统一网管平台中,集成了一个concurrent.jar,可以提供以上所有的函数。在实际应用中,我们最可能用到的是Synchronized、MessageQueue、Executor,对于这几个类的用法,都非常简单,可以后面自己学习。4 资源4.1 概述所谓资源,包括线程、内存、数据库连接、socket连接、文件句柄等。所有这些东西有一个共同特点,它们的数量是有限制的,不能够无限制的获取和使用。这通常有两种情况,某些资源,例如同时打开的文件数目、socket连接等,操作系统允许的数量很少,一个应用程序如果占用这些资源,其他应用程序就不能够正常工作,

37、这将较快的导致系统的不正常。另外一种情况,如内存或者JAVA中的线程数量,系统允许的数量较多,应用程序占用这些资源的后果不会立即显现出来,刚开始仅仅造成系统性能的下降,它的严重影响需要一个缓慢的过程(如持续运行几天或者几个月)才能够显现。对于资源的使用,要注意两件事情:1)不要泄漏,使用完了以后要关闭。这是最基本的要求。2)资源使用要有总量控制,每个模块要对自己使用的资源有估计,整个系统要进行总量控制。对于我们的系统,主要考虑的是以下几类资源: 数据库连接 消息服务器连接 对象远程引用有经验的程序员都知道,如果由于编程不正确导致资源处理不正确,那么将是调试程序的一个噩梦。同功能性错误相比,这些

38、Bug引起的故障,常常无法重现,或者需要很长时间才能够重现。而且,在查找这些Bug的过程中,调试工具通常也起不了太大的作用。当一个系统的功能基本调通以后,资源的问题就成为一个主要的调试内容。因此,有必要从编程开始,就对这些方面的内容引起高度重视。4.2 资源的使用方式4.2.1 永久性使用永久性使用是指程序在初始化的时候申请资源,以后就一直使用,永远不释放,直到程序运行结束,由操作系统或者JVM强行释放。由于每一种资源在系统中的数量是有限制的,所以这种用法非常危险。一个模块永久占用一个资源,意味着其它模块可使用的资源就少了一个。如果有很多模块都这样做,那么各模块单独运行的时候,可能不会发生问题

39、。但是当所有这些模块集成在一起运行的时候,就有可能出现资源不足的情况。永久性用法的另一个危险在于,多线程情况下,如果一项功能可能同时运行,程序员有时候会忘记考虑多线程的因素,而使用同一个资源,从而导致共享冲突的情况发生(如数据库连接)。对于某些资源,并不是申请之后就一直能够正确使用的,有时候程序不得不去写代码维护这个资源。例如:数据库连接在长时间不用之后,系统会自动将其释放。当通讯中断之后,消息服务器的连接需要重新建立。为了维护这些资源,经常需要了解资源的内部情况,这是普通的程序员很难考虑周全的。基于以上3个原因,我们一般情况下,不建议使用永久性方式。如果由于性能或者其他原因必须使用,则应该考

40、虑以上3种情况,并在资源声明的注释中加以详细说明程序是如何处理这3种情况。我们再次回顾一下需要注意的3个方面:1)永久性使用要考虑资源的永久占用是否会影响整个系统。2)考虑共享冲突。3)最重要的,要自己维护资源。某些程序员认为,永久性使用,或者类似的搞一个缓存,可以提高程序的运行效率。这也是某些人坚持使用永久性方式的一个“有力”的理由。有一句名言:“过早的优化是一切麻烦的根源”。正确的方式是:只优化那些需要优化的代码。在进行程序设计的时候,除非已经知道此处将会成为性能瓶颈,否则更多地考虑程序的简单、可维护性,而不是进行复杂的优化而将其变得难以阅读维护,使得潜在的错误更多。即使需要进行资源缓存以

41、提高效率,也应该遵循以下的原则:1) 整个系统应针对某项资源进行统一缓存,而不是每一个模块都自己做缓存处理。2) 该缓存的维护代码由这方面的专家来编写,而不是具体应用程序的编写者自己来做。3) 进行缓存和不进行缓存的情况下,应用编程者面对的接口不变。4.2.2 一次性使用一次性使用,是指每进行一次处理,程序就申请一个资源,处理完成之后立即释放。同永久性使用方式相比,这种方式处理要简单得多,首先,不必考虑多线程共享冲突的问题。因为通常情况下,每次处理只可能在一个线程中进行。其次,应用程序不必考虑资源的维护。资源发生故障,需要进行维护的概率是比较小的,一般说来,一次请求处理非常短暂,在申请成功之后

42、立即发生故障的概率极小,几乎可以不考虑。其次,即使发生了故障,这些故障排除所需要的时间也比一次请求处理所需要的时间长的多,在请求期间基本上只能够返回一个错误。这和不进行维护的效果是一样的。所以,对于一次性使用方式,只需要注意一个方面:资源的释放。尽管这听起来简单,但是却发生了很多错误。对于资源需要释放这一点,无论怎样强调都不为过。4.2.3 资源的使用接口封装提供给应用的资源使用接口应该进行封装,而不是采用示范代码的方式。封装的好处在于:1) 减少了应用出错的机会。2) 简化了以后可能发生的修改。对于资源的封装,最好能够达到这样几个原则:首先,对于一种资源,应用只需要和一个类打交道。为了设计的

43、灵活,或者采取各种各样的设计模式,其结果是应用开发者不得不面对相当多的类。为了完成一个简单的获取资源的功能,需要好几条语句。我个人认为,应当使用一个faade模式,提供给用户一个类,封装所有的操作。其次,资源的获取应当只调用一个函数open即可完成。如果以后发现该函数不能够满足要求,则增加其他的函数open即可。第三,资源的释放只调用一个函数free完成。举例:class DBHandle private Connection con = null;public DbHandle() public void open(String dsName) throws XxxException pub

44、lic void free() throws XxxException4.2.4 应用编程有了以上的基础,对于一次性使用方式,应用编程就非常简单了。DBHandle dbh = new DBHandle();try dbh.get(“”);catch() finally dbh.free();建议尽量采用以上的程序结构。如果无法采用,需要考虑两个原则,首先,对于一次性使用的方式,建议该次处理所需要的所有资源统一在处理开始的时候申请。中间调用其他类方法的时候,将资源作为参数传入。其次,资源的释放要统一在finally语句块中完成。对于统一网管平台,最好统一在处理业务逻辑的Bean方法开始申请资源

45、,在该方法的finally语句块中释放资源。其他公共函数在设计时,都不要自己去申请资源,而是应该将资源句柄作为方法参数,要求客户端程序传入。目前对于资源仍然使用J2EE标准接口的情况下,资源的申请和释放应该使用标准的代码。这部分代码将后续提供。4.3 资源的总量控制4.3.1 效率和资源的平衡为了提高处理效率,采用多线程是最好,也是最省事的办法。实际上,线程的使用是有限度的,一般情况下,线程越多,效率越高。但是线程也有副作用,一是系统对线程的管理有开销,线程之间切换也有代价。另一方面,每多启动一个线程,就会多一些资源的消耗,多出很多对象,多出很多的内存。当线程的数量到达一定程度以后,系统在管理

46、线程的开销,线程带来的各种资源的消耗,就会大于线程带来的好处,导致系统系统的急剧下降。可以用如下的曲线来描述:所以,线程不是越多越好。特别是从某些模块的角度来看,自己对资源的占用并不过分,启动10个线程也不多,但是我们是做一个平台,一个模块单独运行可能没有问题,集成到平台中来就可能出现问题。平台单独运行没有问题,加上应用之后就可能出现问题。所以,每一个模块都要控制自己对资源的使用,控制线程的数量。当然,具体数量是多少,我们也没有总结出合适的数据来。但是至少每一个模块要进行控制,不能够无限制的启动。4.3.2 平台的情况从我们的系统来讲,所有的代码都是4种力量驱动的: 用户的操作驱动。 定时器的

47、驱动。 网元或者其他系统上报的消息驱动。 自己启动的线程。我们根据这4种驱动,就可以简单的计算最大可能情况下的线程数量。超出规定的限制之后,要能够“拒绝”这些请求。因为,计算机的处理能力不是无限大,当请求超出了处理能力之后,就要将这些请求拒绝掉,直到目前的请求处理完成以后,能够恢复回来。否则就会出现越忙越乱,越乱越忙的现象,最后导致了系统的崩溃。以前平台的设计中没有考虑到,导致了很多的问题: JMS的设计。 PCS性能数据入库的问题。总之,从模块设计的角度来看,应当有一种稳定优于服务质量,“try my best”的思想,不要因为为了无限制的提高服务质量而导致系统的不稳定。如果因为这样使得整个

48、系统无法完成任务,就要考虑升级硬件配置。从这方面来讲,类似数据库的优化,平台以后也应当强调部署者的概念,应该根据机器的实际配置情况,网络管理的需求,在部署的时候调整各种参数,如:告警池的大小,允许并发访问的客户端数量等等。根据以上的论述,应当对资源的使用进行控制。目前大部分模块都没有进行控制,少数的模块作了控制,但是大部分也是在出现问题以后。对资源的控制方式,从一个大的系统来说,应当有一个统一的方式。4.3.3 网元上报消息的控制从网元上报的消息处理,一般是占用资源最多的一种模式。处理这类消息,一定要有控制。一个最基本的控制模式就是采用生产者消费者模式,将处理分解为2个步骤,中间使用消息队列来

49、传递消息。如下图:这实际上是一个采用J2EE实现的线城池的Exceutor模式。在这里,我们做两方面的控制: 通过对消费者数量的控制,可以避免资源的使用超出限制,如每个消费者占用一个数据库连接,那么就可以通过这个方式,控制总的数据库连接数,从而避免无限制的启动线程来处理,导致系统资源的枯竭而崩溃。 通过对JMS消息主题最大数量的限制,可以控制消息的积压,防止处理请求超出处理能力的时候,系统无法恢复。这种时候,应当丢弃消息。4.3.4 客户端调用发起操作的控制对于客户端来的调用来说,大部分情况下不需要进行控制,但是少部分情况下,如一个操作特别耗时,则需要做控制,限制同时访问的个数,以避免多个请求

50、同时访问,使得处理时间更长的情况。例如:在目前的平台中存在这样一种典型情况,某个耗时特别长的操作,如查询一个数百万条记录的数据表,可能需要几分钟。这时很容易造成超时,但是我们目前的超时是假的,仅仅是客户端不接受处理结果而已,真正服务器还在后台进行处理。这样,用户再次发起操作,多操作几次,就会导致服务器忙。4.3.5 定时器的控制定时器维护了一个线程池,每次从线程池中取得一个线程来执行某个特定的定时任务。这里需要注意的是:如果一个定时任务的执行时间超出了定时间隔,系统如何处理?例如:我们假定有一个定时任务10秒钟执行一次,但是这个任务很可能执行30秒,在1分的时候执行,这个任务还没有执行完,在1

51、分10秒的时候是否执行新的任务呢?从整个系统来看,如果放任这种情况的发生,是非常危险的,也很容易导致系统处理堆积。所以,这里一定要有所控制。可能的策略是,如果到了定时任务执行的时间,而上一次执行还没有完,那么就不要执行新的任务。这种情况下,要有一个通知的机制,使得应用知道发生了这种问题,好做一些善后的处理工作。最好的方法还是,对于处理时间长的操作,要通过生产者-消费者模式解藕,定时任务作为生产者,把任务的执行等耗时较长的操作放到消费者去做。而且,经过测试发现,JAVA的定时器是极不准确的,最大可能性会相差几十秒。例如,你调用一个sleep(10毫秒),由于调度的关系,可能会睡10秒。所以,对于

52、这种定时间隔差别在分钟之内的任务,无论任务的执行时间是多少,都可能被乱调度。所以,定时任务的定时间隔应该是分钟级。4.4 资源的监控需要PSL做工作。作为一个平台来说,应当提供足够的手段供应用进行监控,调试和诊断发现问题。5 内存5.1 JAVA的内存回收机制在Java中所有对象都是在堆(Heap)中分配的,对象的创建通常都是采用new或者是反射的方式,但对象释放却没有直接的手段,所以对象的回收都是由Java虚拟机通过垃圾回收线程(GC)去完成的。Java中对象回收的原则是这个对象不再被引用,准确的说是不再被系统运行线程中的各种对象引用,具体后面会详细介绍。垃圾回收线程怎么知道一个对象不再使用

53、需要回收呢?这就需要垃圾回收线程监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等为了更好理解GC的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从main进程开始执行,那么该图就是以main进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被GC回收。以下,我们举一个例子说明如何用有向图表示内存管理。对于程序的每一个时刻

54、,我们都有一个有向图表示JVM的内存分配情况。以下右图,就是左边程序运行到第6行的示意图。前面是一个最简单的例子,只是回收一个没有任何引用对象,但大部分情况下都比这个复杂得多,GC往往需要进行复杂的计算来确定一组对象做为整体没有外部引用,而全部可以释放。我们来看一个典型的Java程序的内存使用示意图上图中最外面代表整个内存堆,中间灰色的框代表系统运行所需有效对象空间,从垃圾回收的角度看这里有四类对象, 1、 有效对象,在灰色框中的对象,这是系统运行需要的对象,不能被垃圾回收掉2、 独立的无效对象,系统不需要再使用,而且没有如何外部引用,这类对象很快就会被垃圾回收掉3、 一组无效的对象,从整体看

55、系统已经不再需要这些对象,不过这些对象间存在相互依赖,这类对象的回收与垃圾回收线程算法相关,可能不会立刻回收掉,需要一段时间计算4、 被有些对象引用到的无效对象,这就是我们要解决的内存泄漏的对象对于程序员来说,GC基本是透明的,只有几个函数可以访问GC,常见的是运行GC的函数System.gc(),但是根据Java语言规范定义, 该函数不保证JVM的垃圾收集器一定会执行。不同的JVM实现者可能使用不同的算法管理GC。通常GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。通常来说,程序员

56、不需要关心这些,除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序执行而进行垃圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行。5.2 注意内存泄漏JAVA的内存回收机制已经极大的减轻了程序员的工作量。在大部分情况下,程序员不需要去考虑内存的显式回收。但是仍然需要注意某些情况:特别注意凡是调用了addXXXListerner或者registerXXXListerner或者类似的方法,在退出的时候一定要仔细,看是否需要调用removeXXX方法或者unregis

57、terXXX方法。6 JDBC6.1 Statement、Preparedstatement和CallableStatementStatement每次在执行Statement对象上的SQL语句的时候,都将SQL语句传给数据库,在数据库上执行前都需要进行解析和编译,而这部分工作是比较费时的相比之下Preparedstatement和CallableStatement采用的是预编译缓存SQL语句的方式,不需要每次执行的时候去做SQL语句的解析和编译工作,在需要频繁或重复执行某个SQL语句的时候效率提升明显。这三种对象典型的使用场景 Statement:当系统中某条SQL语句只需要执行一次,或者很少

58、的几次的时候 Preparedstatement:频繁使用一条SQL语句,数据库将解析、编译SQL语句后做临时缓存 CallableStatement:调用存储过程,这是效率最高的方式,对于频繁使用的一些固定SQL语句,如果有效率上优化的需要可以考虑做为存储过程(当然这样做会存在跨数据库移植的问题)6.2 数据库占用的内存我们平时注意了程序运行的JVM的内存,但是数据库的内存占用也是一个关键的因素,数据库操作设计不合理,会使得数据库占用大量的内存,也会导致内存不够用的情况发生。语句在相同的环境下,循环运行同一条语句(值不同): select占用大量内存,一般5分钟可使内存从30增加到512上限,内存不足; insert 大量操作,内存也会不断增长,但比较缓慢。 update和delete,几乎不会占用多大内存。所以建议:对于程序循环中或者频繁使用的select语句要重点走查,一般情况下可以采用内存缓存方式,避免大量使用select语句;或者采用存储过程。7 参考文献略。

展开阅读全文
温馨提示:
1: 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
2: 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
3.本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
5. 装配图网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
关于我们 - 网站声明 - 网站地图 - 资源地图 - 友情链接 - 网站客服 - 联系我们

copyright@ 2023-2025  zhuangpeitu.com 装配图网版权所有   联系电话:18123376007

备案号:ICP2024067431-1 川公网安备51140202000466号


本站为文档C2C交易模式,即用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。装配图网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知装配图网,我们立即给予删除!