Spring容器初始化死锁的问题

发现

今天对系统修改了一些代码,在日常、预发都正常,但是发布到线上的时候,部署10多分钟都没有启动应用,aone日志一直显示tomcat start ,直到超时。登录到服务器上查看日志,翻遍各种日志没有发现异常报错。注释掉notify部分的功能,启动正常。

排查

notify初始化问题?

仔细查看了tomcat_stdout.log,发现日志停留在notify初始化之后,难道是notify初始化异常了?

notify.png

查看/home/admin/logs/notify/notify_client.log日志文件,没有报错

用命令查看下notify的连接情况

连接也一切正常

依赖包版本冲突?

输入命令查看依赖树

把notify下面的依赖冲突全部解决了一遍,启动问题依然存在

死锁了?

在https://ops.alibaba-inc.com/page/appManage.html#appManage 找到相关的应用,对有问题的机器执行jstack操作,然后在http://zprofiler.alibaba-inc.com/thread/index.htm 中分析

image

果然有死锁,罪魁祸首终于找到了,那我们看看是怎么死锁了

image

image

线程msgWorkTP-1734460873-1-thread-8 锁定了 0x0000000771ae7518 等待 0x0000000771aaab08

线程Catalina-startStop-1 锁住了 0x0000000771aaab08 等待 0x0000000771ae7518

分析

死锁的出现在DefaultListableBeanFactory.getBeanDefinitionNames()、DefaultSingletonBeanRegistry.getSingleton()和DefaultListableBeanFactory.preInstantiateSingletons(),那我们就看看具体实现。

image

image

image

image

线程Catalina-startStop-1初始化singleton bean时,需要获取beanDefinitionMap的锁,并在判断是否为FactoryBean的时候需要通过getSingleton()获取实体,在获取实体的时候获取singletonObjects的锁,以保证singleton的bean只初始化一次。

线程msgWorkTP-1734460873-1-thread-8是notify初始化完成,接收到消息,需要获得bean进行请求处理,通过getBean()也调用了getSingleton(),此时也需要获取sigletonObjects的锁,再层层调用到了DefaultListableBeanFactory.getBeanDefinitionNames(),这时候又需要beanDefinitionMap的锁。

所以线程Catalina-startStop-1在初始化的时候先锁定了beanDefinitionMap,在等待singletonObjects的锁,但是这个时候msgWorkTP-1734460873-1-thread-8已经获得了sigletonObjects的锁,等待beanDefinitionMap锁,这样就导致了死锁

解决

1、Spring 3.1.4之前的版本都存在并发死锁的问题,可以采用了升级spring的方案解决

2、可以在容器初始化之后,在对notify的消息进行处理

总结

出现这个问题是因为,我的应用在notify初始化之后,接收消息进行业务处理,这个时候容器还没有初始化完成而导致的死锁,日常和预发没有出现问题是因为没有消息推送。

之前没有遇到此类问题,导致在排查问题的时候走了弯路,在没有任何日志的情况下,没有立马想到可能是死锁问题。

发表在 Java | 留下评论

Android开发(四)Touch事件机制

Android中处理touch事件的两个方法:onInterceptTouchEvent()和onTouchEvent()。

onInterceptTouchEvent()是ViewGroup的一个方法,目的是在系统向该ViewGroup及其各个childView触发onTouchEvent()之前对相关事件进行一次拦截,由于ViewGroup会包含若干childView,因此需要能够统一监控各种touch事件的机会,因此纯粹的不能包含子view的控件是没有这个方法的,如LinearLayout就有,TextView就没有。

Touch事件在该view以及子view之间的传递取决于onInterceptTouchEvent()和onTouchEvent()的返回值,onInterceptTouchEvent()的默认值为false,onTouchEvent()的默认值父view为false,最底层的子view为true,返回true表示本view可以解决touch事件。

  • onInterceptTouchEvent决定是否把事件传递给子view进行处理,true表示不传递,自己处理,那么子view就不会接收到touch事件。
  • onTouchEvent决定是否把时间传递传递给父view进行处理,true表示不传递,那么父view就接收不到touch事件。

再来看一下onInterceptTouchEvent和onTouchEvent事件的传递顺序,假设三个view A、B、C,A包含B,B包含C,那么oninterceptTouchEvent的传递顺序为A->B->C,onTouchEvent的顺序为C->B->A。

看个列子:

布局:Linelayout1包含Linelayout2,Linelayout2包含EWSwipeListView

 

Activity使用该布局,并且初始化EWSwipeListView的数据:

EWSwipeListView继承自ListView:

LineLayout1和LineLayout2继承自LineLayout,重写了onInterceptEvent()和onTouchEvent():

 

第一次运行,所有的返回值按照默认的:

 

onInterceptTouchEvent传递到了EwSwipeListView,被onTouchEvent消费掉,LineLayout1和LineLayout2都不能接收到onTouchEvent事件。

第二次运行,把LineLayout2的onInterceptTouchEvent返回值改成true:

Linelayout2的onInterceptTouchEvent返回了true,那么EWSwipeListView就不会收到任何事件,这里LineLayout2的onTouchEvent返回了false,把事件传递给了LineLayout1的父view,这种LineLayout2的onInterceptToucheEvent返回true,同时onTouchEvent返回false,导致之后的Move和Up事件也接收不到。

第三次运行,把LineLayout2的onTouchEvent返回改成true:

这样LineLayout1的onTouchEvent就不会被触发,并且LineLayout2的Move和Up事件都能接受到。

发表在 Android | 留下评论

Android开发(三)数据存储

Android系统提供了3种数据存储方式,分别是SharedPreferences、SQLite、File,数据存储在data/data/<包名>/目录下。

一、SharedPreferences

SharedPreferences是一种轻型的数据存储方式,它的本质是基于XML文件存储key-value键值对数据,通常用来存储一些简单的配置信息。其存储位置在/data/data/<包名>/shared_prefs目录下。SharedPreferences对象本身只能获取数据而不支持存储和修改,存储修改是通过Editor对象实现

数据写入到SharedPreferences:

执行了这段代码之后,即在/data/data/<包名>/shared_prefs目录下生成一个Share.xml文件,一个应用可以创建多个这样的文件。

读取SharedPreferences中的数据:

二、SQLite

SQLite是轻量级嵌入式数据库引擎,它支持 SQL 语言,并且只利用很少的内存就有很好的性能。此外它还是开源的,任何人都可以使用它。许多开源项目((Mozilla, PHP, Python)都使用了 SQLite.SQLite 由以下几个组件组成:SQL 编译器、内核、后端以及附件。SQLite 通过利用虚拟机和虚拟数据库引擎(VDBE),使调试、修改和扩展 SQLite 的内核变得更加方便。

SQLite只有5中数据类型,分别为NULL(空值)、INTEGER(整型)、REAL(浮点型)、TEXT(文本)、BLOB(大数据)。在SQLite中,并没有专门设计BOOLEAN和DATE类型,因为BOOLEAN型可以用INTEGER的0和1代替true和false,而DATE类型则可以拥有特定格式的TEXT、REAL和INTEGER的值来代替显示,为了能方便的操作DATE类型,SQLite提供了一组函数,详见:http://www.sqlite.org/lang_datefunc.html。这样简单的数据类型设计更加符合嵌入式设备的要求。关于SQLite的更多资料,请参看:http://www.sqlite.org/

不管第三个参数是否包含数据,执行Insert()方法必然会添加一条记录,如果第三个参数为空,会添加一条除主键之外其他字段值为Null的记录。Insert()方法内部实际上通过构造insert SQL语句完成数据的添加,Insert()方法的第二个参数用于指定空值字段的名称,相信大家对该参数会感到疑惑,该参数的作用是什么?是这样的:如果第三个参数values 为Null或者元素个数为0, 由于Insert()方法要求必须添加一条除了主键之外其它字段为Null值的记录,为了满足SQL语法的需要, insert语句必须给定一个字段名,如:insert into person(name) values(NULL),倘若不给定字段名 , insert语句就成了这样: insert into person() values(),显然这不满足标准SQL的语法。对于字段名,建议使用主键之外的字段,如果使用了INTEGER类型的主键字段,执行类似insert into person(personid) values(NULL)的insert语句后,该主键字段值也不会为NULL。如果第三个参数values 不为Null并且元素的个数大于0 ,可以把第二个参数设置为null。

三、File

Android的文件系统和其他平台的文件系统是类型。File对象适合用于存储大数据量的数据,比如图片、网络上的数据等。

Android文件存储分为两部分:内部存储(如手机内存)和外部存储(如SD卡)

1)内部存储

内部存储总是可用的,只有本应用可以访问内部存储区域的数据,当用户卸载了应用的时候,内部存储区域的数据就会随之删除

2)外部存储

外部存储不一定总是可用的,因为有时候可能会被移除,所以在使用外部存储之前需要判断是否可用。外部存储中的数据是对外公开的,其他的应用也可以访问,当用户卸载应用的时候,系统只会删除存储在通过getExternalFilesDir()方法返回的目录下的数据。

外部存储需要在AndroidManifest.xml加入权限

发表在 Android | 留下评论

Android开发(二)Activity生命周期

生命周期图

basic-lifecycle

3种状态

1、Resumed(running):运行状态

Activity在屏幕前台显示,并且有用户焦点,可以操作。

2、Paused:暂停状态

被其他的部分透明的或者没有占全部屏幕的Activity覆盖,但是这个Activity还是可见的,存活的,它保留着所有的状态和成员信息,但用户不能操作,也不能执行任何代码。

3、Stopped:停止状态

与暂停状态不同的是,Stopped状态的Activity是不可见的,是被其他Activity完全覆盖的。

注:Created和Started状态是透明的,在Activity启动时,很快的调用onCreate、onStart、onResume方法,跳转到了Resumed状态。

7个方法

1、onCreate

启动Activity时调用,创建Activity,初始化数据。

2、onStart

在系统调用了oncreate或者onRestart之后调用,使Activity将要被展现给用户。

3、onRestart

在调用了onStop之后,又要重新展现给用户的时候调用。

4、onResume

系统调用了onStart之后调用,使Activity展现给用户。

5、onPause

当Activity被调到后台执行时调用,这是的Activity还是存活的。

6、onStop

在Activity不可见的时候调用,但是也有可能在内存很低的情况下调用了onPause之后就被销毁,导致没有被调用。

7、onDestroy

当调用了finish或者因为内存不足情况下调用该方法来销毁Activity

3种生命周期

1、Activity的entire lifetime(全部的生命期)

发生在调用onCreate()和调用onDestory()之间。在onCreate()方法中执行全局状态的建立(例如定义布局),在onDestroy()方法中释放所有保存的资源。

2、Activity的visible lifetime(可见的生命期)

发生在调用onStart()和onStop()之间。在这个期间,用户能在屏幕上看见Activity,和它进行交互。系统在Activity的完整寿命中可能多次调用onStart()和onStop(),正如Activity交替地对用户可见或隐藏。

3、Activity的foreground lifetime (前台的生命期)

发生在调用onResume()和onPause()之间。在这期间,Activity在屏幕上所有其他Activity的前面,有用户输入焦点。一个Activity能频繁的在前台进入和出去之间转变。

lifecycle2

 

发表在 Android | 留下评论

Android开发(一)启动模拟器报错

在使用Android Studio启动模拟器的时候报错:

1

报错的信息显示HAXM没有安装或者不可用,可是“HAXM”是什么东西?

221559393438600

在官方完整上查到这个HAXM是一个管理硬件加速的,加快模拟器的启动。

接下来看看是否没有安装,在SDK Manager中找到这个东西也是已经安装了的。

3

其实不然,这里仅仅是SDK Manager 帮我们下载了,我们还需要手动的安装一下,找到下载目录,在{SDK_HOME}/extras/intel/Hardware_Accelerated_Execution_Manager

4

双击安装即可,然后命令行中输入 sc query intelhaxm 查看是否运行正常

5

可以看到已经正常的运行了,再次启动模拟器即可看到界面了。

还有可能在安装HAXM的时候报错,无法安装,虚拟技术没有打开:

那就需要在BIOS中打开虚拟技术(Virtualization Technology)

6

7

然后重新再安装一下HAXM即可。

发表在 Android | 留下评论

java虚拟机内存的各个区域及其作用

1、运行时数据区域

运行时数据区域包括方法区、虚拟机栈、本地方法栈、堆、程序计数器。其中方法区和堆是所有线程共享的数据区,其他的是线程隔离的数据区。

1.1、程序计数器

程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器,确定下一条需要执行的字节码指令。java的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何确定的一个时刻,一个处理器只会执行一条线程中的指令。为了线程切换之后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间的计数器互不影响。如果线程正在执行的是一个java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是native方法,则计数器值为空。

1.2、java虚拟机栈

java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法被执行的时候都会创建一个栈帧用于存在局部变量表、操作栈、动态链接、方法出口等信息。通过所说的栈是局部变量表,即与对象内存分配关系最密切的内存区域。局部变量表的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是确定的,在运行期不会改变。

java虚拟机栈有两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,则抛弃StackOverflowError异常;如果虚拟机栈可以动态扩展的,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

1.3、本地方法栈

本地方法栈与虚拟机栈所发挥的作用是相似的,区别在于虚拟机栈为虚拟机执行java方法的服务,本地方法栈则是为虚拟机使用到native方法服务。

1.4、java堆

java堆是虚拟机所管理的内存中最大的一块,是虚拟机启动是创建的能被所有线程共享的一块内存区域。java堆的唯一目的就是存放对象实例,几乎所有的对象实例和数组都在这里分配内存(随着JTI编译器的发展,在栈上也有可能分配)。java堆是垃圾收集器管理的主要区域,在物理上可以使不连续的内存空间,但在逻辑上是联系的。

如果再堆中没有内存完成实例的分配,并且堆也无法在扩展的时候,将会抛出OutOfMemoryError异常。

1.5、方法区

方法区也是线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。这块区域很少进行垃圾回收,甚至可以不实现垃圾收集,主要是针对常量池的回收和对类型的卸载。当方法区无法分配内存的时候,将抛出OutOfMemoryError异常。

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译期的各种字面量和符号引用。并非预置入Class文件中的常量才能进入常量池,运行期间也可能将新的常量放入池中,开发中用的比较多的是String类的intern()方法。

2、例子解析

Object obj = new Object();

假设这句代码出现在方法体中,那么Object obj将会反映到java栈的局部变量表中,作为一个reference类型数据出现,new Object()将会反映到java堆中,形成一块存储了Object类型的实例数据的结构化内存,此对象类型数据,如对象类型、父类、实现的接口、方法等信息存储在方法区。

发表在 Java | 标签为 , | 留下评论

SSO实现方案

一、单点登录介绍
单点登录的机制比较简单,如下图所示:
1、当用户第一次访问应用系统的时候,因为用户还没有登录,应用系统会向认证系统请求ticket。
2、认证系统接收到ticket的请求,如果用户已经登录则返回ticket,进行第4步操作,如果用户还没有登录,则引导到认证系统的登录页面。
3、用户提交用户名密码,认证系统进行身份效验,如果通过效验,应该返回给用户一个认证的凭据(ticket)到应用系统
4、应用系统接收到ticket,向认证系统发起验证ticket的请求
5、认证系统通过ticket的验证,并删除掉保存在认证系统的ticket,然后返回用户信息
6、应用系统接收到返回的用户信息,然后通过本地认证,返回受保护资源给用户
二、认证系统的实现
    在认证系统中接入应用系统。

   认证系统的登录认证,如果只是登录到认证系统,则进去认证系统的首页,如果请求中包含appId和callback参数的,则说明是应用系统的登录请求,则需要返回到应用系统,并且返回ticket。generateTicket()是生成ticket的,一般是一串加密串,我这里为了简单起见直接使用UUID。注:这里还返回了sessionId,因为下面的应用系统请求验证ticket的时候,使用的HttpClient,发送的是http请求,没有sessionId,则读取不到认证系统中的session。

    认证系统接收应用系统的验证ticket请求,并返回登录用户的信息

三、应用系统实现

    用户访问应用系统的受保护资源,如果用户还未在本应用系统登录过,应用系统会向认证系统发起获取ticket的请求

应用系统接收到认证系统返回的ticket,然后发起验证ticket的请求

四、示例下载及运行说明

    示例程序需要maven支持,所以需要运行的示例的,需要安装maven;在hosts文件中配置一条域名解析(127.0.0.1 www.server.com  www.client.com),然后运行test源文件夹下的JettyServer和JettyClient即可,测试url:http://www.client.com:8081/client/admin.htm

 

发表在 Java | 留下评论

Java线程的状态及状态的切换

1、NEW(新建):通过New关键字创建了Thread类(或其子类)的对象
2、RUNNABLE(就绪):Thread类的对象调用了start()方法,这时的线程就等待时间片轮转到自己这,以便获得CPU;第二种情况是线程在处于RUNNABLE状态时并没有运行完自己的run方法,时间片用完之后回到RUNNABLE状态;还有种情况就是处于BLOCKED状态的线程结束了当前的BLOCKED状态之后重新回到RUNNABLE状态。
3、RUNNING(运行):这时的线程指的是获得CPU的RUNNABLE线程。
4、DEAD(死亡):处于RUNNING状态的线程,在执行完run方法之后,就变成了DEAD状态了。
5、BLOCKED(阻塞):这种状态指的是处于RUNNING状态的线程,出于某种原因,比如调用了sleep方法、等待用户输入等而让出当前的CPU给其他的线程。
线程被阻塞可能是由于下面五方面的原因:
1.调用sleep(毫秒数),使线程进入睡眠状态。在规定时间内,这个线程是不会运行的。
2.用suspend()暂停了线程的执行。除非收到resume()消息,否则不会返回“可运行”状态。
3.用wait()暂停了线程的执行。除非线程收到notify()或notifyAll()消息,否则不会变成“可运行”状态。
4.线程正在等候一些IO操作完成。
5.线程试图调用另一个对象的“同步”方法,但那个对象处于锁定状态,暂时无法使用。
发表在 Java | 留下评论

Java程序优化的一些最佳实践

一、衡量程序的标准

衡量一个程序是否优质,可以从多个角度进行分析。其中,最常见的衡量标准是程序的时间复杂度、空间复杂度,以及代码的可读性、可扩展性。针对程序的时间复杂度和空间复杂度,想要优化程序代码,需要对数据结构与算法有深入的理解,并且熟悉计算机系统的基本概念和原理;而针对代码的可读性和可扩展性,想要优化程序代码,需要深入理解软件架构设计,熟知并会应用合适的设计模式。

  • 首先,如今计算机系统的存储空间已经足够大了,达到了 TB 级别,因此相比于空间复杂度,时间复杂度是程序员首要考虑的因素。为了追求高性能,在某些频繁操作执行时,甚至可以考虑用空间换取时间。
  • 其次,由于受到处理器制造工艺的物理限制、成本限制,CPU主频的增长遇到了瓶颈,摩尔定律已渐渐失效,每隔18个月CPU主频即翻倍的时代已经过去了,程序员的编程方式发生了彻底的改变。在目前这个多核多处理器的时代,涌现了原生支持多线程的语言(如Java)以及分布式并行计算框架(如Hadoop)。为了使程序充分地利用多核CPU,简单地实现一个单线程的程序是远远不够的,程序员需要能够编写出并发或者并行的多线程程序。
  • 最后,大型软件系统的代码行数达到了百万级,如果没有一个设计良好的软件架构,想在已有代码的基础上进行开发,开发代价和维护成本是无法想象的。一个设计良好的软件应该具有可读性和可扩展性,遵循“开闭原则”、“依赖倒置原则”、“面向接口编程”等。

二、项目介绍

本文将介绍笔者经历的一个项目中的一部分,通过这个实例剖析代码优化的过程。下面简要地介绍该系统的相关部分。

该系统的开发语言为Java,部署在共拥有4核CPU的Linux服务器上,相关部分主要有以下操作:通过某外部系统D提供的REST API获取信息,从中提取出有效的信息,并通过JDBC 存储到某数据库系统S中,供系统其他部分使用,上述操作的执行频率为每天一次,一般在午夜当系统空闲时定时执行。为了实现高可用性(High Availability),外部系统D部署在两台服务器上,因此需要分别从这两台服务器上获取信息并将信息插入数据库中,有效信息的条数达到了上千条,数据库插入操作次数则为有效信息条数的两倍。

图 1.系统体系结构图

为了快速地实现预期效果,在最初的实现中优先考虑了功能的实现,而未考虑系统性能和代码可读性等。系统大致有以下的实现:

  1. REST API获取信息、数据库操作可能抛出的异常信息都被记录到日志文件中,作为调试用;
  2. 共有5次数据库连接操作,包括第一次清空数据库表,针对两个外部系统D各有两次数据库插入操作,这5个连接都是独立的,用完之后即释放;
  3. 所有的数据库插入语句都是使用java.sql.Statement类生成的;
  4. 所有的数据库插入语句,都是单条执行的,即生成一条执行一条;
  5. 整个过程都是在单个线程中执行的,包括数据库表清空操作,数据库插入操作,释放数据库连接;
  6. 数据库插入操作的JDBC代码散布在代码中。虽然这个版本的系统可以正常运行,达到了预期的效果,但是效率很低,从通过 REST API获取信息,到解析并提取有效信息,再到数据库插入操作,总共耗时100秒左右。而预期的时间应该在一分钟以内,这显然是不符合要求的。

三、代码优化过程

笔者开始分析整个过程有哪些耗时操作,以及如何提升效率,缩短程序执行的时间。通过REST API获取信息,因为是使用外部系统提供的API,所以无法在此处提升效率;取得信息之后解析出有效部分,因为是对特定格式的信息进行解析,所以也无效率提升的空间。所以,效率可以大幅度提升的空间在数据库操作部分以及程序控制部分。下面,分条叙述对耗时操作的改进方法。

1.  针对日志记录的优化

关闭日志记录,或者更改日志输出级别。因为从两台服务器的外部系统D上获取到的信息是相同的,所以数据库插入操作会抛出异常,异常信息类似于“Attempt to insert duplicate record”,这样的异常信息跟有效信息的条数相等,有上千条。这种情况是能预料到的,所以可以考虑关闭日志记录,或者不关闭日志记录而是更改日志输出 级别,只记录严重级别(severe level)的错误信息,并将此类操作的日志级别调整为警告级别(warning level),这样就不会记录以上异常信息了。本项目使用的是 Java 自带的日志记录类,以下配置文件将日志输出级别设置为严重级别。

清单 1. log.properties 设置日志输出级别的片段

通过上述的优化之后,性能有了大幅度的提升,从原来的 100 秒左右降到了 50 秒左右。为什么仅仅不记录日志就能有如此大幅度的性能提升呢?查阅资料,发现已经有人做了相关的研究与实验。经常听到 Java 程序比 C/C++ 程序慢的言论,但是运行速度慢的真正原因是什么,估计很多人并不清楚。对于 CPU 密集型的程序(即程序中包含大量计算),Java 程序可以达到 C/C++ 程序同等级别的速度,但是对于 I/O 密集型的程序(即程序中包含大量 I/O 操作),Java 程序的速度就远远慢于 C/C++ 程序了,很大程度上是因为 C/C++ 程序能直接访问底层的存储设备。因此,不记录日志而得到大幅度性能提升的原因是,Java 程序的 I/O 操作较慢,是一个很耗时的操作。

2.  针对数据库连接的优化

共享数据库连接。共有 5 次数据库连接操作,每次都需重新建立数据库连接,数据库插入操作完成之后又立即释放了,数据库连接没有被复用。为了做到共享数据库连接,可以通过单例模式 (Singleton Pattern)获得一个相同的数据库连接,每次数据库连接操作都共享这个数据库连接。这里没有使用数据库连接池(Database Connection Pool)是因为在程序只有少量的数据库连接操作,只有在大量并发数据库连接的时候才需要连接池。

清单 2. 共享数据库连接的代码片段

通过上述的优化之后,性能有了小幅度的提升,从 50 秒左右降到了 40 秒左右。共享数据库连接而得到的性能提升的原因是,数据库连接是一个耗时耗资源的操作,需要同远程计算机进行网络通信,建立 TCP 连接,还需要维护连接状态表,建立数据缓冲区。如果共享数据库连接,则只需要进行一次数据库连接操作,省去了多次重新建立数据库连接的时间。

3.  针对插入数据库记录的优化 – 1

使用预编译 SQL。具体做法是使用 java.sql.PreparedStatement 代替 java.sql.Statement 生成 SQL 语句。PreparedStatement 使得数据库预先编译好 SQL 语句,可以传入参数。而 Statement 生成的 SQL 语句在每次提交时,数据库都需进行编译。在执行大量类似的 SQL 语句时,可以使用 PreparedStatement 提高执行效率。使用 PreparedStatement 的另一个好处是不需要拼接 SQL 语句,代码的可读性更强。通过上述的优化之后,性能有了小幅度的提升,从 40 秒左右降到了 30~35 秒左右。

清单 3. 使用 Statement 的代码片段

清单 4. 使用 PreparedStatement 的代码片段

4.  针对插入数据库记录的优化 – 2

使用 SQL 批处理。通过 java.sql.PreparedStatement 的 addBatch 方法将 SQL 语句加入到批处理,这样在调用 execute 方法时,就会一次性地执行 SQL 批处理,而不是逐条执行。通过上述的优化之后,性能有了小幅度的提升,从 30~35 秒左右降到了 30 秒左右。

5.  针对多线程的优化

使用多线程实现并发 / 并行。清空数据库表的操作、把从 2 个外部系统 D 取得的数据插入数据库记录的操作,是相互独立的任务,可以给每个任务分配一个线程执行。清空数据库表的操作应该先于数据库插入操作完成,可以通过 java.lang.Thread 类的 join 方法控制线程执行的先后次序。在单核 CPU 时代,操作系统中某一时刻只有一个线程在运行,通过进程 / 线程调度,给每个线程分配一小段执行的时间片,可以实现多个进程 / 线程的并发(concurrent)执行。而在目前的多核多处理器背景下,操作系统中同一时刻可以有多个线程并行(parallel)执行,大大地提高了 计算速度。

清单 5. 使用多线程的代码片段

通过上述的优化之后,性能有了大幅度的提升,从 30 秒左右降到了 15 秒以下,10~15 秒之间。使用多线程而得到的性能提升的原因是,系统部署所在的服务器是多核多处理器的,使用多线程,给每个任务分配一个线程执行,可以充分地利用 CPU 计算资源。

笔者试着给每个任务分配两个线程执行,希望能使程序运行得更快,但是事与愿违,此时程序运行的时间反而比每个任务分配一个线程执行的慢,大约 20 秒。笔者推测,这是因为线程较多(相对于 CPU 的内核数),使得 CPU 忙于线程的上下文切换,过多的线程上下文切换使得程序的性能反而不如之前。因此,要根据实际的硬件环境,给任务分配适量的线程执行。

6.  针对设计模式的优化

使用 DAO 模式抽象出数据访问层。原来的代码中混杂着 JDBC 操作数据库的代码,代码结构显得十分凌乱。使用 DAO 模式(Data Access Object Pattern)可以抽象出数据访问层,这样使得程序可以独立于不同的数据库,即便访问数据库的代码发生了改变,上层调用数据访问的代码无需改变。并且程 序员可以摆脱单调繁琐的数据库代码的编写,专注于业务逻辑层面的代码的开发。通过上述的优化之后,性能并未有提升,但是代码的可读性、可扩展性大大地提高 了。

清单 6. 使用 DAO 模式的代码片段

回顾以上代码优化过程:关闭日志记录、共享数据库连接、使用预编译 SQL、使用 SQL 批处理、使用多线程实现并发 / 并行、使用 DAO 模式抽象出数据访问层,程序运行时间从最初的 100 秒左右降低到 15 秒以下,在性能上得到了很大的提升,同时也具有了更好的可读性和可扩展性。

四、结束语

通过该项目实例,笔者深深地感到,想要写出一个性能优化、可读性可扩展性强的程序,需要对计算机系统的基本概念、原理,编程语言的特性,软件系统 架构设计都有较深入的理解。“纸上得来终觉浅,绝知此事要躬行”,想要将这些基本理论、编程技巧融会贯通,还需要不断地实践,并总结心得体会。

注:本文转自CSDN http://www.csdn.net/article/2013-05-02/2815100-Java

发表在 Java | 留下评论

EGit使用说明(三)

5. 将工程上传到GitHub
登录到github.com上,并创建一个库,不要勾选Initialize this repositiory with a README,否则在push到GitHub的时候会报master[rejected – non-fast-forward]错误。

右击GitTest项目 Team -> remote -> push 提交本地仓库到远程仓库,也就是提交到GitHub上。

发表在 EGit, 开发工具 | 标签为 | 留下评论