4.1.2 创建数据库连接对象

    有时候实例化某些对象可能会非常消耗资源,比如创建数据库连接。我们需要考虑缓存这些实例对象,以方便下次能够继续重用它们,我们可以在工厂对象里创建一个缓存池缓存这些实例对象。在J2EE Web应用中,我们经常使用java.lang.ThreadLocal缓存数据连接对象,我们这里首先讲解一下这个类。

    ThreadLocal主要用来管理同一个线程里持有一个实例的副本,即当前的线程绑定一个实例对象,这个对象对于其他线程是不可见的,当线程执行结束后被回收时,JVM便会自动回收这些绑定的对象。

    这里要注意的是,ThreadLocal对象使用了弱引用(Weak Reference),这可以一定程度防止内存泄露。也许有人使用Java很多年了,还不了解弱引用,我们就JVM支持的引用类型做个简单介绍。

    引用类型

    从Java2开始,引用被分为4个级别,我们一般使用到强引用,只要没有引用指向该对象时,这个对象便会被垃圾回收器回收。还有其他3种特别的引用,分别是软引用(Soft Reference)、弱引用(Weak Reference)和幻影引用(Phantom Reference),它们的级别依次减弱,并都弱于强引用。

    软引用:如果一个对象没有强引用只有软引用时,当JVM发现内存不够时,垃圾回收器便会回收这些对象。

    弱引用:如果一个对象只具有弱引用,在每次回收垃圾时,该对象都会被回收。

    幻影引用:如果一个对象仅持有幻影引用,那么它就和没有引用指向它一样,在任何时候该对象都可能被垃圾回收器回收。

    幻影引用和软引用与弱引用的区别在于:幻影引用必须和引用队列(ReferenceQueue)一起使用。幻影引用可以用来跟踪对象被回收的活动,因为当垃圾回收器准备回收一个对象时,如果发现它还有幻影引用,就会在回收之前,把这个幻影引用加入到与之关联的引用队列(ReferenceQueue)中去。

    这样,程序可以通过判断引用队列中是否已经加入了幻影引用,来了解被引用的对象是否将要被垃圾回收器回收,如果发现某个幻影引用已经被加入到引用队列,那么就可以在所引用的对象被回收之前采取必要的行动。

    软引用和弱引用非常适合做缓存,关于它们更详细介绍,请参见java.lang.ref包。

    ThreadLocal

    java. lang.ThreadLocal是如何实现一个线程绑定一个对象副本的呢?

    其实每个线程都包含一个ThreadLocal.ThreadLocalMap类型的变量threadLocals(延迟创建的,节省空间和时间),这个映射(Map)目的就是为每个线程存储对象的副本,由于一个线程可能使用到多个关联到不同的ThreadLocal对象的副本值,所以我们这里使用ThreadLocal.ThreadLocalMap来做映射,ThreadLocal对象便是它的键(Key)。于是,每次调用ThreadLocal的get()方法,其实就是先获取当前线程(Thread.currentThread()),然后从当前线程的threadLocals映射里,查找当前ThreadLocal关联的副本。

    可能大部分人的想法和我当初的想法一样,都是以为在ThreadLocal里使用一个Map,这个Map的键为Thread,值为绑定的副本,如果这样做是有如下问题的。

    当线程回收时,该线程绑定的变量不能被自动地回收:因为变量存储在ThreadLocal里,必须显式地去回收。但如果此变量存储在线程里,那么线程回收时,这个变量没有被其他引用指向的话,它便随着线程一起被销毁。

    另外,不这样做还有一个好处:如果Map在ThreadLocal里,那你必须得考虑多线程同步访问这个Map,但是这样做确实没有什么必要,因为一个线程访问自己空间内的副本,不应该影响其他线程,所以如果把映射放在线程里,就再不需要做同步的处理(一个CPU总是处理一个线程),这样就极大地加快了ThreadLocal的访问速度。

    我们知道,ThreadLocal.ThreadLocalMap映射使用的键是被WeakReference包装的ThreadLocal对象,如果ThreadLocal对象没有其他强引用和软引用指向时,线程也便不会继续持有ThreadLocal对象,根据JVM规范,它会被垃圾回收器在下次回收时被销毁,这一定程度避免了内存泄露。

    但不表示不会出现内存泄露,关于ThreadLocal引起的内存泄露,主要集中在一些对象不能回收,导致其关联的ClassLoader不能被回收的问题上,如果ClassLoader不能被回收,意味着所有被此ClassLoader加载的二进制代码不能被回收,如果使用动态生成字节码的工具或者Java动态代理方法,这个问题将尤为严重。网上有很多文章都在讨论这个问题。

    从Java 1.5开始,加入了remove()方法,这样我们可以显式地调用此方法,释放内存,避免内存泄露,所以使用ThreadLocal要特别注意内存泄露的问题。

    了解这么多之后,我们来看看在J2EE Web应用中如何使用。由于每接收到一个HTTP请求时,就会启用一个线程来处理这个请求,使用ThreadLocal类很容易保证在处理同一个请求的整个过程中,尽可能使用同一个数据库连接对象。代码片段如下。

    figure_0064_0041

    figure_0065_0042

    代码注解

    定义一个ThreadLocal对象来做数据连接的缓冲池,即这句,private final ThreadLocal<Connection>connections=new ThreadLocal<Connection>()。如果当前线程没有绑定的Connection对象(第一次获取数据连接或者已被回收),则connections.get()返回null,这时我们就创建一个新的java.sql.Connection对象,并把它绑定到当前线程,即这句,connection.set(conn),于是,以后当前请求总会得到刚创建的Connection对象。虽然我们这里为每个HTTP请求缓存Connection对象,但在实际应用中,我们并不是直接去创建一个数据连接对象,我们往往从另外的数据连接池获得缓存的Connection对象。试想如果在一个小型应用中,对10000次HTTP请求创建10000个数据连接,往往会引起不必要的数据库瓶颈问题。数据连接池提供多个线程可以重用的数据库连接对象,它们不会随着线程消亡而被关闭或回收,数据库连接池并非本书重点,在这里就不再深入探讨。

    注意:Servlet规范里并未保证每个HTTP请求都会启用一个新的线程对象,比如Tomcat会为每个请求生成一个新的线程,但WebSphere以及OC4J的某些版本,如果是相同的客户端发送过来的请求,应用服务器有可能重用了前一请求的线程来继续处理当前请求,所以要在适当时候(比如请求处理结束的时候或者请求开始时)使用ThreadLocal的remove方法移除connection变量以防止出错。