注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

北漂的小羊

Java编程,开发者,程序员,软件开发,编程,代码。新浪微博号:IT国子监

 
 
 

日志

 
 
关于我

在这里是面向程序员的高品质IT技术学习社区,是程序员学习成长的地方。让我们更好地用技术改变世界。请关注新浪微博号: IT国子监(http://weibo.com/itguozijian)

网易考拉推荐

java并发:线程安全之编程需要注意的事  

2013-01-29 18:26:26|  分类: JAVA |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

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差。

以下来自网络:

编写线程安全的代码的核心在于,对对象状态访问的控制与管理,特别对共享的、可变的状态。

 

一般地讲,一个对象的状态就是它所包含的数据,存储在状态变量中,比如实例域或静态域。一个对象的状态可能还来自于它所依赖的其他对象,比如HashMap的状态一部分是存储在自己的对象空间之中的,但另一部分存储在许多的Map.Entry对象之间。所以一个对象的状态是指那些可被外界访问的方法所影响(改变)的数据。

 

我们讨论的线程安全性好像是关于代码的,但是我们真正要做的,是在不可控制的并发访问中如何保护共享数据。

 

一个对象是否应该是线程安全的,这取决于它是否会被多个线程访问。

 

Java中首要的同步机制是synchronized关键字,它提供了独占锁。除此之外,还有volatile变量、显示锁(Lock)、原子变量的使用。

 

在没有正确同步的情况下,如果多个线程访问了同一个变量,你的程序就存在隐患,有3种方法来修复它:

1、不要跨线程共享变量;

2、如果确实要共享,则使状态变量不可变;或

3、只能在任何访问状态变量的时候使用同步。

 

设计线程安全的类时,有三种技术很好的做到安全:封装(即尽量降低域的可访问能力)、不可变、明确的规范。

 

无状态的对象永远是线程安全的。

 

Servlet是多线程共享的,所以我们在设计Servlet不要设计状态,否则要确保这些共享的状态域的线程安全。

 

i++,自增操作不是原子性的,它包括三个动作:获取当前值,加1,写回新值。

 

Atomic类型的变量只能保证变量本身一条语句的原子性,不能这个变量多条语句一起执行的原子性。

 

synchronized方法虽然解决了线程安全性问题,但同时可能带来线程性能上的问题。

 

synchronized所获取的锁与ReentrantLock锁都是可重入锁,重入锁的实现是通过为每个锁关联一个请求计数和一个占有它的线程。当计数为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,JVM将记录锁的占有者,并具将请求计数器置为1。如果同一线程再次请求这个锁,计数将递增;每次占用线程退出同步块,计数器值将递减,直到计数器达到0,锁才被释放。

 

即然同步可以避免竞争条件,为什么不将每个方法都声明为synchronized类型?如同Vector一样,仅仅同步它每个方法,并不足以确保在Vector上执行的复合操作是原子的:

if(!vector.contains(element))

    vecotr.add(element);

虽然同步方法确保了不可分割操作的原子性,但是把多个操作整合到一个复合操作时,还是需要额外的锁。

 

锁定整个方法会导致弱并发的问题,并发性能会大大下降,幸运的是,我们可能通过缩小synchronized块的范围来维护线程安全性,很容易提升并发性。但你就应该谨慎地控制synchronized块不要过小,因你你不可以将一个原子操作分解到多个synchronized块中。不过你应该尽量从synchronized块中分离耗时的且不影响共享状态的操作。这样即使在耗时操作的执行过程中,也不会阻止其他线程访问共享状态。

 

请求与释放锁的操作需要开销,所以将synchronized埠分解得过于琐碎是不合理的。在分成多个同步块时要将耗时但又不影共享变量的操作放在同步块外调用,特别是要注意I/O与线程阻塞方法的调用,一般不要将其放在块里调用。

  评论这张
 
阅读(425)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2016