《Java并发编程实践》学习笔记之二:线程安全性(thread-safe)

 

《Java并发编程实践》学习笔记之二:线程安全性(thread-safe)


1、什么是线程安全性


1.1 不可用状态

调用一个函数(假设该函数是正确的)操作某对象常常会使该对象暂时陷入不可用的状态(通常称为不稳定状态),等到操作完全结束,该对象才会重新回到完全可用的状态。

1.2 线程安全性的核心问题

如果其他线程企图访问一个处于不可用状态的对象,该对象将不能正确响应从而产生无法预料的结果,如何避免这种情况发生是线程安全性的核心问题。

单线程的程序中是不存在这种问题的,除非有异常发生。

1.3 线程安全的定义

给线程安全下定义比较困难。存在很多种定义,如:“一个类在可以被多个线程安全调用时就是线程安全的”。

实际上,所有线程安全的定义都有某种程序的循环,因为它必须符合类的规格说明 ——这是对类的功能、其副作用、哪些状态是有效和无效的、不可变量、前置条件、后置条件等等的一种非正式的松散描述。

类要成为线程安全的,首先必须在单线程环境中有正确的行为。

正确性与线程安全性之间的关系非常类似于在描述 ACID(原子性、一致性、独立性和持久性)事务时使用的一致性与独立性之间的关系:从特定线程的角度看,由不同线程所执行的对象操作是先后(虽然顺序不定)而不是并行执行的。

我们都知道,Vector的所有方法都是同步的,然而,尽管如此,在多线程环境下有些时候不进行额外的同步仍然是不安全的。

考虑下面代码:
    Vector v = new Vector(); 
    // contains race conditions -- may require external synchronization 
    for (int i=0; i<v.size(); i++) { 
      doSomething(v.get(i)); 
    }

如果另一个线程恰好在错误的时间里删除了一个元素,则get()会抛出一个ArrayIndexOutOfBoundsException。

这里发生的事情是:get(index)的规格说明里有一条前置条件要求index必须是非负的并且小于size()。但是,在多线程环境中,没有办法可以知道上一次查到的size()值是否仍然有效,因而不能确定i<size(),除非在上一次调用了size()后独占地锁定Vector。

更明确地说,这一问题是由 get() 的前置条件是以 size() 的结果来定义的这一事实所带来的。只要看到这种必须使用一种方法的结果作为另一种讲法的输入条件的样式,它就是一个状态依赖,就必须保证至少在调用这两种方法期间元素的状态没有改变。一般来说,做到这一点的唯一方法在调用第一个方法之前是独占性地锁定对象,一直到调用了后一种方法以后。

2、Java类的线程安全级别


Bloch给出的描述五类线程安全性的分类方法。

2.1 不可变

不可变的对象一定是线程安全的,并且永远也不需要额外的同步。因为一个不可变的对象只要构建正确,其外部可见状态永远也不会改变,永远也不会看到它处于不一致的状态。Java  类库中大多数基本数值类如 Integer、String和 BigInteger都是不可变的。

2.2 线程安全

由类的规格说明所规定的约束在对象被多个线程访问时仍然有效,不管运行时环境如何排列,线程都不需要任何额外的同步。这种线程安全性保证是很严格的——许多类,如Hashtable或者Vector都不能满足这种严格的定义。

2.3 有条件的线程安全

有条件的线程安全类对于单独的操作可以是线程安全的,但是某些操作序列可能需要外部同步。最常见的例子是遍历由Hashtable或者Vector或者返回的迭代器——由这些类返回的fail-fast迭代器假定在迭代器进行遍历的时候底层集合不会有变化。为了保证其他线程不会在遍历的时候改变集合,进行迭代的线程应该确保它是独占性地访问集合以实现遍历的完整性。通常,独占性的访问是由对锁的同步保证的——并且类的文档应该说明是哪个锁(通常是对象的内部监视器(intrinsic monitor))。

2.4 线程兼容

线程兼容类不是线程安全的,但是可以通过正确使用同步而在并发环境中安全地使用。这可能意味着用一个synchronized块包围每一个方法调用,或者创建一个包装器对象,其中每一个方法都是同步的(就像Collections.synchronizedList()一样)。也可能意味着用synchronized块包围某些操作序列。

常见类:ArrayList、HashMap、SimpleDateFormat、Connection和ResultSet等。

2.5 线程对立

线程对立类是那些不管是否调用了外部同步都不能在并发使用时安全地呈现的类。线程对立很少见,当类修改静态数据,而静态数据会影响在其他线程中执行的其他类的行为,这时通常会出现线程对立。

3、记录线程安全级别的好处


3.1 记录线程安全

通过将类记录为线程安全的(假设它确实是线程安全的),您就提供了两种有价值的服务:您告知类的维护者不要进行会影响其线程安全性的修改或者扩展,您还告知类的用户使用它时可以不使用外部同步。

3.2 记录有条件线程安全或线程兼容

通过将类记录为线程兼容或者有条件线程安全的,您就告知了用户这个类可以通过正确使用同步而安全地在多线程中使用。

3.3 线程对立

通过将类记录为线程对立的,您就告知用户即使使用了外部同步,他们也不能在多线程中安全地使用这个类。

知道了线程安全级别,使用时就可以很好的预防严重问题的出现。

注意:一个类的线程安全行为是其规格说明中的固有部分,应该成为其文档的一部分。因为还没有描述类的线程安全行为的声明式方式,所以必须用文字描述。

4、Servlet的线程安全性


Servlet/JSP 默认是以多线程模式执行的。Servlet 体系结构是建立在 Java 多线程机制之上的,它的生命周期是由 Web 容器负责的。当客户端第一次请求某个 Servlet 时,Servlet  容器将会根据 web.xml 配置文件实例化这个Servlet 类。当有新的客户端请求该 Servlet 时,一般不会再实例化该 Servlet 类,也就是有多个线程在使用这个实例。Servlet 容器会自动使用线程池等技术来支持系统的运行。这样,当两个或多个线程同时访问同一个 Servlet时,可能会发生多个线程同时访问同一资源的情况,数据可能会变得不一致。

4.1 无状态Servlet

当Servlet不包含域(成员变量),也没有引用其他类的域,使用的只是局部变量,而局部变量是保存在线程栈中的(各个线程有自己一份)。因而无状态Servlet是线程安全的。

4.2 有状态Servlet

书中举了一个例子,接收两个参数(request中的),计算和(result)。无状态时,result是局部变量,现在提升为实例变量。这样,多用户访问时,有可能就会出现自己的结果显示在别人浏览器中的情况。

解决这种线程不安全性,其中一个主要的方法就是取消 Servlet的实例变量,变成无状态的Servlet;另外一种方法是对共享数据进行同步操作。使用synchronized关键字能保证一次只有一个线程可以访问被保护的区段。

线程安全问题主要是由实例变量造成的,因此在 Servlet 中应避免使用实例变量。如果应用程序设计无法避免使用实例变量,那么使用同步来保护要使用的实例变量,但为保证系统的最佳性能,应该同步可用性最小的代码。

5、补充:Struts1.x与Struts2的线程安全性


5.1 Struts1.x的线程安全性

经过对struts1.x源码的研读发现:
struts1.x获取action的方式是单例的,所有的action都被维护在一个hashMap里,当有请求到达时,先根据action的名称去hashMap里查找要请求的Action是否已经存在,如果存在,则直接返回hashMap里的action。如果不存在,则创建一个新的Action实例。这与Servelt是类似的。

因而,Action类中不应该声明带有状态的实例变量(与Servlet类似),而应该使用ActionForm,因为ActionForm是通过参数形式传入action的,不存在共享变量的问题,其实每个request产生的ActionForm实例也是不同的。

在Struts1.x与Spring集成时,配置Action的Bean时,scope可以不配,因为默认为“singleton”。经过Polaris测试发现,尽管Struts1.x内部对Action实例的产生是“单例模式”,然而,如果将其交由Spring管理,其实例数量却是由Spring的scope决定的。可以通过在Action中打印this来测试scope为singleton与prototype时的不同:singleton时,只产生一个实例;为prototype时,每个请求产生产生一个实例。集成的时候,建议Action的Bean不配scope或配成singleton,以利用Struts1自身提供的线程模式,以获得最大性能或资源利用率。

在此大概说一下Spring中singleton与prototype的不同:

当spring容器中管理bean属性为singleton时,spring容器会管理该bean整个生命周期;当bean的作用域为prototype时,每次调用到该bean都相当于重新new了一次,new出来的对象 如果没有引用,就会被JVM垃圾回收机制回收的。虽然都说spring是容器,的确没错,但是这人为了形象的描述它能带来的功能,其实它的管理不管理生命周期,其实就看它保存没保存这个对象的引用,虽然singleton是spring管理的,但它在spring容器结束的时候,spring也就是让这个引用指向一个空对象而已。

5.2 Struts2的线程安全性

 Struts 2 的 Action 对象为每一个请求产生一个实例,因此,虽然在Action中定义了很多全局变量,也不存在线程安全问题。

Struts 2框架在处理每一个用户请求的时候,都建立一个单独的线程进行处理,值栈ValueStack也是伴随着局部线程而存在的。在该线程存在过程中,可以随意访问值栈,这就保证了值栈的安全性。

在Struts 2中,ActionContext(数据环境)是一个局部线程,这就意味着每个线程中的ActionContext内容都是唯一的。所以开发者不用担心Action的线程安全。

在Struts2与Spring集成时,配置Action的Bean时一定记得加上scope属性,值为:prototype,否则会有线程安全问题。

5.3 Struts1.x与Struts2的性能问题

Struts1.x的单例策略造成了一定的限制,开发时要注意线程安全性问题。

Struts2是线程安全的,据说,Servlet容器会给每一个请求产生许多丟弃的对象,并且不会导致性能和垃圾回收问题。Polaris没有测试,有兴趣的您可以试试。不过,Polaris认为Apache放弃Struts1的更新,转向Struts2,性能方面应该不会比Struts1差。

想了解更多、更详细的Struts1.x与Struts2的区别,请访问: http://tech.ddvip.com/2008-12/122852732297849.html

 

你可能感兴趣的:(java,spring,多线程,thread,编程)