Vincent

Vicent's blog
随笔 - 74, 文章 - 0, 评论 - 5, 引用 - 0
数据加载中……

Java理论与实践: 它是谁的对象?

在没有垃圾收集的语言中,比如C++,必须特别关注内存管理。对于每个动态对象,必须要么实现引用计数以模拟 垃圾收集效果,要么管理每个对象的“所有权”――确定哪个类负责删除一个对象。通常,对这种所有权的维护并没有什么成文的规则,而是按照约定(通常是不成文的)进行维护。尽管垃圾收集意味着Java开发者不必太多地担心内存 泄漏,有时我们仍然需要担心对象所有权,以防止数据争用(data races)和不必要的副作用。在这篇文章中,Brian Goetz 指出了一些这样的情况,即Java开发者必须注意对象所有权。请在 论坛上与作者及其他读者共享您对本文的一些想法(您也可以在文章的顶部或底部点击 讨论来访问论坛)。

如果您是在1997年之前开始学习编程,那么可能您学习的第一种编程语言没有提供透明的垃圾收集。每一个new 操作必须有相应的delete操作 ,否则您的程序就会泄漏内存,最终内存分配器(memory allocator )就会出故障,而您的程序就会崩溃。每当利用 new 分配一个对象时,您就得问自己,谁将删除该对象?何时删除?

别名, 也叫做 ...

内存管理复杂性的主要原因是别名使用:同一块内存或对象具有 多个指针或引用。别名在任何时候都会很自然地出现。例如,在清单 1 中,在 makeSomething 的第一行创建的 Something 对象至少有四个引用:

  • something 引用。
  • 集合 c1 中至少有一个引用。
  • 当 something 被作为参数传递给 registerSomething 时,会创建临时 aSomething 引用。
  • 集合 c2 中至少有一个引用。

清单 1. 典型代码中的别名
												
														    Collection c1, c2;
    
    public void makeSomething {
        Something something = new Something();
        c1.add(something);
        registerSomething(something);
    }

    private void registerSomething(Something aSomething) {
        c2.add(aSomething);
    }

												
										

在非垃圾收集语言中需要避免两个主要的内存管理危险:内存泄漏和悬空指针。为了防止内存泄漏,必须确保每个分配了内存的对象最终都会被删除。 为了避免悬空指针(一种危险的情况,即一块内存已经被释放了,而一个指针还在引用它),必须在最后的引用释放之后才删除对象。为满足这两条约束,采用一定的策略是很重要的。

为内存管理而管理对象所有权
除了垃圾收集之外,通常还有其他两种方法用于处理别名问题: 引用计数和所有权管理。引用计数(reference counting)是对一个给定的对象当前有多少指向它的引用保留有一个计数,然后当最后一个引用被释放时自动删除该对象。在 C和20世纪90年代中期之前的多数 C++ 版本中,这是不可能自动完成的。标准模板库(Standard Template Library,STL)允许创建“灵巧”指针,而不能自动实现引用计数(要查看一些例子,请参见开放源代码 Boost 库中的 shared_ptr 类,或者参见STL中的更加简单的 auto_ptr 类)。

所有权管理(ownership management) 是这样一个过程,该过程指明一个指针是“拥有”指针("owning" pointer),而 所有其他别名只是临时的二类副本( temporary second-class copies),并且只在所拥有的指针被释放时才删除对象。在有些情况下,所有权可以从一个指针“转移”到另一个指针,比如一个这样的方法,它以一个缓冲区作为参数,该方法用于向一个套接字写数据,并且在写操作完成时删除这个缓冲区。这样的方法通常叫做接收器 (sinks)。在这个例子中,缓冲区的所有权已经被有效地转移,因而进行调用的代码必须假设在被调用方法返回时缓冲区已经被删除。(通过确保所有的别名指针都具有与调用堆栈(比如方法参数或局部变量)一致的作用域(scope ),可以进一步简化所有权管理,如果引用将由非堆栈作用域的变量保存,则通过复制对象来进行简化。)





回页首


那么,怎么着?

此时,您可能正纳闷,为什么我还要讨论内存管理、别名和对象所有权。毕竟,垃圾收集是 Java语言的核心特性之一,而内存管理是已经过时的一件麻烦事。就让垃圾收集器来处理这件事吧,这正是它的工作。那些从内存管理的麻烦中解脱出来的人不愿意再回到过去,而那些从未处理过内存管理的人则根本无法想象在过去倒霉的日子里――比如1996年――程序员的编程是多么可怕。





回页首


提防悬空别名

那么这意味着我们可以与对象所有权的概念说再见了吗?可以说是,也可以说不是。 大多数情况下,垃圾收集确实消除了显式资源存储单元分配(explicit resource deallocation)的必要(在以后的专栏中我将讨论一些例外)。但是,有一个区域中,所有权管理仍然是Java 程序中的一个问题,而这就是悬空别名(dangling aliases)问题。 Java 开发者通常依赖于这样一个隐含的假设,即假设由对象所有权来确定哪些引用应该被看作是只读的 (在C++中就是一个 const 指针),哪些引用可以用来修改被引用的对象的状态。当两个类都(错误地)认为自己保存有对给定对象的惟一可写的引用时,就会出现悬空指针。发生这种情况时,如果对象的状态被意外地更改,这两个类中的一个或两者将会产生混淆。

一个贴切的例子

考虑清单 2 中的代码,其中的 UI 组件保存有一个 Point 对象,用于表示它的位置。当调用 MathUtil.calculateDistance 来计算对象移动了多远时,我们依赖于一个隐含而微妙的假设――即 calculateDistance 不会改变传递给它的 Point 对象的状态,或者情况更坏,维护着对那些 Point 对象的一个引用(比如通过将它们保存在集合中或者将它们传递到另一个线程),然后这个引用将用于在 calculateDistance 返回后更改Point 对象的状态。 在 calculateDistance的例子中,为这种行为担心似乎有些可笑,因为这明显是一个可怕的违背惯例的情况。但是,如果要说将一个可变的对象传递给一个方法,之后对象还能够毫发无损地返回来,并且将来对于对象的状态也不会有不可预料的副作用(比如该方法与另一个线程共享引用,该线程可能会等待5分钟,然后更改对象的状态),那么这只不过是一厢情愿的想法而已。


清单 2. 将可变对象传递给外部方法是不可取的
												
														    private Point initialLocation, currentLocation;

    public Widget(Point initialLocation) {
        this.initialLocation = initialLocation;
        this.currentLocation = initialLocation;
    }

    public double getDistanceMoved() {
        return MathUtil.calculateDistance(initialLocation, currentLocation);
    }
    
    . . . 

    // The ill-behaved utility class MathUtil
    public static double calculateDistance(Point p1, 
                                           Point p2) {
        double distance = Math.sqrt((p2.x - p1.x) ^ 2 
                                    + (p2.y - p1.y) ^ 2);
        p2.x = p1.x;
        p2.y = p1.y;
        return distance;
    }

												
										

一个愚蠢的例子

大家对该例子明显而普遍的反应就是――这是一个愚蠢的例子――只是强调了这样一个事实,即对象所有权的概念在 Java 程序中依然存在,而且存在得很好,只是没有说明而已。calculateDistance 方法不应该改变它的参数的状态,因为它并不“拥有”它们――当然,调用方法拥有它们。因此说不用考虑对象所有权。

下面是一个更加实用的例子,它说明了不知道谁拥有对象就有可能会引起混淆。再次考虑一个以Point 属性 来表示其位置的 UI组件。 清单 3 显示了实现存取器方法 setLocation 和 getLocation的三种方式。第一种方式是最懒散的,并且提供了最好的性能,但是对于蓄意攻击和无意识的失误,它有几个薄弱环节。


清单 3. getters 和 setters的值语义以及引用语义
												
														public class Widget {
    private Point location;

    // Version 1: No copying -- getter and setter implement reference 
    // semantics
    // This approach effectively assumes that we are transferring 
    // ownership of the Point from the caller to the Widget, but this 
    // assumption is rarely explicitly documented. 
    public void setLocation(Point p) {
        this.location = p;
    }

    public Point getLocation() {
        return location;
    }

    // Version 2: Defensive copy on setter, implementing value 
    // semantics for the setter
    // This approach effectively assumes that callers of 
    // getLocation will respect the assumption that the Widget 
    // owns the Point, but this assumption is rarely documented.
    public void setLocation(Point p) {
        this.location = new Point(p.x, p.y);
    }

    public Point getLocation() {
        return location;
    }

    // Version 3: Defensive copy on getter and setter, implementing 
    // true value semantics, at a performance cost
    public void setLocation(Point p) {
        this.location = new Point(p.x, p.y);
    }

    public Point getLocation() {
        return (Point) location.clone();
    }
}

												
										

现在来考虑 setLocation 看起来是无意的使用 :

												
														    Widget w1, w2;
    . . . 
    Point p = new Point();
    p.x = p.y = 1;
    w1.setLocation(p);
    
    p.x = p.y = 2;
    w2.setLocation(p);

												
										

或者是:

												
														    w2.setLocation(w1.getLocation());

												
										

在setLocation/getLocation存取器实现的版本 1 之下,可能看起来好像第一个Widget的 位置是 (1, 1) ,第二个Widget的位置是 (2, 2),而事实上,二者都是 (2, 2)。这可能对于调用者(因为第一个Widget意外地移动了)和Widget 类(因为它的位置改变了,而与Widget代码无关)来说都会产生混淆。在第二个例子中,您可能认为自己只是将Widget w2移动到 Widget w1当前所在的位置 ,但是实际上您这样做便规定了每次w1 移动时w2都跟随w1 。

防御性副本

setLocation 的版本 2 做得更好:它创建了传递给它的参数的一个副本,以确保不存在可以意外改变其状态的 Point的别名。但是它也并非无可挑剔,因为下面的代码也将具有一个很可能不希望出现的效果,即Widget在不知情的情况下被移动了:

												
														    Point p = w1.getLocation();
    . . .
    p.x = 0;

												
										

getLocation 和 setLocation 的版本 3 对于别名引用的恶意或无意使用是完全安全的。这一安全是以一些性能为代价换来的:每次调用一个 getter 或 setter 都会创建一个新对象。

getLocation 和 setLocation 的不同版本具有不同的语义,通常这些语义被称作值语义(版本 1)和引用语义(版本 3)。不幸的是,通常没有说明实现者应该使用的是哪种语义。结果,这个类的使用者并不清楚这一点,从而作出了更差的假设(即选择了不是最合适的语义)。

getLocation 和 setLocation 的版本 3 所使用的技术叫做防御性复制( defensive copying),尽管存在着明显的性能上的代价,您也应该养成这样的习惯,即几乎每次返回和存储对可变对象或数组的引用时都使用这一技术,尤其是在您编写一个通用的可能被不是您自己编写的代码调用(事实上这很常见)的工具时更是如此。有别名的可变对象被意外修改的情况会以许多微妙且令人惊奇的方式突然出现,并且调试起来相当困难。

而且情况还会变得更坏。假设您是Widget类的一个使用者,您并不知道存取器具有值语义还是引用语义。 谨慎的做法是,在调用存取器方法时也使用防御性副本。所以,如果您想要将 w2 移动到 w1 的当前位置,您应该这样去做:

												
														    Point p = w1.getLocation();
    w2.setLocation(new Point(p.x, p.y));

												
										

如果 Widget 像在版本 2 或 3 中一样实现其存取器,那么我们将为每个调用创建两个临时对象 ――一个在 setLocation 调用的外面,一个在里面。

文档说明存取器语义

getLocation 和 setLocation 的版本 1 的真正问题不是它们易受混淆别名副作用的不良影响(确实是这样),而是它们的语义没有清楚的说明。如果存取器被清楚地说明为具有引用语义(而不是像通常那样被假设为值语义),那么调用者将更可能认识到,在它们调用setLocation时,它们是将Point对象的所有权转移给另一个实体,并且也不大可能仍然认为它们还拥有Point对象的所有权,因而还能够再次使用它。





回页首


利用不可改变性解决以上问题

如果一开始就使得Point 成为不可变的,那么这些与 Point 有关的问题早就迎刃而解了。不可变对象上没有副作用,并且缓存不可变对象的引用总是安全的,不会出现别名问题。如果 Point是不可变的,那么与setLocation 和 getLocation存取器的语义有关的所有问题都是非常确定的 。不可变属性的存取器将总是具有值引用,因而调用的任何一方都不需要防御性复制,这使得它们效率更高。

那么为什么不在一开始就使得Point 成为不可变的呢?这可能是出于性能上的原因,因为早期的 JVM具有不太有效的垃圾收集器。 那时,每当一个对象(甚至是鼠标)在屏幕上移动就创建一个新的Point的对象创建开销可能有些让人生畏,而创建防御性副本的开销则不在话下。

依后见之明,使Point成为可变的这个决定被证明对于程序清晰性和性能是昂贵的代价。Point类的可变性使得每一个接受Point作为参数或者要返回一个Point的方法背上了编写文档说明的沉重负担。也就是说,它得说明它是要改变Point,还是在返回之后保留对Point的一个引用。因为很少有类真正包含这样的文档,所以在调用一个没有用文档说明其调用语义或副作用行为的方法时,安全的策略是在传递它到任何这样的方法之前创建一份防御副本。

有讽刺意味的是,使 Point成为可变的这个决定所带来的性能优势被由于Point的可变性而需要进行的防御性复制给抵消了。由于缺乏清晰的文档说明(或者缺少信任),在方法调用的两边都需要创建防御副本 ――调用者需要这样做是因为它不知道被调用者是否会粗暴地改变 Point,而被调用者需要这样做是因为它不知道是否保留了对 Point 的引用。





回页首


一个现实的例子

下面是悬空别名问题的另一个例子,该例子非常类似于我最近在一个服务器应用中所看到的。 该应用在内部使用了发布-订阅式消息传递方式,以将事件和状态更新传达到服务器内的其他代理。这些代理可以订阅任何一个它们感兴趣的消息流。一旦发布之后,传递到其他代理的消息就可能在将来某个时候在一个不同的线程中被处理。

清单 4 显示了一个典型的消息传递事件(即发布拍卖系统中一个新的高投标通知)和产生该事件的代码。不幸的是,消息传递事件实现和调用者实现的交互合起来创建了一个悬空别名。通过简单地复制而不是克隆数组引用,消息和产生消息的类都保存了前一投标数组的主副本的一个引用。如果消息发布时的时间和消费时的时间有任何延迟,那么订阅者看到的 previous5Bids 数组的值将不同于消息发布时的时间,并且多个订阅者看到的前面投标的值可能会互不相同。在这个例子中,订阅者将看到当前投标的历史值和前面投标的更接近现在的值,从而形成了这样的错觉,认为前面投标比当前投标的值要高。不难设想这将如何引起问题――这还不算,当应用在很大的负载下时,这样一个问题则更是暴露无遗。 使得消息类不可变并在构造时克隆像数组这样的可变引用,就可以防止该问题。


清单 4. 发布-订阅式消息传递代码中的悬空数组别名
												
														public interface MessagingEvent { ... }

public class CurrentBidEvent implements MessagingEvent { 
  public final int currentBid;
  public final int[] previous5Bids;

  public CurrentBidEvent(int currentBid, int[] previousBids) {
    this.currentBid = currentBid;
    // Danger -- copying array reference instead of values
    this.previous5Bids = previous5Bids;
  }

  ...
}

  // Now, somewhere in the bid-processing code, we create a 
  // CurrentBidEvent and publish it.  
  public void newBid(int newBid) { 
    if (newBid > currentBid) { 
      for (int i=1; i<5; i++) 
        previous5Bids[i] = previous5Bids[i-1];
      previous5Bids[0] = currentBid;
      currentBid = newBid;

      messagingTopic.publish(new CurrentBidEvent(currentBid, previousBids));
    }
  }
}

												
										





回页首


可变对象的指导

如果您要创建一个可变类 M,那么您应该准备编写比 M 是不可变的情况下多得多的文档说明,以说明怎样处理 M 的引用。 首先,您必须选择以 M 为参数或返回 M 对象的方法是使用值语义还是引用语义,并准备在每一个在其接口内使用 M 的其他类中清晰地文档说明这一点 。如果接受或返回 M 对象的任何方法隐式地假设 M 的所有权被转移,那么您必须也文档说明这一点。您还要准备着接受在必要时创建防御副本的性能开销。

一个必须处理对象所有权问题的特殊情况是数组,因为数组不可以是不可变的。当传递一个数组引用到另一个类时,可能有创建防御副本的代价,除非您能确保其他类要么创建了它自己的副本,要么只在调用期间保存引用,否则您可能需要在传递数组之前创建副本。另外,您可以容易地结束这样一种情形,即调用的两边的类都隐式地假设它们拥有数组,只是这样会有不可预知的结果出现。

posted @ 2006-08-24 17:33 Binary 阅读(260) | 评论 (0)编辑 收藏

轻松使用线程: 不共享有时是最好的

ThreadLocal 类是悄悄地出现在 Java 平台版本 1.2 中的。虽然支持线程局部变量早就是许多线程工具(例如 Posix pthreads 工具)的一部分,但 Java Threads API 的最初设计却没有这项有用的功能。而且,最初的实现也相当低效。由于这些原因, ThreadLocal 极少受到关注,但对简化线程安全并发程序的开发来说,它却是很方便的。在 轻松使用线程的第 3 部分,Java 软件顾问 Brian Goetz 研究了 ThreadLocal 并提供了一些使用技巧。

参加 Brian 的 多线程 Java 编程讨论论坛以获得您工程中的线程和并发问题的帮助。

编写线程安全类是困难的。它不但要求仔细分析在什么条件可以对变量进行读写,而且要求仔细分析其它类能如何使用某个类。 有时,要在不影响类的功能、易用性或性能的情况下使类成为线程安全的是很困难的。有些类保留从一个方法调用到下一个方法调用的状态信息,要在实践中使这样的类成为线程安全的是困难的。

管理非线程安全类的使用比试图使类成为线程安全的要更容易些。非线程安全类通常可以安全地在多线程程序中使用,只要您能确保一个线程所用的类的实例不被其它线程使用。例如,JDBC Connection 类是非线程安全的 — 两个线程不能在小粒度级上安全地共享一个 Connection — 但如果每个线程都有它自己的 Connection ,那么多个线程就可以同时安全地进行数据库操作。

不使用 ThreadLocal 为每个线程维护一个单独的 JDBC 连接(或任何其它对象)当然是可能的;Thread API 给了我们把对象和线程联系起来所需的所有工具。而 ThreadLocal 则使我们能更容易地把线程和它的每线程(per-thread)数据成功地联系起来。

什么是线程局部变量(thread-local variable)?

线程局部变量高效地为每个使用它的线程提供单独的线程局部变量值的副本。每个线程只能看到与自己相联系的值,而不知道别的线程可能正在使用或修改它们自己的副本。一些编译器(例如 Microsoft Visual C++ 编译器或 IBM XL FORTRAN 编译器)用存储类别修饰符(像 staticvolatile )把对线程局部变量的支持集成到了其语言中。Java 编译器对线程局部变量不提供特别的语言支持;相反地,它用 ThreadLocal 类实现这些支持, 核心 Thread 类中有这个类的特别支持。

因为线程局部变量是通过一个类来实现的,而不是作为 Java 语言本身的一部分,所以 Java 语言线程局部变量的使用语法比内建线程局部变量语言的使用语法要笨拙一些。要创建一个线程局部变量,请实例化类 ThreadLocal 的一个对象。 ThreadLocal 类的行为与 java.lang.ref 中的各种 Reference 类的行为很相似; ThreadLocal 类充当存储或检索一个值时的间接句柄。清单 1 显示了 ThreadLocal 接口。


清单 1. ThreadLocal 接口
												
														public class ThreadLocal { 
  public Object get();
  public void set(Object newValue);
  public Object initialValue();
}

												
										

get() 访问器检索变量的当前线程的值; set() 访问器修改当前线程的值。 initialValue() 方法是可选的,如果线程未使用过某个变量,那么您可以用这个方法来设置这个变量的初始值;它允许延迟初始化。用一个示例实现来说明 ThreadLocal 的工作方式是最好的方法。清单 2 显示了 ThreadLocal 的一个实现方式。它不是一个特别好的实现(虽然它与最初实现非常相似),所以很可能性能不佳,但它清楚地说明了 ThreadLocal 的工作方式。


清单 2. ThreadLocal 的糟糕实现
												
														public class ThreadLocal { 
  private Map values = Collections.synchronizedMap(new HashMap());

  public Object get() {
    Thread curThread = Thread.currentThread();
    Object o = values.get(curThread);
    if (o == null && !values.containsKey(curThread)) {
      o = initialValue();
      values.put(curThread, o);
    }
    return o;
  }

  public void set(Object newValue) {
    values.put(Thread.currentThread(), newValue);
  }

  public Object initialValue() {
    return null;
  }
}

												
										

这个实现的性能不会很好,因为每个 get()set() 操作都需要 values 映射表上的同步,而且如果多个线程同时访问同一个 ThreadLocal ,那么将发生争用。此外,这个实现也是不切实际的,因为用 Thread 对象做 values 映射表中的关键字将导致无法在线程退出后对 Thread 进行垃圾回收,而且也无法对死线程的 ThreadLocal 的特定于线程的值进行垃圾回收。





回页首


用 ThreadLocal 实现每线程 Singleton

线程局部变量常被用来描绘有状态“单子”(Singleton) 或线程安全的共享对象,或者是通过把不安全的整个变量封装进 ThreadLocal ,或者是通过把对象的特定于线程的状态封装进 ThreadLocal 。例如,在与数据库有紧密联系的应用程序中,程序的很多方法可能都需要访问数据库。在系统的每个方法中都包含一个 Connection 作为参数是不方便的 — 用“单子”来访问连接可能是一个虽然更粗糙,但却方便得多的技术。然而,多个线程不能安全地共享一个 JDBC Connection 。如清单 3 所示,通过使用“单子”中的 ThreadLocal ,我们就能让我们的程序中的任何类容易地获取每线程 Connection 的一个引用。这样,我们可以认为 ThreadLocal 允许我们创建 每线程单子


清单 3. 把一个 JDBC 连接存储到一个每线程 Singleton 中
												
														public class ConnectionDispenser { 
  private static class ThreadLocalConnection extends ThreadLocal {
    public Object initialValue() {
      return DriverManager.getConnection(ConfigurationSingleton.getDbUrl());
    }
  }

  private ThreadLocalConnection conn = new ThreadLocalConnection();

  public static Connection getConnection() {
    return (Connection) conn.get();
  }
}

												
										

任何创建的花费比使用的花费相对昂贵些的有状态或非线程安全的对象,例如 JDBC Connection 或正则表达式匹配器,都是可以使用每线程单子(singleton)技术的好地方。当然,在类似这样的地方,您可以使用其它技术,例如用池,来安全地管理共享访问。然而,从可伸缩性角度看,即使是用池也存在一些潜在缺陷。因为池实现必须使用同步,以维护池数据结构的完整性,如果所有线程使用同一个池,那么在有很多线程频繁地对池进行访问的系统中,程序性能将因争用而降低。





回页首


用 ThreadLocal 简化调试日志纪录

其它适合使用 ThreadLocal 但用池却不能成为很好的替代技术的应用程序包括存储或累积每线程上下文信息以备稍后检索之用这样的应用程序。例如,假设您想创建一个用于管理多线程应用程序调试信息的工具。您可以用如清单 4 所示的 DebugLogger 类作为线程局部容器来累积调试信息。在一个工作单元的开头,您清空容器,而当一个错误出现时,您查询该容器以检索这个工作单元迄今为止生成的所有调试信息。


清单 4. 用 ThreadLocal 管理每线程调试日志
												
														public class DebugLogger {
  private static class ThreadLocalList extends ThreadLocal {
    public Object initialValue() {
      return new ArrayList();
    }

    public List getList() { 
      return (List) super.get(); 
    }
  }

  private ThreadLocalList list = new ThreadLocalList();
  private static String[] stringArray = new String[0];

  public void clear() {
    list.getList().clear();
  }

  public void put(String text) {
    list.getList().add(text);
  }

  public String[] get() {
    return list.getList().toArray(stringArray);
  }
}

												
										

在您的代码中,您可以调用 DebugLogger.put() 来保存您的程序正在做什么的信息,而且,稍后如果有必要(例如发生了一个错误),您能够容易地检索与某个特定线程相关的调试信息。 与简单地把所有信息转储到一个日志文件,然后努力找出哪个日志记录来自哪个线程(还要担心线程争用日志纪录对象)相比,这种技术简便得多,也有效得多。

ThreadLocal 在基于 servlet 的应用程序或工作单元是一个整体请求的任何多线程应用程序服务器中也是很有用的,因为在处理请求的整个过程中将要用到单个线程。您可以通过前面讲述的每线程单子技术用 ThreadLocal 变量来存储各种每请求(per-request)上下文信息。





回页首


ThreadLocal 的线程安全性稍差的堂兄弟,InheritableThreadLocal

ThreadLocal 类有一个亲戚,InheritableThreadLocal,它以相似的方式工作,但适用于种类完全不同的应用程序。创建一个线程时如果保存了所有 InheritableThreadLocal 对象的值,那么这些值也将自动传递给子线程。如果一个子线程调用 InheritableThreadLocalget() ,那么它将与它的父线程看到同一个对象。为保护线程安全性,您应该只对不可变对象(一旦创建,其状态就永远不会被改变的对象)使用 InheritableThreadLocal ,因为对象被多个线程共享。 InheritableThreadLocal 很合适用于把数据从父线程传到子线程,例如用户标识(user id)或事务标识(transaction id),但不能是有状态对象,例如 JDBC Connection





回页首


ThreadLocal 的性能

虽然线程局部变量早已赫赫有名并被包括 Posix pthreads 规范在内的很多线程框架支持,但最初的 Java 线程设计中却省略了它,只是在 Java 平台的版本 1.2 中才添加上去。在很多方面, ThreadLocal 仍在发展之中;在版本 1.3 中它被重写,版本 1.4 中又重写了一次,两次都专门是为了性能问题。

在 JDK 1.2 中, ThreadLocal 的实现方式与清单 2 中的方式非常相似,除了用同步 WeakHashMap 代替 HashMap 来存储 values 之外。(以一些额外的性能开销为代价,使用 WeakHashMap 解决了无法对 Thread 对象进行垃圾回收的问题。)不用说, ThreadLocal 的性能是相当差的。

Java 平台版本 1.3 提供的 ThreadLocal 版本已经尽量更好了;它不使用任何同步,从而不存在可伸缩性问题,而且它也不使用弱引用。相反地,人们通过给 Thread 添加一个实例变量(该变量用于保存当前线程的从线程局部变量到它的值的映射的 HashMap )来修改 Thread 类以支持 ThreadLocal 。因为检索或设置一个线程局部变量的过程不涉及对可能被另一个线程读写的数据的读写操作,所以您可以不用任何同步就实现 ThreadLocal.get()set() 。而且,因为每线程值的引用被存储在自已的 Thread 对象中,所以当对 Thread 进行垃圾回收时,也能对该 Thread 的每线程值进行垃圾回收。

不幸的是,即使有了这些改进,Java 1.3 中的 ThreadLocal 的性能仍然出奇地慢。据我的粗略测量,在双处理器 Linux 系统上的 Sun 1.3 JDK 中进行 ThreadLocal.get() 操作,所耗费的时间大约是无争用同步的两倍。性能这么差的原因是 Thread.currentThread() 方法的花费非常大,占了 ThreadLocal.get() 运行时间的三分之二还多。虽然有这些缺点,JDK 1.3 ThreadLocal.get() 仍然比争用同步快得多,所以如果在任何存在严重争用的地方(可能是有非常多的线程,或者同步块被频繁地执行,或者同步块很大), ThreadLocal 可能仍然要高效得多。

在 Java 平台的最新版本,即版本 1.4b2 中, ThreadLocalThread.currentThread() 的性能都有了很大提高。有了这些提高, ThreadLocal 应该比其它技术,如用池,更快。由于它比其它技术更简单,也更不易出错,人们最终将发现它是避免线程间出现不希望的交互的有效途径。





回页首


ThreadLocal 的好处

ThreadLocal 能带来很多好处。它常常是把有状态类描绘成线程安全的,或者封装非线程安全类以使它们能够在多线程环境中安全地使用的最容易的方式。使用 ThreadLocal 使我们可以绕过为实现线程安全而对何时需要同步进行判断的复杂过程,而且因为它不需要任何同步,所以也改善了可伸缩性。除简单之外,用 ThreadLocal 存储每线程单子或每线程上下文信息在归档方面还有一个颇有价值好处 — 通过使用 ThreadLocal ,存储在 ThreadLocal 中的对象都是 被线程共享的是清晰的,从而简化了判断一个类是否线程安全的工作。

posted @ 2006-08-24 17:32 Binary 阅读(254) | 评论 (0)编辑 收藏

Java 理论与实践: 非阻塞算法简介

Java™ 5.0 第一次让使用 Java 语言开发非阻塞算法成为可能,java.util.concurrent 包充分地利用了这个功能。非阻塞算法属于并发算法,它们可以安全地派生它们的线程,不通过锁定派生,而是通过低级的原子性的硬件原生形式 —— 例如比较和交换。非阻塞算法的设计与实现极为困难,但是它们能够提供更好的吞吐率,对生存问题(例如死锁和优先级反转)也能提供更好的防御。在这期的 Java 理论与实践 中,并发性大师 Brian Goetz 演示了几种比较简单的非阻塞算法的工作方式。

在不只一个线程访问一个互斥的变量时,所有线程都必须使用同步,否则就可能会发生一些非常糟糕的事情。Java 语言中主要的同步手段就是 synchronized 关键字(也称为内在锁),它强制实行互斥,确保执行 synchronized 块的线程的动作,能够被后来执行受相同锁保护的 synchronized 块的其他线程看到。在使用得当的时候,内在锁可以让程序做到线程安全,但是在使用锁定保护短的代码路径,而且线程频繁地争用锁的时候,锁定可能成为相当繁重的操作。

“流行的原子” 一文中,我们研究了原子变量,原子变量提供了原子性的读-写-修改操作,可以在不使用锁的情况下安全地更新共享变量。原子变量的内存语义与 volatile 变量类似,但是因为它们也可以被原子性地修改,所以可以把它们用作不使用锁的并发算法的基础。

非阻塞的计数器

清单 1 中的 Counter 是线程安全的,但是使用锁的需求带来的性能成本困扰了一些开发人员。但是锁是必需的,因为虽然增加看起来是单一操作,但实际是三个独立操作的简化:检索值,给值加 1,再写回值。(在 getValue 方法上也需要同步,以保证调用 getValue 的线程看到的是最新的值。虽然许多开发人员勉强地使自己相信忽略锁定需求是可以接受的,但忽略锁定需求并不是好策略。)

在多个线程同时请求同一个锁时,会有一个线程获胜并得到锁,而其他线程被阻塞。JVM 实现阻塞的方式通常是挂起阻塞的线程,过一会儿再重新调度它。由此造成的上下文切换相对于锁保护的少数几条指令来说,会造成相当大的延迟。


清单 1. 使用同步的线程安全的计数器
												
														public final class Counter {
    private long value = 0;

    public synchronized long getValue() {
        return value;
    }

    public synchronized long increment() {
        return ++value;
    }
}

												
										

清单 2 中的 NonblockingCounter 显示了一种最简单的非阻塞算法:使用 AtomicIntegercompareAndSet() (CAS)方法的计数器。compareAndSet() 方法规定 “将这个变量更新为新值,但是如果从我上次看到这个变量之后其他线程修改了它的值,那么更新就失败”(请参阅 “流行的原子” 获得关于原子变量以及 “比较和设置” 的更多解释。)


清单 2. 使用 CAS 的非阻塞算法
												
														public class NonblockingCounter {
    private AtomicInteger value;

    public int getValue() {
        return value.get();
    }

    public int increment() {
        int v;
        do {
            v = value.get();
        while (!value.compareAndSet(v, v + 1));
        return v + 1;
    }
}

												
										

原子变量类之所以被称为原子的,是因为它们提供了对数字和对象引用的细粒度的原子更新,但是在作为非阻塞算法的基本构造块的意义上,它们也是原子的。非阻塞算法作为科研的主题,已经有 20 多年了,但是直到 Java 5.0 出现,在 Java 语言中才成为可能。

现代的处理器提供了特殊的指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。(如果要做的只是递增计数器,那么 AtomicInteger 提供了进行递增的方法,但是这些方法基于 compareAndSet(),例如 NonblockingCounter.increment())。

非阻塞版本相对于基于锁的版本有几个性能优势。首先,它用硬件的原生形态代替 JVM 的锁定代码路径,从而在更细的粒度层次上(独立的内存位置)进行同步,失败的线程也可以立即重试,而不会被挂起后重新调度。更细的粒度降低了争用的机会,不用重新调度就能重试的能力也降低了争用的成本。即使有少量失败的 CAS 操作,这种方法仍然会比由于锁争用造成的重新调度快得多。

NonblockingCounter 这个示例可能简单了些,但是它演示了所有非阻塞算法的一个基本特征 —— 有些算法步骤的执行是要冒险的,因为知道如果 CAS 不成功可能不得不重做。非阻塞算法通常叫作乐观算法,因为它们继续操作的假设是不会有干扰。如果发现干扰,就会回退并重试。在计数器的示例中,冒险的步骤是递增 —— 它检索旧值并在旧值上加一,希望在计算更新期间值不会变化。如果它的希望落空,就会再次检索值,并重做递增计算。





回页首


非阻塞堆栈

非阻塞算法稍微复杂一些的示例是清单 3 中的 ConcurrentStackConcurrentStack 中的 push()pop() 操作在结构上与 NonblockingCounter 上相似,只是做的工作有些冒险,希望在 “提交” 工作的时候,底层假设没有失效。push() 方法观察当前最顶的节点,构建一个新节点放在堆栈上,然后,如果最顶端的节点在初始观察之后没有变化,那么就安装新节点。如果 CAS 失败,意味着另一个线程已经修改了堆栈,那么过程就会重新开始。


清单 3. 使用 Treiber 算法的非阻塞堆栈
												
														public class ConcurrentStack<E> {
    AtomicReference<Node<E>> head = new AtomicReference<Node<E>>();

    public void push(E item) {
        Node<E> newHead = new Node<E>(item);
        Node<E> oldHead;
        do {
            oldHead = head.get();
            newHead.next = oldHead;
        } while (!head.compareAndSet(oldHead, newHead));
    }

    public E pop() {
        Node<E> oldHead;
        Node<E> newHead;
        do {
            oldHead = head.get();
            if (oldHead == null) 
                return null;
            newHead = oldHead.next;
        } while (!head.compareAndSet(oldHead,newHead));
        return oldHead.item;
    }

    static class Node<E> {
        final E item;
        Node<E> next;

        public Node(E item) { this.item = item; }
    }
}

												
										

性能考虑

在轻度到中度的争用情况下,非阻塞算法的性能会超越阻塞算法,因为 CAS 的多数时间都在第一次尝试时就成功,而发生争用时的开销也不涉及线程挂起和上下文切换,只多了几个循环迭代。没有争用的 CAS 要比没有争用的锁便宜得多(这句话肯定是真的,因为没有争用的锁涉及 CAS 加上额外的处理),而争用的 CAS 比争用的锁获取涉及更短的延迟。

在高度争用的情况下(即有多个线程不断争用一个内存位置的时候),基于锁的算法开始提供比非阻塞算法更好的吞吐率,因为当线程阻塞时,它就会停止争用,耐心地等候轮到自己,从而避免了进一步争用。但是,这么高的争用程度并不常见,因为多数时候,线程会把线程本地的计算与争用共享数据的操作分开,从而给其他线程使用共享数据的机会。(这么高的争用程度也表明需要重新检查算法,朝着更少共享数据的方向努力。)“流行的原子” 中的图在这方面就有点儿让人困惑,因为被测量的程序中发生的争用极其密集,看起来即使对数量很少的线程,锁定也是更好的解决方案。





回页首


非阻塞的链表

目前为止的示例(计数器和堆栈)都是非常简单的非阻塞算法,一旦掌握了在循环中使用 CAS,就可以容易地模仿它们。对于更复杂的数据结构,非阻塞算法要比这些简单示例复杂得多,因为修改链表、树或哈希表可能涉及对多个指针的更新。CAS 支持对单一指针的原子性条件更新,但是不支持两个以上的指针。所以,要构建一个非阻塞的链表、树或哈希表,需要找到一种方式,可以用 CAS 更新多个指针,同时不会让数据结构处于不一致的状态。

在链表的尾部插入元素,通常涉及对两个指针的更新:“尾” 指针总是指向列表中的最后一个元素,“下一个” 指针从过去的最后一个元素指向新插入的元素。因为需要更新两个指针,所以需要两个 CAS。在独立的 CAS 中更新两个指针带来了两个需要考虑的潜在问题:如果第一个 CAS 成功,而第二个 CAS 失败,会发生什么?如果其他线程在第一个和第二个 CAS 之间企图访问链表,会发生什么?

对于非复杂数据结构,构建非阻塞算法的 “技巧” 是确保数据结构总处于一致的状态(甚至包括在线程开始修改数据结构和它完成修改之间),还要确保其他线程不仅能够判断出第一个线程已经完成了更新还是处在更新的中途,还能够判断出如果第一个线程走向 AWOL,完成更新还需要什么操作。如果线程发现了处在更新中途的数据结构,它就可以 “帮助” 正在执行更新的线程完成更新,然后再进行自己的操作。当第一个线程回来试图完成自己的更新时,会发现不再需要了,返回即可,因为 CAS 会检测到帮助线程的干预(在这种情况下,是建设性的干预)。

这种 “帮助邻居” 的要求,对于让数据结构免受单个线程失败的影响,是必需的。如果线程发现数据结构正处在被其他线程更新的中途,然后就等候其他线程完成更新,那么如果其他线程在操作中途失败,这个线程就可能永远等候下去。即使不出现故障,这种方式也会提供糟糕的性能,因为新到达的线程必须放弃处理器,导致上下文切换,或者等到自己的时间片过期(而这更糟)。

清单 4 的 LinkedQueue 显示了 Michael-Scott 非阻塞队列算法的插入操作,它是由 ConcurrentLinkedQueue 实现的:


清单 4. Michael-Scott 非阻塞队列算法中的插入
												
														public class LinkedQueue <E> {
    private static class Node <E> {
        final E item;
        final AtomicReference<Node<E>> next;

        Node(E item, Node<E> next) {
            this.item = item;
            this.next = new AtomicReference<Node<E>>(next);
        }
    }

    private AtomicReference<Node<E>> head
        = new AtomicReference<Node<E>>(new Node<E>(null, null));
    private AtomicReference<Node<E>> tail = head;

    public boolean put(E item) {
        Node<E> newNode = new Node<E>(item, null);
        while (true) {
            Node<E> curTail = tail.get();
            Node<E> residue = curTail.next.get();
            if (curTail == tail.get()) {
                if (residue == null) /* A */ {
                    if (curTail.next.compareAndSet(null, newNode)) /* C */ {
                        tail.compareAndSet(curTail, newNode) /* D */ ;
                        return true;
                    }
                } else {
                    tail.compareAndSet(curTail, residue) /* B */;
                }
            }
        }
    }
}

												
										

像许多队列算法一样,空队列只包含一个假节点。头指针总是指向假节点;尾指针总指向最后一个节点或倒数第二个节点。图 1 演示了正常情况下有两个元素的队列:


图 1. 有两个元素,处在静止状态的队列

清单 4 所示,插入一个元素涉及两个指针更新,这两个更新都是通过 CAS 进行的:从队列当前的最后节点(C)链接到新节点,并把尾指针移动到新的最后一个节点(D)。如果第一步失败,那么队列的状态不变,插入线程会继续重试,直到成功。一旦操作成功,插入被当成生效,其他线程就可以看到修改。还需要把尾指针移动到新节点的位置上,但是这项工作可以看成是 “清理工作”,因为任何处在这种情况下的线程都可以判断出是否需要这种清理,也知道如何进行清理。

队列总是处于两种状态之一:正常状态(或称静止状态,图 1图 3)或中间状态(图 2)。在插入操作之前和第二个 CAS(D)成功之后,队列处在静止状态;在第一个 CAS(C)成功之后,队列处在中间状态。在静止状态时,尾指针指向的链接节点的 next 字段总为 null,而在中间状态时,这个字段为非 null。任何线程通过比较 tail.next 是否为 null,就可以判断出队列的状态,这是让线程可以帮助其他线程 “完成” 操作的关键。


图 2. 处在插入中间状态的队列,在新元素插入之后,尾指针更新之前

插入操作在插入新元素(A)之前,先检查队列是否处在中间状态,如 清单 4 所示。如果是在中间状态,那么肯定有其他线程已经处在元素插入的中途,在步骤(C)和(D)之间。不必等候其他线程完成,当前线程就可以 “帮助” 它完成操作,把尾指针向前移动(B)。如果有必要,它还会继续检查尾指针并向前移动指针,直到队列处于静止状态,这时它就可以开始自己的插入了。

第一个 CAS(C)可能因为两个线程竞争访问队列当前的最后一个元素而失败;在这种情况下,没有发生修改,失去 CAS 的线程会重新装入尾指针并再次尝试。如果第二个 CAS(D)失败,插入线程不需要重试 —— 因为其他线程已经在步骤(B)中替它完成了这个操作!


图 3. 在尾指针更新后,队列重新处在静止状态

幕后的非阻塞算法

如果深入 JVM 和操作系统,会发现非阻塞算法无处不在。垃圾收集器使用非阻塞算法加快并发和平行的垃圾搜集;调度器使用非阻塞算法有效地调度线程和进程,实现内在锁。在 Mustang(Java 6.0)中,基于锁的 SynchronousQueue 算法被新的非阻塞版本代替。很少有开发人员会直接使用 SynchronousQueue,但是通过 Executors.newCachedThreadPool() 工厂构建的线程池用它作为工作队列。比较缓存线程池性能的对比测试显示,新的非阻塞同步队列实现提供了几乎是当前实现 3 倍的速度。在 Mustang 的后续版本(代码名称为 Dolphin)中,已经规划了进一步的改进。





回页首


结束语

非阻塞算法要比基于锁的算法复杂得多。开发非阻塞算法是相当专业的训练,而且要证明算法的正确也极为困难。但是在 Java 版本之间并发性能上的众多改进来自对非阻塞算法的采用,而且随着并发性能变得越来越重要,可以预见在 Java 平台的未来发行版中,会使用更多的非阻塞算法。

posted @ 2006-08-24 17:31 Binary 阅读(303) | 评论 (0)编辑 收藏

Java 理论和实践: 了解泛型

JDK 5.0 中增加的泛型类型,是 Java 语言中类型安全的一次重要改进。但是,对于初次使用泛型类型的用户来说,泛型的某些方面看起来可能不容易明白,甚至非常奇怪。在本月的“Java 理论和实践”中,Brian Goetz 分析了束缚第一次使用泛型的用户的常见陷阱。您可以通过讨论论坛与作者和其他读者分享您对本文的看法。(也可以单击本文顶端或底端的讨论来访问这个论坛。)

表面上看起来,无论语法还是应用的环境(比如容器类),泛型类型(或者泛型)都类似于 C++ 中的模板。但是这种相似性仅限于表面,Java 语言中的泛型基本上完全在编译器中实现,由编译器执行类型检查和类型推断,然后生成普通的非泛型的字节码。这种实现技术称为擦除(erasure)(编译器使用泛型类型信息保证类型安全,然后在生成字节码之前将其清除),这项技术有一些奇怪,并且有时会带来一些令人迷惑的后果。虽然范型是 Java 类走向类型安全的一大步,但是在学习使用泛型的过程中几乎肯定会遇到头痛(有时候让人无法忍受)的问题。

注意:本文假设您对 JDK 5.0 中的范型有基本的了解。

泛型不是协变的

虽然将集合看作是数组的抽象会有所帮助,但是数组还有一些集合不具备的特殊性质。Java 语言中的数组是协变的(covariant),也就是说,如果 Integer 扩展了 Number(事实也是如此),那么不仅 IntegerNumber,而且 Integer[] 也是 Number[],在要求 Number[] 的地方完全可以传递或者赋予 Integer[]。(更正式地说,如果 NumberInteger 的超类型,那么 Number[] 也是 Integer[] 的超类型)。您也许认为这一原理同样适用于泛型类型 —— List<Number>List<Integer> 的超类型,那么可以在需要 List<Number> 的地方传递 List<Integer>。不幸的是,情况并非如此。

不允许这样做有一个很充分的理由:这样做将破坏要提供的类型安全泛型。如果能够将 List<Integer> 赋给 List<Number>。那么下面的代码就允许将非 Integer 的内容放入 List<Integer>

												
														List<Integer> li = new ArrayList<Integer>();
List<Number> ln = li; // illegal
ln.add(new Float(3.1415));

												
										

因为 lnList<Number>,所以向其添加 Float 似乎是完全合法的。但是如果 lnli 的别名,那么这就破坏了蕴含在 li 定义中的类型安全承诺 —— 它是一个整数列表,这就是泛型类型不能协变的原因。

其他的协变问题

数组能够协变而泛型不能协变的另一个后果是,不能实例化泛型类型的数组(new List<String>[3] 是不合法的),除非类型参数是一个未绑定的通配符(new List<?>[3] 是合法的)。让我们看看如果允许声明泛型类型数组会造成什么后果:

												
														List<String>[] lsa = new List<String>[10]; // illegal
Object[] oa = lsa;  // OK because List<String> is a subtype of Object
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[0] = li; 
String s = lsa[0].get(0); 

												
										

最后一行将抛出 ClassCastException,因为这样将把 List<Integer> 填入本应是 List<String> 的位置。因为数组协变会破坏泛型的类型安全,所以不允许实例化泛型类型的数组(除非类型参数是未绑定的通配符,比如 List<?>)。





回页首


构造延迟

因为可以擦除功能,所以 List<Integer>List<String> 是同一个类,编译器在编译 List<V> 时只生成一个类(和 C++ 不同)。因此,在编译 List<V> 类时,编译器不知道 V 所表示的类型,所以它就不能像知道类所表示的具体类型那样处理 List<V> 类定义中的类型参数(List<V> 中的 V)。

因为运行时不能区分 List<String>List<Integer>(运行时都是 List),用泛型类型参数标识类型的变量的构造就成了问题。运行时缺乏类型信息,这给泛型容器类和希望创建保护性副本的泛型类提出了难题。

比如泛型类 Foo

												
														class Foo<T> { 
  public void doSomething(T param) { ... }
}

												
										

假设 doSomething() 方法希望复制输入的 param 参数,会怎么样呢?没有多少选择。您可能希望按以下方式实现 doSomething()

												
														public void doSomething(T param) { 
  T copy = new T(param);  // illegal
}

												
										

但是您不能使用类型参数访问构造函数,因为在编译的时候还不知道要构造什么类,因此也就不知道使用什么构造函数。使用泛型不能表达“T 必须拥有一个拷贝构造函数(copy constructor)”(甚至一个无参数的构造函数)这类约束,因此不能使用泛型类型参数所表示的类的构造函数。

clone() 怎么样呢?假设在 Foo 的定义中,T 扩展了 Cloneable

												
														class Foo<T extends Cloneable> { 
  public void doSomething(T param) {
    T copy = (T) param.clone();  // illegal 
  }
}

												
										

不幸的是,仍然不能调用 param.clone()。为什么呢?因为 clone()Object 中是保护访问的,调用 clone() 必须通过将 clone() 改写公共访问的类引用来完成。但是重新声明 clone() 为 public 并不知道 T,因此克隆也无济于事。

构造通配符引用

因此,不能复制在编译时根本不知道是什么类的类型引用。那么使用通配符类型怎么样?假设要创建类型为 Set<?> 的参数的保护性副本。您知道 Set 有一个拷贝构造函数。而且别人可能曾经告诉过您,如果不知道要设置的内容的类型,最好使用 Set<?> 代替原始类型的 Set,因为这种方法引起的未检查类型转换警告更少。于是,可以试着这样写:

												
														class Foo {
  public void doSomething(Set<?> set) {
    Set<?> copy = new HashSet<?>(set);  // illegal
  }
}

												
										

不幸的是,您不能用通配符类型的参数调用泛型构造函数,即使知道存在这样的构造函数也不行。不过您可以这样做:

												
														class Foo {
  public void doSomething(Set<?> set) {
    Set<?> copy = new HashSet<Object>(set);  
  }
}

												
										

这种构造不那么直观,但它是类型安全的,而且可以像 new HashSet<?>(set) 那样工作。

构造数组

如何实现 ArrayList<V>?假设类 ArrayList 管理一个 V 数组,您可能希望用 ArrayList<V> 的构造函数创建一个 V 数组:

												
														class ArrayList<V> {
  private V[] backingArray;
  public ArrayList() {
    backingArray = new V[DEFAULT_SIZE]; // illegal
  }
}

												
										

但是这段代码不能工作 —— 不能实例化用类型参数表示的类型数组。编译器不知道 V 到底表示什么类型,因此不能实例化 V 数组。

Collections 类通过一种别扭的方法绕过了这个问题,在 Collections 类编译时会产生类型未检查转换的警告。ArrayList 具体实现的构造函数如下:

												
														class ArrayList<V> {
  private V[] backingArray;
  public ArrayList() {
    backingArray = (V[]) new Object[DEFAULT_SIZE]; 
  }
}

												
										

为何这些代码在访问 backingArray 时没有产生 ArrayStoreException 呢?无论如何,都不能将 Object 数组赋给 String 数组。因为泛型是通过擦除实现的,backingArray 的类型实际上就是 Object[],因为 Object 代替了 V。这意味着:实际上这个类期望 backingArray 是一个 Object 数组,但是编译器要进行额外的类型检查,以确保它包含 V 类型的对象。所以这种方法很奏效,但是非常别扭,因此不值得效仿(甚至连泛型 Collections 框架的作者都这么说,请参阅参考资料)。

还有一种方法就是声明 backingArrayObject 数组,并在使用它的各个地方强制将它转化为 V[]。仍然会看到类型未检查转换警告(与上一种方法一样),但是它使一些未明确的假设更清楚了(比如 backingArray 不应逃避 ArrayList 的实现)。

其他方法

最好的办法是向构造函数传递类文字(Foo.class),这样,该实现就能在运行时知道 T 的值。不采用这种方法的原因在于向后兼容性 —— 新的泛型集合类不能与 Collections 框架以前的版本兼容。

下面的代码中 ArrayList 采用了以下方法:

												
														public class ArrayList<V> implements List<V> {
  private V[] backingArray;
  private Class<V> elementType;

  public ArrayList(Class<V> elementType) {
    this.elementType = elementType;
    backingArray = (V[]) Array.newInstance(elementType, DEFAULT_LENGTH);
  }
}

												
										

但是等一等!仍然有不妥的地方,调用 Array.newInstance() 时会引起未经检查的类型转换。为什么呢?同样是由于向后兼容性。Array.newInstance() 的签名是:

												
														public static Object newInstance(Class<?> componentType, int length)

												
										

而不是类型安全的:

												
														public static<T> T[] newInstance(Class<T> componentType, int length)

												
										

为何 Array 用这种方式进行泛化呢?同样是为了保持向后兼容。要创建基本类型的数组,如 int[],可以使用适当的包装器类中的 TYPE 字段调用 Array.newInstance()(对于 int,可以传递 Integer.TYPE 作为类文字)。用 Class<T> 参数而不是 Class<?> 泛化 Array.newInstance(),对于引用类型有更好的类型安全,但是就不能使用 Array.newInstance() 创建基本类型数组的实例了。也许将来会为引用类型提供新的 newInstance() 版本,这样就两者兼顾了。

在这里可以看到一种模式 —— 与泛型有关的很多问题或者折衷并非来自泛型本身,而是保持和已有代码兼容的要求带来的副作用。





回页首


泛化已有的类

在转化现有的库类来使用泛型方面没有多少技巧,但与平常的情况相同,向后兼容性不会凭空而来。我已经讨论了两个例子,其中向后兼容性限制了类库的泛化。

另一种不同的泛化方法可能不存在向后兼容问题,这就是 Collections.toArray(Object[])。传入 toArray() 的数组有两个目的 —— 如果集合足够小,那么可以将其内容直接放在提供的数组中。否则,利用反射(reflection)创建相同类型的新数组来接受结果。如果从头开始重写 Collections 框架,那么很可能传递给 Collections.toArray() 的参数不是一个数组,而是一个类文字:

												
														interface Collection<E> { 
  public T[] toArray(Class<T super E> elementClass);
}

												
										

因为 Collections 框架作为良好类设计的例子被广泛效仿,但是它的设计受到向后兼容性约束,所以这些地方值得您注意,不要盲目效仿。

首先,常常被混淆的泛型 Collections API 的一个重要方面是 containsAll()removeAll()retainAll() 的签名。您可能认为 remove()removeAll() 的签名应该是:

												
														interface Collection<E> { 
  public boolean remove(E e);  // not really
  public void removeAll(Collection<? extends E> c);  // not really
}

												
										

但实际上却是:

												
														interface Collection<E> { 
  public boolean remove(Object o);  
  public void removeAll(Collection<?> c);
}

												
										

为什么呢?答案同样是因为向后兼容性。x.remove(o) 的接口表明“如果 o 包含在 x 中,则删除它,否则什么也不做。”如果 x 是一个泛型集合,那么 o 不一定与 x 的类型参数兼容。如果 removeAll() 被泛化为只有类型兼容时才能调用(Collection<? extends E>),那么在泛化之前,合法的代码序列就会变得不合法,比如:

												
														// a collection of Integers
Collection c = new HashSet();
// a collection of Objects
Collection r = new HashSet();
c.removeAll(r);

												
										

如果上述片段用直观的方法泛化(将 c 设为 Collection<Integer>r 设为 Collection<Object>),如果 removeAll() 的签名要求其参数为 Collection<? extends E> 而不是 no-op,那么就无法编译上面的代码。泛型类库的一个主要目标就是不打破或者改变已有代码的语义,因此,必须用比从头重新设计泛型所使用类型约束更弱的类型约束来定义 remove()removeAll()retainAll()containsAll()

在泛型之前设计的类可能阻碍了“显然的”泛型化方法。这种情况下就要像上例这样进行折衷,但是如果从头设计新的泛型类,理解 Java 类库中的哪些东西是向后兼容的结果很有意义,这样可以避免不适当的模仿。





回页首


擦除的实现

因为泛型基本上都是在 Java 编译器中而不是运行库中实现的,所以在生成字节码的时候,差不多所有关于泛型类型的类型信息都被“擦掉”了。换句话说,编译器生成的代码与您手工编写的不用泛型、检查程序的类型安全后进行强制类型转换所得到的代码基本相同。与 C++ 不同,List<Integer>List<String> 是同一个类(虽然是不同的类型但都是 List<?> 的子类型,与以前的版本相比,在 JDK 5.0 中这是一个更重要的区别)。

擦除意味着一个类不能同时实现 Comparable<String>Comparable<Number>,因为事实上两者都在同一个接口中,指定同一个 compareTo() 方法。声明 DecimalString 类以便与 StringNumber 比较似乎是明智的,但对于 Java 编译器来说,这相当于对同一个方法进行了两次声明:

												
														public class DecimalString implements Comparable<Number>, Comparable<String> { ... } // nope

												
										

擦除的另一个后果是,对泛型类型参数是用强制类型转换或者 instanceof 毫无意义。下面的代码完全不会改善代码的类型安全性:

												
														public <T> T naiveCast(T t, Object o) { return (T) o; }

												
										

编译器仅仅发出一个类型未检查转换警告,因为它不知道这种转换是否安全。naiveCast() 方法实际上根本不作任何转换,T 直接被替换为 Object,与期望的相反,传入的对象被强制转换为 Object

擦除也是造成上述构造问题的原因,即不能创建泛型类型的对象,因为编译器不知道要调用什么构造函数。如果泛型类需要构造用泛型类型参数来指定类型的对象,那么构造函数应该接受类文字(Foo.class)并将它们保存起来,以便通过反射创建实例。

posted @ 2006-08-24 17:31 Binary 阅读(245) | 评论 (0)编辑 收藏

一个线程池的实现

设计目标
Ø    提供一个线程池的组件,具有良好的伸缩性,当线程够用时,销毁不用线程,当线程不够用时,自动增加线程数量;
Ø    提供一个工作任务接口和工作队列,实际所需要的任务都必须实现这个工作任务接口,然后放入工作队列中;
Ø    线程池中的线程从工作队列中,自动取得工作任务,执行任务。
主要控制类和功能接口设计
线程池管理器 ThreadPoolManager 的功能:
Ø    管理线程池中的各个属性变量
ü    最大工作线程数
ü    最小工作线程数
ü    激活的工作线程总数
ü    睡眠的工作线程总数
ü    工作线程总数 (即:激活的工作线程总数+睡眠的工作线程总数)
Ø    创建工作线程
Ø    销毁工作线程
Ø    启动处于睡眠的工作线程
Ø    睡眠处于激活的工作线程
Ø    缩任务:当工作线程总数小于或等于最小工作线程数时,销毁多余的睡眠的工作线程,使得现有工作线程总数等于最小工作任务总数
Ø    伸任务:当任务队列任务总数大于工作线程数时,增加工作线程总数至最大工作线程数
Ø    提供线程池启动接口
Ø    提供线程池销毁接口
工作线程 WorkThread  的功能:
Ø    从工作队列取得工作任务
Ø    执行工作任务接口中的指定任务
工作任务接口 ITask   的功能:
Ø    提供指定任务动作
工作队列 IWorkQueue  的功能:
Ø    提供获取任务接口,并删除工作队列中的任务;
Ø    提供加入任务接口;
Ø    提供删除任务接口;
Ø    提供取得任务总数接口;
Ø    提供自动填任务接口;(当任务总数少于或等于默认总数的25%时,自动装填)
Ø    提供删除所有任务接口;


Code


ThreadPoolManager:
=====================================
CODE:
package test.thread.pool1;
import java.util.ArrayList;
import java.util.List;
import test.thread.pool1.impl.MyWorkQueue;

/**
 * <p>Title: 线程池管理器</p>
 * <p>Description: </p>
 * <p>Copyright: Copyright (c) 2005</p>
 * <p>Company: </p>
 * @author not attributable
 * @version 1.0
 */

public class ThreadPoolManager {
  /*最大线程数*/
  private int threads_max_num;

  /*最小线程数*/
  private int threads_min_num;
  
  /* 线程池线程增长步长 */
  private int threads_increase_step = 5;

  /* 任务工作队列 */
  private IWorkQueue queue;
  
  /* 线程池监视狗 */
  private PoolWatchDog poolWatchDog ;
  
  /* 队列线程 */
  private Thread queueThread ;
  
  /* 线程池 封装所有工作线程的数据结构 */
  private List pool = new ArrayList();
  
  /* 线程池中 封装所有钝化后的数据结构*/
  private List passivePool = new ArrayList();
  
  /* 空闲60秒 */
  private static final long IDLE_TIMEOUT = 60000L;
  
  /* 关闭连接池标志位 */
  private boolean close = false;
  
  /**
   * 线程池管理器
   * @param queue 任务队列
   * @param threads_min_num 工作线程最小数
   * @param threads_max_num 工作线程最大数
   */
  public ThreadPoolManager(int threads_max_num
                          ,int threads_min_num
                          ,IWorkQueue queue){
    this.threads_max_num = threads_max_num;
    this.threads_min_num = threads_min_num;
    this.queue = queue;    
  }

  /**
   * 线程池启动
   */
  public void startPool(){
    System.out.println("=== startPool..........");
    poolWatchDog = new PoolWatchDog("PoolWatchDog");
    poolWatchDog.setDaemon(true);
    poolWatchDog.start();
    System.out.println("=== startPool..........over");
  }

  /**
   * 线程池销毁接口
   */
  public void destoryPool(){
    System.out.println("==========================DestoryPool starting ...");
    this.close = true;
    int pool_size = this.pool.size();
    
    //中断队列线程
    System.out.println("===Interrupt queue thread ... ");
    queueThread.interrupt();
    queueThread = null;
    
    System.out.println("===Interrupt thread pool ... ");
    Thread pool_thread = null;
    for(int i=0; i<pool_size; i++){
      pool_thread = (Thread)pool.get(i);
      if(pool_thread !=null 
      && pool_thread.isAlive() 
      && !pool_thread.isInterrupted()){
        pool_thread.interrupt();
        System.out.println("Stop pool_thread:"
                          +pool_thread.getName()+"[interrupt] "
                          +pool_thread.isInterrupted());
      }
    }//end for
    
    if(pool != null){
      pool.clear();
    }
    if(passivePool != null){
      pool.clear();
    }
    
    try{
      System.out.println("=== poolWatchDog.join() starting ...");
      poolWatchDog.join();
      System.out.println("=== poolWatchDog.join() is over ...");
    }
    catch(Throwable ex){
      System.out.println("###poolWatchDog ... join method throw a exception ... "
                          +ex.toString());
    }
    
    poolWatchDog =null;
    System.out.println("==============================DestoryPool is over ...");    
  }
  
  
  public static void main(String[] args) throws Exception{
    ThreadPoolManager threadPoolManager1 = new ThreadPoolManager(10,5,new MyWorkQueue(50,30000));
    
    threadPoolManager1.startPool();
    Thread.sleep(60000);
    threadPoolManager1.destoryPool();
  }
  
  /**
   * 线程池监视狗
   */
  private class PoolWatchDog extends Thread{
    public PoolWatchDog(String name){
      super(name);
    }
  
    public void run(){
      Thread workThread = null;
      Runnable run = null;
      
      //开启任务队列线程,获取数据--------
      System.out.println("===QueueThread starting ... ... ");
      queueThread = new Thread(new QueueThread(),"QueueThread");
      queueThread.start();
      
      System.out.println("===Initial thread Pool ... ...");
      //初始化线程池的最小线程数,并放入池中
      for(int i=0; i<threads_min_num; i++){
        run = new WorkThread();
        workThread = new Thread(run,"WorkThread_"+System.currentTimeMillis()+i);
        workThread.start();
        if(i == threads_min_num -1){
          workThread = null;
          run = null;
        }
      }
      System.out.println("===Initial thread Pool..........over ,and get pool's size:"+pool.size());

      //线程池线程动态增加线程算法--------------
      while(!close){
      
        //等待5秒钟,等上述线程都启动----------
        synchronized(this){          
          try{
            System.out.println("===Wait the [last time] threads starting ....");
            this.wait(15000);
          }
          catch(Throwable ex){
            System.out.println("###PoolWatchDog invoking is failure ... "+ex);
          }
        }//end synchronized
          
        //开始增加线程-----------------------spread动作
        int queue_size = queue.getTaskSize();
        int temp_size = (queue_size - threads_min_num);
        
        if((temp_size > 0) && (temp_size/threads_increase_step > 2) ){
          System.out.println("================Spread thread pool starting ....");
          for(int i=0; i<threads_increase_step && (pool.size() < threads_max_num); i++){
            System.out.println("=== Spread thread num : "+i);
            run = new WorkThread();
            workThread = new Thread(run,"WorkThread_"+System.currentTimeMillis()+i);
            workThread.start();
          }//end for
          
          workThread = null;
          run = null;    
          System.out.println("===Spread thread pool is over .... and pool size:"+pool.size());
        }//end if
          
        //删除已经多余的睡眠线程-------------shrink动作
        int more_sleep_size = pool.size() - threads_min_num;//最多能删除的线程数
        int sleep_threads_size = passivePool.size();
        if(more_sleep_size >0 && sleep_threads_size >0){
          System.out.println("================Shrink thread pool starting ....");        
          for(int i=0; i < more_sleep_size && i < sleep_threads_size ; i++){
            System.out.println("=== Shrink thread num : "+i);
            Thread removeThread = (Thread)passivePool.get(0);
            if(removeThread != null && removeThread.isAlive() && !removeThread.isInterrupted()){
              removeThread.interrupt();
            }
          }
          System.out.println("===Shrink thread pool is over .... and pool size:"+pool.size());          
        }

        System.out.println("===End one return [shrink - spread operator] ....");    
      }//end while
    }//end run 
  }//end private class
  
  /**
   * 工作线程
   */
  class WorkThread implements Runnable{
  
    public WorkThread(){
    }
  
    public void run(){
      String name = Thread.currentThread().getName();
      System.out.println("===Thread.currentThread():"+name);
      pool.add(Thread.currentThread());    
    
      while(true){
      
        //获取任务---------
        ITask task = null;
        try{
          System.out.println("===Get task from queue is starting ... ");
          //看线程是否被中断,如果被中断停止执行任务----
          if(Thread.currentThread().isInterrupted()){
            System.out.println("===Breaking current thread and jump whlie [1] ... ");
            break;
          }
          task = queue.getTask();
        }
        catch(Throwable ex){
          System.out.println("###No task in queue:"+ex);
        }//end tryc
        
        if(task != null){
          //执行任务---------
          try{
            System.out.println("===Execute the task is starting ... ");
            //看线程是否被中断,如果被中断停止执行任务----
            if(Thread.currentThread().isInterrupted()){
              System.out.println("===Breaking current thread and jump whlie [1] ... ");
              break;
            }     
            task.executeTask();
            //任务执行完毕-------
            System.out.println("===Execute the task is over ... ");
          }
          catch(Throwable ex){
            System.out.println("###Execute the task is failure ... "+ex);
          }//end tryc
          
        }else{
          //没有任务,则钝化线程至规定时间--------
          synchronized(this){
            try{
              System.out.println("===Passivate into passivePool ... ");
              
              //看线程是否被中断,如果被中断停止执行任务----
              boolean isInterrupted = Thread.currentThread().isInterrupted();
              if(isInterrupted){
                System.out.println("===Breaking current thread and jump whlie [1] ... ");
                break;
              }
//              passivePool.add(this);
            passivePool.add(Thread.currentThread());

              
              //准备睡眠线程-------
              isInterrupted = Thread.currentThread().isInterrupted();
              if(isInterrupted){
                System.out.println("===Breaking current thread and jump whlie [2] ... ");
                break;
              }              
              this.wait(IDLE_TIMEOUT);
            }
            catch(Throwable ex1){
              System.out.println("###Current Thread passivate is failure ... break while cycle. "+ex1);
              break;
            }
          }          
        }        
      }//end while--------
      
      if(pool.contains(passivePool)){
        pool.remove(this);
      }
      if(passivePool.contains(passivePool)){
        passivePool.remove(this);
      }
      System.out.println("===The thread execute over ... "); 
    }//end run----------
  }
  
  
  class QueueThread implements Runnable{
  
    public QueueThread(){
    }
  
    public void run(){
      while(true){
        //自动装在任务--------
        queue.autoAddTask();
        System.out.println("===The size of queue's task is "+queue.getTaskSize());
      
        synchronized(this){
          if(Thread.currentThread().isInterrupted()){
            break;
          }else{
              try{
                this.wait(queue.getLoadDataPollingTime());
              }
              catch(Throwable ex){
                System.out.println("===QueueThread invoked wait is failure ... break while cycle."+ex);
                break;
              }
          }//end if
        }//end synchr
        
      }//end while
    }//end run
  } 
}






WorkQueue
=====================================
CODE:
package test.thread.pool1;

import java.util.LinkedList;
import test.thread.pool1.impl.MyTask;

/**
 * <p>Title: 工作队列对象 </p>
 * <p>Description: </p>
 * <p>Copyright: Copyright (c) 2005</p>
 * <p>Company: </p>
 * @author not attributable
 * @version 1.0
 */

public abstract class WorkQueue implements IWorkQueue{
  /* 预计装载量 */
  private int load_size;
  
  /* 数据装载轮循时间 */
  private long load_polling_time;
  
  /* 队列 */
  private LinkedList queue = new LinkedList();
  
  /**
   * 
   * @param load_size 预计装载量
   * @param load_polling_time 数据装载轮循时间
   */
  public WorkQueue(int load_size,long load_polling_time){
    this.load_size = (load_size <= 10) ? 10 : load_size;
    this.load_polling_time = load_polling_time;
  }

  /* 数据装载轮循时间 */
  public long getLoadDataPollingTime(){
    return this.load_polling_time;
  }


  /*获取任务,并删除队列中的任务*/
  public synchronized ITask getTask(){
    ITask task = (ITask)queue.getFirst();
    queue.removeFirst();
    return task;
  }

  /*加入任务*/
  public void  addTask(ITask task){
    queue.addLast(task);
  }

  /*删除任务*/
  public synchronized void removeTask(ITask task){
    queue.remove(task);
  }

  /*任务总数*/
  public synchronized int getTaskSize(){
    return queue.size();
  }

  /*自动装填任务*/
  public synchronized void autoAddTask(){
  
    synchronized(this){
      float load_size_auto = load_size - getTaskSize() / load_size;
      System.out.println("===load_size_auto:"+load_size_auto);
      
      if(load_size_auto > 0.25){        
        autoAddTask0();
      }
      else {
        System.out.println("=== Not must load new work queue ... Now! ");
      }    
    }
  }

  /*删除所有任务*/
  public synchronized void clearAllTask(){
    queue.clear();
  }
  
  /**
   * 程序员自己实现该方法
   */
  protected abstract void autoAddTask0();
}





MyWorkQueue
=====================================
CODE:
package test.thread.pool1.impl;

import java.util.LinkedList;
import test.thread.pool1.WorkQueue;

/**
 * <p>Title: 例子工作队列对象 </p>
 * <p>Description: </p>
 * <p>Copyright: Copyright (c) 2005</p>
 * <p>Company: </p>
 * @author not attributable
 * @version 1.0
 */

public class MyWorkQueue extends WorkQueue{

  /**
   * @param load_size 预计装载量
   * @param load_polling_time 数据装载轮循时间
   */
  public MyWorkQueue(int load_size,long load_polling_time){
    super(load_size,load_polling_time);
  }

  /**
   * 自动加载任务
   */
  protected synchronized void autoAddTask0(){
    //-------------------
    System.out.println("===MyWorkQueue ...  invoked autoAddTask0() method ...");
    for(int i=0; i<10; i++){
      System.out.println("===add task :"+i);
      this.addTask(new MyTask());
    }    
    //-------------------
  }
}





MyTask
=====================================
CODE:
package test.thread.pool1.impl;
import test.thread.pool1.ITask;

/**
 * <p>Title: 工作任务接口 </p>
 * <p>Description: </p>
 * <p>Copyright: Copyright (c) 2005</p>
 * <p>Company: </p>
 * @author not attributable
 * @version 1.0
 */

public class MyTask implements ITask {

  /**
   * 执行的任务
   * @throws java.lang.Throwable
   */
  public void executeTask() throws Throwable{
    System.out.println("["+this.hashCode()+"] MyTask ... invoked executeTask() method ... ");
  }
}

posted @ 2006-08-24 16:55 Binary 阅读(3711) | 评论 (2)编辑 收藏

正确优雅的解决用户退出问题——JSP和Struts解决方案

注:本文是由 马嘉楠 翻译的javaworld.com上的一篇名为《Solving the logout problem properly and elegantly》的文章,原文请参看 Solving the logout problem properly and elegantly
花了2天翻译过后才发现wolfmanchen已经捷足先登了,而且翻译得很准确(比我的好,^+^)我进行了修改,在此谢谢wolfmanchen。
文中所有示例程序的代码可以从javaworld.com中下载,文章后面有资源链接。

我看过之后觉得很好,希望对你也有所帮助!


                             正确优雅的解决用户退出问题
                                           ------JSP和Struts解决方案


摘要

在一个有密码保护的Web应用当中,正确妥善的处理用户退出过程并不仅仅只需要调用HttpSession对象的invalidate()方法,因为现在大部分浏览器上都有后退(Back)和前进(Forward)按钮,允许用户后退或前进到一个页面。在用户退出一个Web应用之后,如果按了后退按钮,浏览器把缓存中的页面呈现给用户,这会使用户产生疑惑,他们会开始担心他们的个人数据是否安全。

实际上,许多Web应用会弹出一个页面,警告用户退出时关闭整个浏览器,以此来阻止用户点击后退按钮。还有一些使用JavaScript,但在某些客户端浏览器中这却不一定起作用。这些解决方案大多数实现都很笨拙,且不能保证在任何情况下都100%有效,同时,它还要求用户有一定的操作经验。

这篇文章以简单的程序示例阐述了正确解决用户退出问题的方案。作者Kevin Le首先描述了一个理想的密码保护Web应用,然后以示例程序解释问题如何产生并讨论解决问题的方案。文章虽然是针对JSP进行讨论阐述,但作者所阐述的概念很容易理解而且能够为其他Web技术所采用。最后最后,作者Kevin Le用Jakarta Struts更为优雅地解决用户退出问题。文中包含JSP和Struts的示例程序 (3,700 words; September 27, 2004)




大部分Web应用不会包含像银行账户或信用卡资料那样机密的信息,但是一旦涉及到敏感数据,就需要我们提供某些密码保护机制。例如,在一个工厂当中,工人必须通过Web应用程序访问他们的时间安排、进入他们的培训课程以及查看他们的薪金等等。此时应用SSL(Secure Socket Layer)就有些大材小用了(SSL页面不会在缓存中保存,关于SSL的讨论已经超出本文的范围)。但是这些应用又确实需要某种密码保护措施,否则,工人(在这种情况下,也就是Web应用的使用者)就可以发现工厂中所有员工的私人机密信息。

类似上面的情况还包括位于公共图书馆、医院、网吧等公共场所的计算机。在这些地方,许多用户共同使用几台计算机,此时保护用户的个人数据就显得至关重要。
同时应用程序的良好设计与实现对用户专业知识以及相关培训要求少之又少。

让我们来看一下现实世界中一个完美的Web应用是怎样工作的:
1. 用户在浏览器中输入URL,访问一个页面。
2. Web应用显示一个登陆页面,要求用户输入有效的验证信息。
3. 用户输入用户名和密码。
4. 假设用户提供的验证信息是正确的,经过了验证过程,Web应用允许用户浏览他有权访问的区域。
5. 退出时,用户点击页面的退出按钮,Web应用显示确认页面,询问用户是否真的需要退出。一旦用户点击确定按钮,Session结束,Web应用重新定位到登陆页面。用户现在可以放心的离开而不用担心他的信息会被泄露。
6. 另一个用户坐到了同一台电脑前。他点击后退按钮,Web应用不应该显示上一个用户访问过的任何一个页面。
事实上,Web应用将一直停留在登陆页面上,除非第二个用户提供正确的验证信息,之后才可以访问他有权限的区域。

通过示例程序,文章向您阐述了如何在一个Web应用中实现上面的功能。




一. JSP samples

为了更为有效地向您说明这个解决方案,本文将从展示一个Web应用logoutSampleJSP1中碰到的问题开始。这个示例代表了许多没有正确解决退出过程的Web应用。logoutSampleJSP1包含一下JSP页面:login.jsp,  home.jsp,  secure1.jsp,  secure2.jsp,  logout.jsp,  loginAction.jsp, 和 logoutAction.jsp。其中页面home.jsp,  secure1.jsp,  secure2.jsp, 和 logout.jsp是不允许未经认证的用户访问的,也就是说,这些页面包含了重要信息,在用户登陆之前或者退出之后都不应该显示在浏览器中。login.jsp页面包含了用于用户输入用户名和密码的form。logout.jsp页面包含了要求用户确认是否退出的form。loginAction.jsp和logoutAction.jsp作为控制器分别包含了登陆和退出动作的代码。

第二个Web示例应用logoutSampleJSP2展示了如何纠正示例logoutSampleJSP1中的问题。但是第二个示例logoutSampleJSP2自身也是有问题的。在特定情况下,退出问题依然存在。

第三个Web示例应用logoutSampleJSP3对logoutSampleJSP2进行了改进,比较妥善地解决了退出问题。

最后一个Web示例logoutSampleStruts展示了JakartaStruts如何优雅地解决退出问题。

注意:本文所附示例在最新版本的Microsoft Internet Explorer (IE), Netscape Navigator, Mozilla, FireFox和Avant浏览器上测试通过。



二. Login action

Brian Pontarelli的经典文章
《J2EE Security: Container Versus Custom》 讨论了不同的J2EE认证方法。文章同时指出,HTTP协议和基于form的认证方法并不能提供处理用户退出问题的机制。因此,解决方法便是引入用户自定义的安全实现机制,这就提供了更大的灵活性。

在用户自定义的认证方法中,普遍采用的方法是从用户提交的form中获得用户输入的认证信息,然后到诸如LDAP (lightweight directory access protocol)或关系数据库(relational database management system, RDBMS)的安全域中进行认证。如果用户提供的认证信息是有效的,登陆动作在HttpSession对象中保存某个对象。HttpSession存在着保存的对象则表示用户已经登陆到Web应用当中。为了方便起见,本文所附的示例只在HttpSession中保存一个用户名以表明用户已经登陆。清单1是从loginAction.jsp页面中节选的一段代码以此讲解登陆动作:




Listing 1
//...

//initialize RequestDispatcher object; set forward to home page by default
RequestDispatcher rd = request.getRequestDispatcher(
"home.jsp" );

//Prepare connection and statement

rs = stmt.executeQuery(
"select password from USER where userName = '" + userName + "'" );
if (rs.next()) {
//Query only returns 1 record in the result set;
//Only 1
password per userName which is also the primary key
    if (rs.getString(
"password" ).equals(password)) { //If valid password

        session.setAttribute(
"User" , userName); //Saves username string in the session object
    }
    else {
//Password does not match, i.e., invalid user password
        request.setAttribute(
"Error" , "Invalid password." );

        rd = request.getRequestDispatcher(
"login.jsp"
);
    }
}
//No record in the result set, i.e., invalid username

    else {

        request.setAttribute(
"Error" , "Invalid user name." );
        rd = request.getRequestDispatcher(
"login.jsp"
);
    }
}

//As a controller, loginAction.jsp finally either forwards to "login.jsp" or "home.jsp"

rd.forward(request, response);
//...

				
				
		


本文当中所附Web应用示例均以关系型数据库作为安全域,但本问所讲述的内容同样适用于其他任何类型的安全域。



三. Logout action

退出动作包含删除用户名以及调用用户的HttpSession对象的invalidate()方法。清单2是从loginoutAction.jsp中节选的一段代码,以此说明退出动作:



Listing 2
//...

session.removeAttribute(
"User" );
session.invalidate();
//...

						
						
				


四. 阻止未经认证访问受保护的JSP页面

从提交的form中获取用户提交的认证信息并经过验证后,登陆动作仅仅在HttpSession对象中写入一个用户名。退出动作则刚好相反,它从HttpSession中删除用户名并调用HttpSession对象的invalidate()方法。为了使登陆和退出动作真正发挥作用,所有受保护的JSP页面必须首先验证HttpSession中包含的用户名,以便确认用户当前是否已经登陆。如果HttpSession中包含了用户名,就说明用户已经登陆,Web应用会将剩余的JSP页中的动态内容发送给浏览器。否则,JSP页将跳转到登陆页面,login.jsp。页面home.jsp,  secure1.jsp,  secure2.jsp和 logout.jsp均包含清单3中的代码段:


Listing 3
//...
String userName = (String) session.getAttribute(
"User" );
if (null == userName) {
    request.setAttribute(
"Error" , "Session has ended. Please login."
);
    RequestDispatcher rd = request.getRequestDispatcher(
"login.jsp"
);
    rd.forward(request, response);
}
//...

//Allow the rest of the dynamic content in this JSP to be served to the browser
//...
						
						
				


在这个代码段中,程序从HttpSession中检索username字符串。如果username字符串为空,Web应用则自动中止执行当前页面并跳转到登陆页,同时给出错误信息“Session has ended. Please log in.”;如果不为空,Web应用继续执行,把剩余的页面提供给用户,从而使JSP页面的动态内容成为服务对象。



五.运行logoutSampleJSP1

运行logoutSampleJSP1将会出现如下几种情形:

• 如果用户没有登陆,Web应用将会正确中止受保护页面home.jsp,  secure1.jsp,  secure2.jsp和logout.jsp中动态内容的执行。也就是说,假如用户并没有登陆,但是在浏览器地址栏中直接敲入受保护JSP页的地址试图访问,Web应用将自动跳转到登陆页面,同时显示错误信息“Session has ended.Please log in.”

• 同样的,当一个用户已经退出,Web应用将会正确中止受保护页面home.jsp,  secure1.jsp,  secure2.jsp和logout.jsp中动态内容的执行。也就是说,用户退出以后,如果在浏览器地址栏中直接敲入受保护JSP页的地址试图访问,Web应用将自动跳转到登陆页面,同时显示错误信息“Session has ended.Please log in.”

• 用户退出以后,如果点击浏览器上的后退按钮返回到先前的页面,Web应用将不能正确保护受保护的JSP页面——在Session销毁后(用户退出)受保护的JSP页会重新显示在浏览器中。然而,点击该页面上的任何链接,Web应用都会跳转到登陆页面,同时显示错误信息“Session has ended.Please log in.”



六. 阻止浏览器缓存
 
上述问题的根源就在于现代大部分浏览器都有一个后退按钮。当点击后退按钮时,默认情况下浏览器不会从Web服务器上重新获取页面,而是简单的从浏览器缓存中重新载入页面。这个问题并不仅限于基于Java(JSP/servlets/Struts) 的Web应用当中,在基于PHP (Hypertext Preprocessor)、ASP、(Active Server Pages)、和.NET的Web应用中也同样存在。

在用户点击后退按钮之后,浏览器到Web服务器(一般来说)或者应用服务器(在java的情况下)再从服务器到浏览器这样通常意义上的HTTP回路并没有建立。仅仅只是用户,浏览器和缓存之间进行了交互。所以即使受保护的JSP页面,例如home.jsp,  secure1.jsp,  secure2.jsp和logout.jsp包含了清单3上的代码,当点击后退按钮时,这些代码也永远不会执行的。

缓存的好坏,真是仁者见仁智者见智。缓存事实上的确提供了一些便利,但这些便利通常只存在于静态的HTML页面或基于图形或影像的页面。而另一方面,Web应用通常是面向数据的。由于Web应用中的数据频繁变更,所以与为了节省时间从缓存中读取并显示过期的数据相比,提供最新的数据显得尤为重要!

幸运的是,HTTP头信息“Expires”和“Cache-Control”为应用程序服务器提供了一个控制浏览器和代理服务器上缓存的机制。HTTP头信息Expires告诉代理服务器它的缓存页面何时将过期。HTTP1.1规范中新定义的头信息Cache-Control在Web应用当中可以通知浏览器不缓存任何页面。当点击后退按钮时,浏览器发送Http请求道应用服务器以便获取该页面的最新拷贝。如下是使用Cache-Control的基本方法:

• no-cache:强制缓存从服务器上获取该页面的最新拷贝
• no-store: 在任何情况下缓存不保存该页面

HTTP1.0规范中的Pragma:no-cache等同于HTTP1.1规范中的Cache-Control:no-cache,同样可以包含在头信息中。

通过使用HTTP头信息的cache控制,第二个示例应用logoutSampleJSP2解决了logoutSampleJSP1的问题。logoutSampleJSP2与logoutSampleJSP1不同表现在如下代码段中,这一代码段加入进所有受保护的页面中:




//...
response.setHeader(
"Cache-Control" , "no-cache" ); //Forces caches to obtain a new copy of the page from the origin server
response.setHeader(
"Cache-Control" , "no-store" ); //Directs caches not to store the page under any circumstance
response.setDateHeader(
"Expires" , 0); //Causes the proxy cache to see the page as "stale"
response.setHeader(
"Pragma" , "no-cache" ); //HTTP 1.0 backward compatibility
String userName = (String) session.getAttribute(
"User" );
if (null == userName) {
    request.setAttribute(
"Error" , "Session has ended. Please login."
);
    RequestDispatcher rd = request.getRequestDispatcher(
"login.jsp"
);
    rd.forward(request, response);
}
//...

						
						
				


通过设置头信息和检查HttpSession对象中的用户名来确保浏览器不会缓存JSP页面。同时,如果用户未登陆,JSP页面的动态内容不会发送到浏览器,取而代之的将是登陆页面login.jsp。



七. 运行logoutSampleJSP2

运行Web示例应用logoutSampleJSP2后将会看到如下结果:

• 当用户退出后试图点击后退按钮,浏览器不会重新显示受保护的页面,它只会显示登陆页login.jsp同时给出提示信息Session has ended. Please log in.

• 然而,当按了后退按钮返回的页是处理用户提交数据的页面时,IE和Avant浏览器将弹出如下信息提示:

           警告:页面已过期
           The page you requested was created using information you submitted in a form. This page is no longer available. As a security precaution, Internet Explorer does not automatically  resubmit your information for you.

Mozilla和FireFox浏览器将会显示一个对话框,提示信息如下:

            The page you are trying to view contains POSTDATA that has expired from cache. If you  resend the data, any action from the form carried out (such as a search or online purchase) will be repeated. To resend the data, click OK. Otherwise, click Cancel.

在IE和Avant浏览器中选择刷新或者在Mozilla和FireFox浏览器中选择重新发送数据后,前一个JSP页面将重新显示在浏览器中。显然的,这病不是我们所想看到的因为它违背了logout动作的目的。发生这一现象时,很可能是一个恶意用户在尝试获取其他用户的数据。然而,这个问题仅仅出现在点击后退按钮后,浏览器返回到一个处理POST请求的页面。



八. 记录最后登陆时间

上述问题的发生是因为浏览器重新提交了其缓存中的数据。这本文的例子中,数据包含了用户名和密码。尽管IE浏览器给出了安全警告信息,但事实上浏览器此时起到了负面作用。

为了解决logoutSampleJSP2中出现的问题,logoutSampleJSP3的login.jsp除了包含username和password的之外,还增加了一个称作lastLogon的隐藏表单域,此表单域将会动态的被初始化为一个long型值。这个long型值是通过调用System.currentTimeMillis()获取到的自1970年1月1日以来的毫秒数。当login.jsp中的form提交时,loginAction.jsp首先将隐藏域中的值与用户数据库中的lastLogon值进行比较。只有当lastLogon表单域中的值大于数据库中的值时Web应用才认为这是个有效的登陆。

为了验证登陆,数据库中lastLogon字段必须用表单中的lastLogon值进行更新。上例中,当浏览器重复提交缓存中的数据时,表单中的lastLogon值不比数据库中的lastLogon值大,因此,loginAction将跳转到login.jsp页面,并显示如下错误信息“Session has ended.Please log in.”清单5是loginAction中节选的代码段:




清单5
//...
RequestDispatcher rd = request.getRequestDispatcher(
"home.jsp" ); //Forward to homepage by default
//...
if (rs.getString(
"password" ).equals(password)) { //If valid password
    long lastLogonDB = rs.getLong(
"lastLogon" );
    if (lastLogonForm > lastLogonDB) {
        session.setAttribute(
"User" , userName); //Saves username string in the session object

        stmt.executeUpdate(
"update USER set lastLogon= " + lastLogonForm + " where userName = '" + userName + "'" );
    }
    else {
        request.setAttribute(
"Error" , "Session has ended. Please login."
);
        rd = request.getRequestDispatcher(
"login.jsp"
); }
    }
else {
//Password does not match, i.e., invalid user password

    request.setAttribute(
"Error" , "Invalid password." );
    rd = request.getRequestDispatcher(
"login.jsp"
);
}
//...

rd.forward(request, response);
//...
						
						
				


为了实现上述方法,你必须记录每个用户的最后登陆时间。对于采用关系型数据库安全域来说,这点可以可以通过在某个表中加上lastLogin字段轻松实现。虽然对LDAP以及其他的安全域来说需要稍微动下脑筋,但最后登陆方法很显然是可以实现的。

表示最后登陆时间的方法有很多。示例logoutSampleJSP3利用了自1970年1月1日以来的毫秒数。这个方法即使在许多人在不同浏览器中用一个用户账号登陆时也是可行的。



九. 运行logoutSampleJSP3

运行示例logoutSampleJSP3将展示如何正确处理退出问题。一旦用户退出,点击浏览器上的后退按钮在任何情况下都不会在浏览器中显示受保护的JSP页面。这个示例展示了如何正确处理退出问题而不需要对用户进行额外的培训。

为了使代码更简练有效,一些冗余的代码可以剔除。一种途径就是把清单4中的代码写到一个单独的JSP页中,其他JSP页面可以通过标签
<jsp:include>进行使用



十. Struts框架下的退出实现

与直接使用JSP或JSP/servlets进行Web应用开发相比,另一个更好的可选方案是使用Struts。对于一个基于Struts的Web应用来说,添加一个处理退出问题的框架可以优雅地不费气力的实现。这归功于Struts是采用MVC设计模式的,因此可以将模型和视图代码清晰的分离。另外,Java是一个面向对象的语言,支持继承,可以比JSP中的脚本更为容易地实现代码重用。对于Struts来说,清单4中的代码可以从JSP页面中移植到Action类的execute()方法中。

此外,我们还可以定义一个继承Struts Action类的Action基类,其execute()方法中包含了类似清单4中的代码。通过继承,其他Action类可以继承基本类中的通用逻辑来设置HTTP头信息以及检索HttpSession对象中的username字符串。这个Action基类是一个抽象类并定义了一个抽象方法executeAction()。所有继承自Action基类的子类都必须实现exectuteAction()方法而不是覆盖它。通过继承这一机制,所有继承自Action基类的子类都不必再担心退出代码接口。(plumbing实在不知道怎么翻译了,^+^,高手帮帮忙啊!原文:With this inheritance hierarchy in place, all of the base Action's subclasses no longer need to worry about any plumbing logout code.)。他们将只包含正常的业务逻辑代码。清单6是基类的部分代码:



清单6
publicabstractclass BaseAction extends Action {
    public ActionForward execute(ActionMapping mapping, ActionForm form,
                                                 HttpServletRequest request, HttpServletResponse response) 
               throws IOException, ServletException {

response.setHeader(
"Cache-Control" , "no-cache" ); //Forces caches to obtain a new copy of the page from the origin server
response.setHeader(
"Cache-Control" , "no-store" ); //Directs caches not to store the page under any circumstance
response.setDateHeader(
"Expires" , 0); //Causes the proxy cache to see the page as "stale"
response.setHeader(
"Pragma" , "no-cache" ); //HTTP 1.0 backward compatibility

if (!this.userIsLoggedIn(request)) {
    ActionErrors errors = new ActionErrors();

    errors.add(
"error" , new ActionError( "logon.sessionEnded" ));
    this.saveErrors(request, errors);

    return mapping.findForward(
"sessionEnded"
);
}

return executeAction(mapping, form, request, response);
}

protectedabstract ActionForward executeAction(ActionMapping mapping,  ActionForm form, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException;

privateboolean userIsLoggedIn(HttpServletRequest request) {
if (request.getSession().getAttribute(
"User"
) == null) {
    return false;
}

return true;
}
}
						
						
				


清单6中的代码与清单4中的很相像,唯一区别是用ActionMapping findForward替代了RequestDispatcher forward。清单6中,如果在HttpSession中未找到username字符串,ActionMapping对象将找到名为sessionEnded的forward元素并跳转到对应的path。如果找到了,子类通过实现executeAction()方法,将执行他们自己的业务逻辑。因此,在struts-web.xml配置文件中为所有继承自Action基类的子类声明个一名为sessionEnded的forward元素并将其指向login.jsp是至关重要的。清单7以secure1 action阐明了这样一个声明:


清单7
<action path=
"/secure1"
type=
"com.kevinhle.logoutSampleStruts.Secure1Action"
scope=
"request" >
<forward name=
"success" path= "/WEB-INF/jsps/secure1.jsp"
/>
<forward name=
"sessionEnded" path= "/login.jsp"
/>
</action>

						
						
				


继承自BaseAction类的子类Secure1Action实现了executeAction()方法而不是覆盖它。Secure1Action类不需要执行任何退出代码,如清单8:



清单8
publicclass Secure1Action extends BaseAction {
    public ActionForward executeAction(ActionMapping mapping, ActionForm form,
                                                           HttpServletRequest request, HttpServletResponse response)
               throws IOException, ServletException {

               HttpSession session = request.getSession(); 
               return (mapping.findForward(
"success" ));
         }
}
						
						
				


上面的解决方案是如此的优雅有效,它仅仅只需要定义一个基类而不需要额外的代码工作。将通用的行为方法写成一个继承StrutsAction的基类是者的推荐的,而且这是许多Struts项目的共同经验。




十一. 局限性

上述解决方案对JSP或基于Struts的Web应用都是非常简单而实用的,但它还是有某些局限。在我看来,这些局限并不是至关紧要的。

•   通过取消与浏览器后退按钮有关的缓存机制,一旦用户离开页面而没有对数据进行提交,那么页面将会丢失所有输入的数据。即使点击浏览器的后退按钮返回到刚才的页面也无济于事,因为浏览器会从服务器获取新的空白页面显示出来。一种可能的方法并不是阻止这些JSP页面包含数据数据表格。在基于JSP的解决方案当中,那些JSP页面可以删除在清单4中的代码。在基于Struts的解决方案当中,Action类需要继承自Struts的Action类而非BaseAction类。

•  上面讲述的方法在Opera浏览器中不能工作。事实上没有适用于Opera浏览器的解决方案,因为Opera浏览器与2616 Hypertext Transfer Protocol—HTTP/1.1紧密相关。Section 13.13 of RFC 2616 states:          
User agents often have history mechanisms, such as "Back" buttons and history lists, which can be used to redisplay an entity retrieved earlier in a session.

History mechanisms and caches are different. In particular history mechanisms SHOULD NOT try to show a semantically transparent view of the current state of a resource. Rather, a history mechanism is meant to show exactly what the user saw at the time when the resource was retrieved.

幸运的是,使用微软的IE和基于Mozilla的浏览器用户多余Opera浏览器。上面讲述的解决方案对大多数用户来说还是有帮助的。另外,无论是否使用上述的解决方案,Opera浏览器仍然存在用户退出问题,就Opera来说没有任何改变。然而,正如RFC2616中所说,通过像上面一样设置头文件指令,当用户点击一个链接时,Opera浏览器不会从缓存中获取页面。




十二. 结论

这篇文章讲述了处理退出问题的解决方案,尽管方案简单的令人惊讶,但在所有情况下都能有效地工作。无论是对JSP还是Struts,所要做的不过是写一段不超过50行的代码以及一个记录用户最后登陆时间的方法。在有密码保护的Web应用中使用这些方案能够确保在任何情况下用户的私人数据不致泄露,同时,也能增加用户的经验。

About the author
[i]Kevin H. Le[/i] has more than 12 years of experience in software development. In the first half of his career, his programming language of choice was C++. In 1997, he shifted his focus to Java. He has engaged in and successfully completed several J2EE and EAI projects as both developer and architect. In addition to J2EE, his current interests now include Web services and SOA. More information on Kevin can be found on his Website http://kevinhle.com.

posted @ 2006-08-24 15:56 Binary 阅读(293) | 评论 (0)编辑 收藏

区分eclipse中的两种JRE

CowNew 开源团队网站 www.cownew.com

论坛 http://www.cownew.com/newpeng/ 

转载请保留此信息

今天一个CownewStudio的使用者通过QQ问我他的Eclipse安装CownewStudio以后在eclipse中可以看到studio,但是运行的时候提示类加载错误。因为CownewStudio目前的版本只支持JDK5,所以我询问他Eclipse使用的是不是1.4的JRE,但是他确认它用的就是1.5的。
后来经过实验,我确认还是JRE版本的问题,他就把他的Eclipse截图发给了我,以证明他用的是JDK1.5,但是我发现他发过来的图片工程编译器配置对话框的。哈哈,我终于明白了,让他把Eclipse的配置详细信息(“帮助”=》“关于Eclipse”=》“配置详细信息”)发过来,果然:
-vm
c:\programe\jdk1.4.2\jre\bin\javaw.exe

原来他装了多个版本的JDK。我要他把JDK1.5目录下的JRE目录拷贝到eclipse安装目录下,然后重启Eclipse,一切工作正常了。
其实这是很多刚刚接触Eclipse、甚至用了好长时间Eclipse的开发人员经常犯的错,也就是把Eclipse运行时的JRE与工作空间中项目所用的JRE版本弄混乱。
Eclipse也是一个普通的Java程序,因此必须有一个JRE做为运行环境。如果你的机器上没有安装任何JRE(或者JDK,本文不做二者的区分),那么点击eclipse.exe就会报错说找不到JRE。此时可以安装一个JRE、或者直接把JRE目录拷贝到eclipse安装目录下。
在Eclipse的每个项目中可以为项目指定不同的JRE版本,比如A项目使用JDK1.4编译,B项目使用JDK1.5编译。这个JDK版本是和Eclipse运行JRE没有直接关系的。
项目的JDK版本是很容易修改的,那么任何指定Eclipse启动所用的JRE呢?
Eclipse启动的时候找JRE的顺序是:如果eclipse.ini中配置了-vm参数,那么则使用这个参数指定的JRE;否则就去查看eclipse安装目录下是否有JRE文件夹,如果有的话就使用这个JRE;否则的话就去系统中查找安装的JRE,如果还找不到的话就报错。
所以如果不想卸载掉其他的JDK的话,可以有两种方式:(1)直接把要使用的JRE文件夹拷贝到Eclipse目录下,这是懒人常用的方法(2)修改eclipse.ini文件,添加-vm参数,指定要运行的虚拟机的地址,使用 -vm 命令行自变量例子:-vm c:\jre\bin\javaw.exe

posted @ 2006-08-24 15:54 Binary 阅读(219) | 评论 (0)编辑 收藏

对象传递和信息完整性

    很少有孤立存在的实体对象,它们之间总是会有所关联。对象们因其职责而分离,又因其联系而聚合。而我们在使用对象时,往往不需要把对象及其聚合的所有其他对象一次性全部初始化,部分的对象聚合足以提供足够的信息了。这时候,我们使用的对象的信息是不完整的。

    当具有不完整信息的对象被做为参数传递时,很可能导致我们对对象失去控制。部分对象的聚合在不同情景下的切换可能导致编程上的错误,而且对系统的维护带来负面的影响。由于对象职责的分割,对象本身无法理解这个问题,我们无法通过对对象本身进行处理(如增加职责)来消除这种不良影响。如何使用对象的部分信息是由使用者根据情景要求决定的,编码人员往往对使用情景没有深刻的认识。在连续对应不同情景的处理链中,容易导致编码错误。如下:

 1// Entity objects.
 2public class Obj {
 3  private ObjRef1 ref1;
 4  Private ObjRef2 ref2;
 5
 6  public Obj() {
 7  }
 8
 9  public Obj(ObjRef1 ref1) {
10    this.ref1 = ref1;
11  }
12
13  public Obj(ObjRef1 ref1, ObjRef2 ref2) {
14    this.ref1 = ref1;
15    this.ref2 = ref2;
16  }
17  // Accessors omitted.
18} // ObjRef1, ObjRef2 omitted.
19
20// Process1
21public class Prs1 {
22  public Obj method1() {
23    Obj obj = new Obj();
24    obj.setObjRef1(loadObjRef1());
25    // Do something to obj.
26    return obj;
27  }
     // Load method omitted.
     private ObjRef1 loadObjRef1() {
       // ......
     }
28}
29
30// Process2
31public class Prs2 {
32  public void method2() {
33    Obj obj = new Clt1().method1();
34    // Do something to obj.ref2. Coding error may be found until debug phase.
35  }
36}

    在Process1.method1方法中持久化Obj,在Process2.method2中自行初始化Obj。这个方案面向对象并解决了一致性问题,但增加性能成本。在Process1.method1中loadObjRef2,不够面向对象,增加了维护成本。
   
    尽可能不使用这种链式的处理,代之以扁平的、可以总控Obj的方式:
   
 1public class Process {
 2  public void proceed() {
 3    // Need to change Prs1, Prs2's method signatures.
 4    Obj obj = 
 5    obj = new Prs1().method1(obj);
 6    obj.setObjRef2();
 7    obj = new Prs2().method2(obj);
 8    
 9    // Future processes can be inserted anywhere inside this method.
10  }
11}


    对于树形结构或整体-部分结构,有一个统一的处理合理的,但内部的复杂性还是很高。

posted @ 2006-08-24 15:44 Binary 阅读(267) | 评论 (0)编辑 收藏

Java线程安全

一直不敢写点什么,是因为战战兢兢,生怕写的不好甚至写错了会误人子弟。随笔可以随便写一下,不用太过计较,可是技术从来都要不得半点马虎,差之毫厘,谬以千里啊!但敝帚自珍又不是我的风格,虽然文笔不好,也要勉为其难了。废话少说,进入正题。

 

       从我开始接触 Java 的多线程起就总是觉得书上讲的不是那么清楚。不是说读完了不会写,而是对写出来的多线程代码懵懵懂懂,不知道每一句会有什么影响,心里感觉忐忑。后来仔细研读 Java 语言规范后,才慢慢搞明白一些细节。我主要想说的,也就是这些经验吧。

 

       首先要搞清楚的是线程的共享资源,共享资源是多线程中每个线程都要访问的类变量或实例变量,共享资源可以是单个类变量或实例变量,也可以是一组类变量或实例变量。多线程程序可以有多个共享资源。下面描述他们之间的一对多关系( * 表示多):

      

                     多线程程序( 1 ---- 共享资源( * ---- 类变量或实例变量( 1…*

 

只有类变量和实例变量可以成为共享资源,细分如下:

<!--[if !supportLists]-->1.       <!--[endif]-->实现线程的类(继承Thread类、实现Throwable接口的类)的类变量、实例变量。

<!--[if !supportLists]-->2.       <!--[endif]-->实现线程的类的类变量、实例变量的类变量、实例变量,可以不规范的写为:TreadClass.ClassOrInstanceVar[.ClassOrInstanceVar]*[]*的内容表示无限可重复。

<!--[if !supportLists]-->3.       <!--[endif]-->不是实现线程的类,但其对象可能是线程的类变量或实例变量。如ServletEJB。这些类的类变量和实例变量,不规范的写为:ServletOrEJB.ClassOrInstanceVar[.ClassOrInstanceVar]*

<!--[if !supportLists]-->4.       <!--[endif]-->特别注意:局部变量、做为参数传递的非类变量、非实例变量不是共享资源。

 

那么什么是线程安全呢?关于这个问题我在网上百度了一下(没办法,有时候 GOOGLE 用不了),发现不少人在问这个问题,也有不少错误的理解。所以我给出一个较容易理解的解释:在线程中使用共享资源时,能够保证共享资源在任何时候都是原子的、一致的,这样的线程就是线程安全的线程。还不太理解?没有关系,慢慢解释。

 

首先来介绍一下共享资源的类型(这是我自己分类的,为了后文好解释),共享资源从其类型可以分为三类(下文讲到变量一律指类变量或实例变量,不再特别指出):

<!--[if !supportLists]-->1.       <!--[endif]-->独立的基本类型共享资源,如一个简单的int变量,例:

public class Cls1 {

       private int a;

       public int getA(){return a;}

       public void setA(int a){this.a = a;}

}

可以看到 a 没有任何依赖。

public class Cls2{

       private int a;

       private int b;

       private int c;

       // 没有对 a 的访问方法, a Cls 外不可见。

}

假设上面类中 b c 都不依赖 a ,则 a 是这种类型。

 

<!--[if !supportLists]-->2.       <!--[endif]-->相互依赖的基本类型共享资源,一个类中的几个基本类型变量互相依赖,但从对象设计的角度又不能单独把这几个变量设计成一个类。

假设上例 Cls2 中的 b c 互相依赖,则属此种情况。

<!--[if !supportLists]-->3.       <!--[endif]-->64位的基本类型变量。这个比较特殊,因为某些机器上64变量会分成两个32位的操作,所以和1不一样。如doublelong类型。

<!--[if !supportLists]-->4.       <!--[endif]-->类类型的共享资源。如下例中的obj

public class Cls3{

       private SomeObj obj;

}

public class SomeObj{

       private int a;

       private int b;

}

 

       其次来看看什么是原子性、一致性。其实在这里我借用了事务 ACID 属性的 A C ,熟悉的朋友就不用我废话了。所谓原子性,是指一个共享资源的所有属性在任何时刻都是一起变化、密不可分的;所谓一致性,是指一个共享资源的所有属性在变化之后一定会达到一个一致的状态。

 

       最后根据上述四种共享资源类型,来看看如何做到线程安全。

 

<!--[if !supportLists]-->1.       <!--[endif]-->不用做什么,只一个独立的变量,任何时候它都是原子、一致的。

<!--[if !supportLists]-->2.       <!--[endif]-->使用synchronized关键字,保证几个变量被一起修改、一起读取。

<!--[if !supportLists]-->3.       <!--[endif]-->使用volatile关键字,然后就和1一样了。

<!--[if !supportLists]-->4.       <!--[endif]-->2一样处理。

 

当对访问共享资源的方法不同时使用 synchronized 关键字时,是什么样一种情况呢?这是需要特别注意的,这样不能保证线程安全!看看下面例子的运行结果就知道了(自己运行啊,我不贴结果了):

/**

 * $Author: $

 * $Date: $

 * $Revision: $

 * $History: $

 *

 * Created by feelyou, at time 22:31:53 , 2005-11-16.

 */

 

public class TestThread extends Thread {

 

  private int a = 0;

  private int b = 0;

 

  public static void main(String[] args) {

    TestThread test = new TestThread();

    for (int i = 0; i < 10; i++) {

      Thread thread = new Thread(test, "thread-" + i);

      thread.start();

    }

  }

 

  public synchronized void doWrite() {

    a++;

    try {

      sleep((int)(Math.random()*100));

    }

    catch (InterruptedException e) {

    }

    b++;

    try {

      sleep((int)(Math.random()*100));

    }

    catch (InterruptedException e) {

    }

  }

 

  public void print() {

    System.out.println("" + Thread.currentThread().getName() + ":a:" + a);

    System.out.println("" + Thread.currentThread().getName() + ":b:" + b);

  }

 

  public void run() {

    super.run();    //To change body of overridden methods use File | Settings | File Templates.

    for (int i = 0; i < 10; i++) {

      doWrite();

      print();

    }

  }

 

  public synchronized void start() {

    super.start();    //To change body of overridden methods use File | Settings | File Templates.

  }

}

 

ThreadLocal ThreadLocal 对于线程安全还是很有用的,如果资源不是共享的,那么应该使用 ThreadLocal ,但如果确实需要在线程间共享资源, ThreadLocal 就没有用了!

 

最后,来一个完整的线程安全的例子:

/**

 * $Author: $

 * $Date: $

 * $Revision: $

 * $History: $

 *

 * Created by feelyou, at time 22:31:53 , 2005-11-16.

 */

 

public class TestThread extends Thread {

 

  private int a = 0; // 独立的共享资源

  private int b = 0; //b c 互相依赖

  private int c = 0;

  private volatile long d = 0L; //64

//  private SomeObj obj = new SomeObj(); // 对象类型,大家自己写吧,我就不写了。

 

  public static void main(String[] args) {

    TestThread test = new TestThread();

    for (int i = 0; i < 10; i++) {

      Thread thread = new Thread(test, "thread-" + i);

      thread.start();

    }

  }

 

  public synchronized void doWrite() {

    b++;

    try {

      sleep((int)(Math.random()*100));

    }

    catch (InterruptedException e) {

    }

    c++;

    try {

      sleep((int)(Math.random()*100));

    }

    catch (InterruptedException e) {

    }

  }

 

  public synchronized void print() {

    System.out.println("" + Thread.currentThread().getName() + ":b:" + b);

    System.out.println("" + Thread.currentThread().getName() + ":c:" + c);

  }

 

  private void setA(int a) {

      this.a = a;

  }

 

  private int getA() {

      return a;

  }

 

  public long getD() {

      return d;

  }

 

  public void setD(long d) {

      this.d = d;

  }

 

  public void run() {

    super.run();    //To change body of overridden methods use File | Settings | File Templates.

    for (int i = 0; i < 10; i++) {

      doWrite();

      print();

      setA(i);

      System.out.println(getA());

      setD(18456187413L * i);

      System.out.println(getD());

    }

  }

 

  public synchronized void start() {

    super.start();    //To change body of overridden methods use File | Settings | File Templates.

  }

}

posted @ 2006-08-24 15:42 Binary 阅读(413) | 评论 (0)编辑 收藏

Hibernate 延迟初始化错误(ERROR LazyInitializer)是如何产生的?

摘要:
延迟初始化错误是运用Hibernate开发项目时最常见的错误。如果对一个类或者集合配置了延迟检索策略,那么必须当代理类实例或代理集合处于持久化状态(即处于Session范围内)时,才能初始化它。如果在游离状态时才初始化它,就会产生延迟初始化错误。

延迟初始化错误(ERROR LazyInitializer)是如何产生的?

选自<<精通Hibernate:Java对象持久化技术详解>> 作者:孙卫琴 来源:www.javathinker.org

延迟初始化错误是运用Hibernate开发项目时最常见的错误。如果对一个类或者集合配置了延迟检索策略,那么必须当代理类实例或代理集合处于持久化状态(即处于Session范围内)时,才能初始化它。如果在游离状态时才初始化它,就会产生延迟初始化错误。

下面把Customer.hbm.xml文件的<class>元素的lazy属性设为true,表示使用延迟检索策略:
<class name="mypack.Customer" table="CUSTOMERS" lazy="true">
当执行Session的load()方法时,Hibernate不会立即执行查询CUSTOMERS表的select语句,仅仅返回Customer类的代理类的实例,这个代理类具由以下特征:
(1) 由Hibernate在运行时动态生成,它扩展了Customer类,因此它继承了Customer类的所有属性和方法,但它的实现对于应用程序是透明的。
(2) 当Hibernate创建Customer代理类实例时,仅仅初始化了它的OID属性,其他属性都为null,因此这个代理类实例占用的内存很少。
(3)当应用程序第一次访问Customer代理类实例时(例如调用customer.getXXX()或customer.setXXX()方法), Hibernate会初始化代理类实例,在初始化过程中执行select语句,真正从数据库中加载Customer对象的所有数据。但有个例外,那就是当应用程序访问Customer代理类实例的getId()方法时,Hibernate不会初始化代理类实例,因为在创建代理类实例时OID就存在了,不必到数据库中去查询。

提示:Hibernate采用CGLIB工具来生成持久化类的代理类。CGLIB是一个功能强大的Java字节码生成工具,它能够在程序运行时动态生成扩展Java类或者实现Java接口的代理类。关于CGLIB的更多知识,请参考:
http://cglib.sourceforge.net/。
以下代码先通过Session的load()方法加载Customer对象,然后访问它的name属性:
tx = session.beginTransaction();
Customer customer=(Customer)session.load(Customer.class,new Long(1));
customer.getName();tx.commit();

在运行session.load()方法时Hibernate不执行任何select语句,仅仅返回Customer类的代理类的实例,它的OID为1,这是由load()方法的第二个参数指定的。当应用程序调用customer.getName()方法时,Hibernate会初始化Customer代理类实例,从数据库中加载Customer对象的数据,执行以下select语句:
select * from CUSTOMERS where ID=1;select * from ORDERS where CUSTOMER_ID=1;

当<class>元素的lazy属性为true,会影响Session的load()方法的各种运行时行为,下面举例说明。

1.如果加载的Customer对象在数据库中不存在,Session的load()方法不会抛出异常,只有当运行customer.getName()方法时才会抛出以下异常:
ERROR LazyInitializer:63 - Exception initializing proxynet.sf.hibernate.ObjectNotFoundException: No row with the given identifier exists: 1, of class: mypack.Customer

2.如果在整个Session范围内,应用程序没有访问过Customer对象,那么Customer代理类的实例一直不会被初始化,Hibernate不会执行任何select语句。以下代码试图在关闭Session后访问Customer游离对象:
tx = session.beginTransaction();
Customer customer=(Customer)session.load(Customer.class,new Long(1));
tx.commit();
session.close();
customer.getName();

由于引用变量customer引用的Customer代理类的实例在Session范围内始终没有被初始化,因此在执行customer.getName()方法时,Hibernate会抛出以下异常:
ERROR LazyInitializer:63 - Exception initializing proxynet.sf.hibernate.HibernateException:
Could not initialize proxy - the owning Session was closed
由此可见,Customer代理类的实例只有在当前Session范围内才能被初始化。

3.net.sf.hibernate.Hibernate类的initialize()静态方法用于在Session范围内显式初始化代理类实例,isInitialized()方法用于判断代理类实例是否已经被初始化。例如:
tx = session.beginTransaction();
Customer customer=(Customer)session.load(Customer.class,new Long(1));
if(!Hibernate.isInitialized(customer)) Hibernate.initialize(customer);
tx.commit();
session.close();
customer.getName();
以上代码在Session范围内通过Hibernate类的initialize()方法显式初始化了Customer代理类实例,因此当Session关闭后,可以正常访问Customer游离对象。

4.当应用程序访问代理类实例的getId()方法时,不会触发Hibernate初始化代理类实例的行为,例如:
tx = session.beginTransaction();
Customer customer=(Customer)session.load(Customer.class,new Long(1));
customer.getId();
tx.commit();
session.close();
customer.getName();
当应用程序访问customer.getId()方法时,该方法直接返回Customer代理类实例的OID值,无需查询数据库。由于引用变量 customer始终引用的是没有被初始化的Customer代理类实例,因此当Session关闭后再执行customer.getName()方法, Hibernate会抛出以下异常:
ERROR LazyInitializer:63 - Exception initializing proxynet.sf.hibernate.HibernateException:
Could not initialize proxy - the owning Session was closed

posted @ 2006-08-24 15:25 Binary 阅读(427) | 评论 (0)编辑 收藏

仅列出标题
共8页: 上一页 1 2 3 4 5 6 7 8 下一页