我们经常会遇到这样那样的连接未关闭的问题,连接没有及时关闭导致的直接后果就是内存泄漏直至down机。我们也都知道解决的方式,但是在解决了问题之后经常会思考为什么会这样呢?连接close()掉,然后在创建不是很浪费cpu等系统资源嘛?有没有更好的方法解决呢?大家也经常听到连接池、线程池之类的线程、池的概念,那么究竟这些概念与我们的连接有什么关系呢?
下面我就想就上面的问题谈谈我的一点浅见,请大家批评指正。
大家都知道java语言是一种语言级的多线程机制的面向对象语言。比如说,java的基类object,它就有一些诸如wati(),notify(),notifyall()等线程控制的方法。如果大家学习过操作系统的化,我想应该知道线程存在同步和异步的问题,解决的方法很多,其中就有“信号量机制”实现线程的同步,java的这种同步机制与操作系统大同小异。同步机制是一个比较复杂的问题,如果感兴趣可以找一本操作系统的书看看。
下面简单介绍一些进程和线程的概念:
1. 进程:
进程是资源分配和独立运行的基本单位。进程的定义很多,下面列举一些
Ø 进程是程序的一次执行;
Ø 进程是可以和别的计算机并发执行的计算;
Ø 进程可定义为一个数据结构及能在其上进行操作的一个程序
Ø 进程是一个程序及其数据在处理机上顺序执行时发生的活动;
Ø 进程时程序在一个数据集合上的运行过程,时系统进行资源分配和调度的一个独立单位。
2. 线程
由于进程是一个资源拥有者,因而在进程的创建、撤消和切换过程中,系统必须为之付出较大的时空开销。也正因为如此,在系统中所设置的进程数目不宜过多,进程切换的频率也不宜过高,但这也就限制了并发程度的进一步提高。因此便引出了线程的概念
把线程作为调度和分派的基本单位,而把进程作为资源拥有的基本单位,使传统进程的两个属性分开,线程便能轻装运行,从而显著提高系统的并发程度。
Ø 在同一个进程内可以有多个线程;
Ø 同一个进程内的线程切换不会引起进程切换;
Ø 一个进程的线程切换到另一个进程的线程时会引起进程切换
3. JSP/SERVLET
而我们常用的jsp/ervlet这种j2ee的体系结构正是建立在java的多线程机制之上的。JSP/SERVLET容器会自动使用线程池等技术来支持系统的运行。因此,JSP/SERVLET的实质是一种线程技术,JSP会在运行时被编译成servlet来运行,如图所示:
当客户端向服务器发出一个请求时,servlet容器会分配一个线程专门处理这个请求,线程内容就是JSP/servlet应用程序。
这部分内容与本主体无关,只是顺便说说。
4. 线程池
首先介绍一下线程池:
线程的创建和销毁,以及切换,执行都是要耗费系统资源的。当系统访问量比较大的时候,服务器内就会创建太多的线程,直至资源完全消耗,这对于应用系统的正常运行是有致命伤害的。
为了能够在访问尖峰时限制活动线程的数量,同时减少线程频繁创建和销毁带来的系统开销,提高系统的大访问量的处理性能和速度,需要事先创建一定数量的线程供调用者循环反复使用,这就是“池”技术。
线程的基本原理是基于队列queue这种数据结构的,通过不断查询队列queue是否有可以运行的线程。如果有,就立即运行线程,没有,则锁定等待,直到有新的线程加入被解锁。(这种锁定机制,就是所谓的“信号量机制”)。
一种线程池必须解决如下的问题:死锁、资源不足、并发错误、线程泄漏和请求过载。下面我们具体举一个成熟的开源线程池的例子来说明线程池的原理:
PooledExecutor pool=new PooledExecutor(new BoundedBuffer(20),100);
pool.setMinimumPoolSize(10);//最小线程数为10
poole.setKeepAliveTime(-1);//线程一直运行
上面的语句设置了线程的最大数目为100,这样,就可以保护系统不会因为访问量增加导致线程数目的无限增加。使用该线程池如下:
pool.execute(java.lang.Runnable 自己的线程);
这一句实际上是将“自己的线程”加入一个队列中,而队列(先进先出FIFO)另一段正开启多个线程不断读取这个队列,一旦队列中有空闲的线程,线程管理器就将读取并分配线程来运行它。
public void execute(Runnable command) throws InterruptedException {
for (;;) { //一直循环
synchronized (this) {
if (!shutdown_) { //确保线程池没有关闭
int size = poolSize_; //当前线程池中线程的数目
if (size < minimumPoolSize_) { //如果当前线程数目少于线程池最小数目
addThread(command);
return;
}
//如果目前线程池中有超过或等于最小数目的线程
//分配一个存在的空闲线程来运行command,handOff是队列
if (handOff_.offer(command, 0)) {
return;
}
//如果不能分配已有的线程来运行command,那么创建一个新线程
if (size < maximumPoolSize_) {
addThread(command);
return;
}
}
}
//如果阻塞,则请求帮助
if (getBlockedExecutionHandler().bolckedAction(command)) {
return;
}
}
}
由上面的代码可见,PooledExecutor线程池的原理是,当执行execute加载一个应用系统的线程时,线程池内部首先检查当前线程数目是否达到设定的最小数目。如果没有达到,启动新线程运行;如果达到了,那么检查有无空闲线程可用;如果没有空闲的,则创建新线程,直到达到最大数目。
使用线程池的好处是:首先是循环使用,一经创建后,空闲的线程可以被反复使用,提高了运行效率;其次有最大数目的限制,保证了系统的安全性。
5. 连接池
终于轮到连接池了,通过上面的介绍,我们对线程及线程池都有个一个大致的了解。
在正常情况下,直接使用JDBC调用数据库可以满足一个小型系统的要求,但是当系统规模比较大的情况下,就会出现数据库的访问量迅速提升而令服务器不堪重负的现象,因而为了解决这一性能问题,常常会使用数据库连接池作为一个缓存的方式解决。
连接池类似上面介绍的线程池。
每次数据库连接的建立都需要花费一定的时空费用,而使用连接池,可以事先建立连接。当应用程序需要开始使用时,就从连接池中获取一个连接使用,应用程序使用完毕,通过close()方法将连接归还连接池。讲到这里,我门就不必在担心close()方法会不会影响性能了,完全可以放心大胆的使用。因为,它实际上并没有关闭连接,而是将连接归还连接池,供下次使用。
当并发增加是,连接池会不断的自动创建新的连接满足调用,直到达到连接池的最大数目;当连接池连接减少甚至没有时,连接池自动关闭一些连接,保持最小数目。
因此连接池的使用节省了连接建立时间,消除了数据库频繁连接带来的开销和瓶颈。
小提示:不知道大家有没有注意到配置websphere时有关于连接池最大最小数目的配置。呵呵,道理就在这。
那么,我们经常面对的连接未关闭的问题导致的系统速度很慢的问题就很容易说明了,就是因为线程池已经达到了最大数目,没有可用的了。所以,其他操作只有等待的份,等待那些应用用完了,被垃圾回收了,才能释放出可用的连接。