Java并发编程第二章.docx

上传人:b****8 文档编号:10229529 上传时间:2023-02-09 格式:DOCX 页数:34 大小:67.77KB
下载 相关 举报
Java并发编程第二章.docx_第1页
第1页 / 共34页
Java并发编程第二章.docx_第2页
第2页 / 共34页
Java并发编程第二章.docx_第3页
第3页 / 共34页
Java并发编程第二章.docx_第4页
第4页 / 共34页
Java并发编程第二章.docx_第5页
第5页 / 共34页
点击查看更多>>
下载资源
资源描述

Java并发编程第二章.docx

《Java并发编程第二章.docx》由会员分享,可在线阅读,更多相关《Java并发编程第二章.docx(34页珍藏版)》请在冰豆网上搜索。

Java并发编程第二章.docx

Java并发编程第二章

 

第2章构建线程安全应用程序

 

第二章构建线程安全应用程序.................................................................1

2.1.什么是线程安全性...........................................................................................................2

2.2.Servlet的线程安全性.......................................................................................................5

2.3.同步与互斥.......................................................................................................................9

2.3.1线程干扰...............................................................................................................9

2.3.2同步.....................................................................................................................11

2.4.同步与volatile.................................................................................................................13

2.5.活性................................................................................................................................14

2.6.ThreadLocal变量............................................................................................................15

2.7.高级并发对象.................................................................................................................19

参考文献....................................................................................................................................20

 

2.1.什么是线程安全性

 

当对一个复杂对象进行某种操作时,从操作开始到操作结束,被操作的对象往往会经历

若干非法的中间状态。

这跟外科医生做手术有点像,尽管手术的目的是改善患者的健康,但

医生把手术过程分成了几个步骤,每个步骤如果不是完全结束的话,都会严重损害患者的健

康。

想想看,如果一个医生切开患者的胸腔后要休三周假会怎么样?

与此类似,调用一个函

数(假设该函数是正确的)操作某对象常常会使该对象暂时陷入不可用的状态(通常称为不

稳定状态),等到操作完全结束,该对象才会重新回到完全可用的状态。

如果其他线程企图

访问一个处于不可用状态的对象,该对象将不能正确响应从而产生无法预料的结果,如何避

免这种情况发生是线程安全性的核心问题。

单线程的程序中是不存在这种问题的,因为在一

个线程更新某对象的时候不会有其他线程也去操作同一个对象。

(除非其中有异常,异常是可

能导致上述问题的。

当一个正在更新某对象的线程因异常而中断更新过程后,再去访问没有

完全更新的对象,会出现同样的问题)

给线程安全下定义是比较困难的。

很多正式的定义都比较复杂。

如,有这样的定义:

“一

个类在可以被多个线程安全调用时就是线程安全的”。

但是它不能帮助我们区分一个线程安

全的类与一个线程不安全的类。

实际上,所有线程安全的定义都有某种程序的循环,因为它必须符合类的规格说明——

这是对类的功能、其副作用、哪些状态是有效和无效的、不可变量、前置条件、后置条件等

等的一种非正式的松散描述(由规格说明给出的对象状态约束只应用于外部可见的状态,即那

些可以通过调用其公共方法和访问其公共字段看到的状态,而不应用于其私有字段中表示的

内部状态)[1]。

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

如果一个类实现正确(这

是说它符合规格说明的另一种方式),那么没有一种对这个类的对象的操作序列(读或者写公

共字段以及调用公共方法)可以让对象处于无效状态,观察到对象处于无效状态、或者违反类

的任何不可变量、前置条件或者后置条件的情况。

此外,一个类要成为线程安全的,在被多个线程访问时,不管运行时环境执行这些线程

有什么样的时序安排或者交错,它必须仍然有如上所述的正确行为,并且在调用的代码中没

有任何额外的同步。

其效果就是,在所有线程看来,对于线程安全对象的操作是以固定的、

全局一致的顺序发生的。

 

正确性与线程安全性之间的关系非常类似于在描述ACID(原子性、一致性、独立性和持

久性)事务时使用的一致性与独立性之间的关系:

从特定线程的角度看,由不同线程所执行的

对象操作是先后(虽然顺序不定)而不是并行执行的。

考虑下面的代码片段,它迭代一个Vector中的元素。

尽管Vector的所有方法都是同步的,

但是在多线程的环境中不做额外的同步就使用这段代码仍然是不安全的,因为如果另一个线

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

ArrayIndexOutOfBoundsException。

Vectorv=newVector();

//containsraceconditions--mayrequireexternalsynchronization

for(inti=0;i

doSomething(v.get(i));

}

这里发生的事情是:

get(index)的规格说明里有一条前置条件要求index必须是非负的

并且小于size()。

但是,在多线程环境中,没有办法可以知道上一次查到的size()值是否仍

然有效,因而不能确定i

更明确地说,这一问题是由get()的前置条件是以size()的结果来定义的这一事实所带

来的。

只要看到这种必须使用一种方法的结果作为另一种讲法的输入条件的样式,它就是一

个状态依赖,就必须保证至少在调用这两种方法期间元素的状态没有改变。

一般来说,做到

这一点的唯一方法在调用第一个方法之前是独占性地锁定对象,一直到调用了后一种方法以

后。

在上面的迭代Vector元素的例子中,您需要在迭代过程中同步Vector对象。

如上面的例子所示,线程安全性不是一个非真即假的命题。

Vector的方法都是同步的,

并且Vector明确地设计为在多线程环境中工作。

但是它的线程安全性是有限制的,即在某些

方法之间有状态依赖(类似地,如果在迭代过程中Vector被其他线程修改,那么由

Vector.iterator()返回的iterator会抛出ConcurrentModificationException)。

对于Java类中常见的线程安全性级别,没有一种分类系统可被广泛接受,不过重要的

是在编写类时尽量记录下它们的线程安全行为。

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

不可变、线程安全、有条件线程安全、

线程兼容和线程对立。

只要明确地记录下线程安全特性,那么您是否使用这种系统都没关系。

这种系统有其局限性——各类之间的界线不是百分之百地明确,而且有些情况它没照顾到,

但是这套系统是一个很好的起点。

这种分类系统的核心是调用者是否可以或者必须用外部同

步包围操作(或者一系列操作)。

下面分别描述了线程安全性的这五种类别。

 

1)不可变

不可变的对象一定是线程安全的,并且永远也不需要额外的同步。

因为一个不可变的对

象只要构建正确,其外部可见状态永远也不会改变,永远也不会看到它处于不一致的状态。

Java类库中大多数基本数值类如Integer、String和BigInteger都是不可变的。

2)线程安全

由类的规格说明所规定的约束在对象被多个线程访问时仍然有效,不管运行时环境如何

排列,线程都不需要任何额外的同步。

这种线程安全性保证是很严格的——许多类,如

Hashtable或者Vector都不能满足这种严格的定义。

3)有条件的线程安全

有条件的线程安全类对于单独的操作可以是线程安全的,但是某些操作序列可能需要外

部同步。

条件线程安全的最常见的例子是遍历由Hashtable或者Vector或者返回的迭代器

——由这些类返回的fail-fast迭代器假定在迭代器进行遍历的时候底层集合不会有变化。

了保证其他线程不会在遍历的时候改变集合,进行迭代的线程应该确保它是独占性地访问集

合以实现遍历的完整性。

通常,独占性的访问是由对锁的同步保证的——并且类的文档应该

说明是哪个锁(通常是对象的内部监视器(intrinsicmonitor))。

如果对一个有条件线程安全类进行记录,那么您应该不仅要记录它是有条件线程安全的,

而且还要记录必须防止哪些操作序列的并发访问。

用户可以合理地假设其他操作序列不需要

任何额外的同步。

4)线程兼容

线程兼容类不是线程安全的,但是可以通过正确使用同步而在并发环境中安全地使用。

这可能意味着用一个synchronized块包围每一个方法调用,或者创建一个包装器对象,其中

每一个方法都是同步的(就像Collections.synchronizedList()一样)。

也可能意味着用

synchronized块包围某些操作序列。

为了最大程度地利用线程兼容类,如果所有调用都使用

同一个块,那么就不应该要求调用者对该块同步。

这样做会使线程兼容的对象作为变量实例

包含在其他线程安全的对象中,从而可以利用其所有者对象的同步。

许多常见的类是线程兼容的,如集合类ArrayList和HashMap、

java.text.SimpleDateFormat、或者JDBC类Connection和ResultSet。

5)线程对立

线程对立类是那些不管是否调用了外部同步都不能在并发使用时安全地呈现的类。

线程

对立很少见,当类修改静态数据,而静态数据会影响在其他线程中执行的其他类的行为,这

 

时通常会出现线程对立。

线程对立类的一个例子是调用System.setOut()的类。

线程安全类(以及线程安全性程度更低的的类)可以允许或者不允许调用者锁定对象以

进行独占性访问。

Hashtable类对所有的同步使用对象的内部监视器,但是

ConcurrentHashMap类不是这样,事实上没有办法锁定一个ConcurrentHashMap对象以进

行独占性访问。

除了记录线程安全程序,还应该记录是否某些锁——如对象的内部锁——对

类的行为有特殊的意义。

通过将类记录为线程安全的(假设它确实是线程安全的),您就提供了两种有价值的服务:

您告知类的维护者不要进行会影响其线程安全性的修改或者扩展,您还告知类的用户使用它

时可以不使用外部同步。

通过将类记录为线程兼容或者有条件线程安全的,您就告知了用户

这个类可以通过正确使用同步而安全地在多线程中使用。

通过将类记录为线程对立的,您就

告知用户即使使用了外部同步,他们也不能在多线程中安全地使用这个类。

不管是哪种情况,

您都在潜在的严重问题出现之前防止了它们,而要查找和修复这些问题是很昂贵的。

一个类的线程安全行为是其规格说明中的固有部分,应该成为其文档的一部分。

因为还

没有描述类的线程安全行为的声明式方式,所以必须用文字描述。

虽然Bloch的描述类的线

程安全程度的五层系统没有涵盖所有可能的情况,但是它是一个很好的起点。

如果每一个类

都将这种线程行为的程度加入到其Javadoc中,那么可以肯定的是我们大家都会受益。

 

2.2.Servlet的线程安全性

 

Servlet/JSP默认是以多线程模式执行的,所以,在编写代码时需要非常细致地考虑多线

程的安全性问题。

然而,很多人编写Servlet/JSP程序时并没有注意到多线程安全性的问题,

这往往造成编写的程序在少量用户访问时没有任何问题,而在并发用户上升到一定值时,就

会经常出现一些莫明其妙的问题。

Servlet体系结构是建立在Java多线程机制之上的,它的生命周期是由Web容器负责的。

当客户端第一次请求某个Servlet时,Servlet容器将会根据web.xml配置文件实例化这个

Servlet类。

当有新的客户端请求该Servlet时,一般不会再实例化该Servlet类,也就是有多

个线程在使用这个实例。

Servlet容器会自动使用线程池等技术来支持系统的运行。

 

这样,当两个或多个线程同时访问同一个Servlet时,可能会发生多个线程同时访问同一

资源的情况,数据可能会变得不一致。

所以在用Servlet构建的Web应用时如果不注意线程

安全的问题,会使所写的Servlet程序有难以发现的错误。

1.无状态Servlet

下面是一个无状态的Servlet,它从Request中解包数据,然后将这两个数据进行相乘,

最后把结果封装在Response中。

importjava.io.IOException;

importjava.io.PrintWriter;

importjavax.servlet.ServletException;

importjavax.servlet.http.HttpServlet;

importjavax.servlet.http.HttpServletRequest;

importjavax.servlet.http.HttpServletResponse;

publicclassConcurrentServletextendsHttpServlet{

privatestaticfinallongserialVersionUID=1L;

publicConcurrentServlet(){

super();

}

protectedvoiddoGet(HttpServletRequestrequest,

HttpServletResponseresponse)throwsServletException,

IOException{

Strings1=request.getParameter("num1");

Strings2=request.getParameter("num2");

intresult=0;

if(s1!

=null&&s1!

=null){

result=Integer.parseInt(s1)*Integer.parseInt(s2);

}

PrintWriterout=response.getWriter();

out.print(result);

out.close();

}

}

这个Servlet是无状态的,它不包含域,也没有引用其它类的域,一次特定计算的瞬时状

 

态,会唯一的存储在本地变量中,这些本地变量存在线程的栈中,只有执行线程才能访问,

一个执行该Servlet的线程不会影响访问同一个Servlet的其它线程的计算结果,因为两个线

程不共享状态,他们如同在访问不同的实例。

因为线程访问无状态对象的行为,不会影响其它线程访问对象时的正确性,所以无状态

对象是线程安全的。

2有状态Servlet

对上面的Servlet进行修改,把result变量提升为类的实例变量。

那么这个Servlet就有状

态了。

有状态的Servlet在多线程访问时,有可能发生线程不安全性。

请看下面的代码。

importjava.io.IOException;

importjava.io.PrintWriter;

importjavax.servlet.ServletException;

importjavax.servlet.http.HttpServlet;

importjavax.servlet.http.HttpServletRequest;

importjavax.servlet.http.HttpServletResponse;

publicclassStatefulServletextendsHttpServlet{

privatestaticfinallongserialVersionUID=1L;

intresult=0;

publicStatefulServlet(){

super();

}

protectedvoiddoGet(HttpServletRequestrequest,

HttpServletResponseresponse)throwsServletException,

IOException{

Strings1=request.getParameter("num1");

Strings2=request.getParameter("num2");

if(s1!

=null&&s1!

=null){

result=Integer.parseInt(s1)*Integer.parseInt(s2);

}

try{

Thread.sleep(5000);

}catch(InterruptedExceptione){

e.printStackTrace();

}

PrintWriterout=response.getWriter();

out.print(result);

out.close();

}

}

在Servlet中定义了一个实例变量result,Servlet把它的值进行输出。

当只有一个用户访

问该Servlet时,程序会正常的运行,但当多个用户并发访问时,就可能会出现其它用户的信

 

息显示在另外一些用户的浏览器上的问题。

这是一个严重的问题。

为了突出并发问题,便于测试、观察,我们在回显用户信息时执行了一个延时的操作。

打开两个浏览器窗口,分别输入:

http:

//localhost:

8080/test/StatefulServlet?

num1=5&num2=80

http:

//localhost:

8080/test/StatefulServlet?

num1=5&num2=70。

相隔5000毫秒之内执行这两个请求,产生的结果如下图:

 

从运行结果可以看出,两个请求显示了相同的计算结果,也就是说,因为两个线程访问

了共同的有状态的Servlet,其中一个线程的计算结果覆盖了另外一个线程的计算结果。

从程

序分析可以看出第一个线程在输出result时,暂停了一段时间,那么它的值就被第二个线程

的计算结果所覆盖,两个请求输出了相同的结果。

这就是潜在的线程不安全性。

要解决线程不安全性,其中一个主要的方法就是取消Servlet的实例变量,变成无状态的

Servlet。

另外一种方法是对共享数据进行同步操作。

使用synchronized关键字能保证一次只

有一个线程可以访问被保护的区段,同步后的Servlet如下:

importjava.io.IOException;

importjava.io.PrintWriter;

importjavax.servlet.ServletException;

importjavax.servlet.http.HttpServlet;

importjavax.servlet.http.HttpServletRequest;

importjavax.servlet.http.HttpServletResponse;

publicclassStatefulServletextendsHttpServlet{

privatestaticfinallongserialVersionUID=1L;

intresult=0;

publicStatefulServlet(){

super();

}

protectedvoiddoGet(HttpServletRequestrequest,

HttpServletResponseresponse)throwsServletException,

 

IOException{

Strings1=request.getParameter("num1");

Strings2=request.getParameter("num2");

synchronized(this){

if(s1!

=null&&s1!

=null){

result=Integer.parseInt(s1)*Integer.parseInt(s2);

}

try{

Thread.sleep(5000);

}catch(InterruptedExceptione){

e.printStackTrace();

}

PrintWriterout=response.getWriter();

out.print(result);

out.close();

}

}

}

 

Servlet的线程安全问题只有在大量的并发访问时才会显现出来,并且很难发现,因此在

编写Servlet程序时要特别注意。

线程安全问题主要是由实例变量造成的,因此在Servlet中

应避免使用实例变量。

如果应用程序设计无法避免使用实例变量,那么使用同步来保护要使

用的实例变量,但为保证系统的最佳性能,应该同步可用性最小的代码路径。

 

2.3.同步与互斥

 

线程通

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 求职职场 > 简历

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1