Thread Safety

来源:互联网 发布:ipad pro实用软件 编辑:程序博客网 时间:2024/05/16 12:03

1、什么是线程安全(thread safe)?
一个类要成为线程安全的类,就是在该类被多个线程访问时,不管运行环境中执行这些线程有什么样的时序安排或者交错,它仍然执行正确行为,并且在调用的代码中没有任何额外的同步。
2、什么时候考虑线程安全问题?
当一个类的实例为singleton的时候,你就要考虑该实例在调用的时候是否是线程安全的。最熟悉的例子就是servlet, 每个servlet在servlet engineer中只有一个实例。除非它实现SingleThreaded接口。所以我们一般要求在servlet中不要定义成员变量,以避免线程不安全。是不是凡是singleton的对象都不是线程安全的呢?答案是No。准确的表达应该是:只有该类中定义了有状态的成员时该类才是线程不安全的。
举个例子:

public class A{    String id;    public void process(){       print(id);       ...    }}

id是一个有状态的变量。什么是有状态,就是指每次调用该类的时候如果该id值可能存在不同的值,那么这个id就是有状态的。
我们再看看下面的例子。

public class B{    public void process(){     int i;     int j;     println(i*y);    }}

这个class B在单实例的情况下就是线程安全的。原因是:该类没有有状态的成员。i,j是局部变量,某个线程都会有自己的stack保存这些局部变量。所以对于不同线程来说,这些变量是相互不影响的。
对于存在线程不安全的类,如何避免出现线程安全问题呢?
1、采用synchronized同步。缺点就是存在堵塞问题。
2、使用ThreadLocal(实际上就是一个HashMap),这样不同的线程维护自己的对象,线程之间相互不干扰。
总结:
1、我们一般要求商业逻辑的BO为线程安全的类,这样就可以将该BO创建成一个单实例的对象,提高访问的效率。为了使BO为线程安全的对象,我们所要做的很简单,就是该类中不要有与状态相关的成员变量。

Servlet线程安全

Thread Safe in DispatcherServlet and Controllers

Any implementation of the Controller interface should be a reusable, thread-safe class, capable of handling multiple HTTP requests throughout the lifecycle of an application. To be able to configure Controller in an easy way, Controllers are usually JavaBeans. more …

Spring并发访问的线程安全性问题

Must Spring MVC Classes be Thread-Safe

SPRING MVC: HOW TO BUILD A THREAD-SAFE CONTROLLER

Spring MVC Controller单例陷阱

Thread-safe webapps using Spring

java线程安全总结
http://www.iteye.com/topic/806990
http://www.iteye.com/topic/808550

Spring MVC, Spring做项目时,习惯性分成三层(Controller、Service 和 Dao),通常这三层的实例是单例模式(singleton),当然也可以是prototype。
问题:
1. 为什么要将这三层对象设置成单例模式(singleton)呢?
单例好处:避免频繁的创建销毁对象,可以提高性能。在web中,有成千上万的请求来访问系统,尤其是高并发要求比较严格的系统。如果设置成prototype,每一个请求都会创建新的controller对象,成千上万请求,后果可想而知。
2. 怎么避免线程安全(Thread-Safe)问题?
使用prototype不引起线程安全问题,但是性能受影响。使用singleton可能会引起线程安全问题,取决怎么使用,比如在singleton的成员变量是有状态的bean,当多个线程访问该成员变量时,就会引起线程安全问题。如果单例的成员变量时有状态的bean,那么可以说该单例是一个有状态的单例。

一般来说,单例应该设计成一个没有状态的,是多线程可以访问的,并且是线程安全的。
1. 变量声明:有状态和无状态
2. ThreadLocal
有状态 VS 无状态 bean

首先要搞清楚的是线程的共享资源,共享资源是多线程中每个线程都要访问的类变量或实例变量,共享资源可以是单个类变量或实例变量,也可以是一组类变量或实例变量。

在Java的server side开发过程中,线程安全(Thread Safe)是一个尤为突出的问题。因为容器,如Servlet、EJB等一般都是多线程运行的。虽然在开发过程中,我们一般不考虑这些问题,但诊断问题 (Robust),程序优化(Performance),我们必须深入它们。

在Java里,线程安全一般体现在两个方面:
1、多个thread对同一个java实例的访问(read和modify)不会相互干扰,它主要体现在关键字synchronized。如 ArrayList和Vector,HashMap和Hashtable(后者每个方法前都有synchronized关键字)。如果你在 interator一个List对象时,其它线程remove一个element,问题就出现了。
2、每个线程都有自己的字段,而不会在多个线程之间共享。它主要体现在java.lang.ThreadLocal类,而没有Java关键字支持,如像static、transient那样。

实践:
当客户端请求一个Servlet时,该Servlet实例化,然后调用Servlet中的init方法,进行初始化;当其他客户端对该Servlet再进行请求时,发现Servlet并没有实例化,init方法再没有调用;当该servlet长时间没有访问时,就会自动调用destroy方法,说面销毁该对象了。

Servlet在应用服务器中只有一个实例(在Tomcat中是这样,其他的应用服务器可能有不同的实现),而这个实例会被许多个线程并发调用。也就是说,Servlet 运行是多线程的,而应用服务器并不会为每个线程都创建一个Servlet实例。— 单例

所以,Web容器默认采用单实例(单Servlet实例)多线程的方式来处理Http请求。这种处理方式能够减少新建Servlet实例的开销,从而缩短了对Http请求的响应时间。但是,这样的处理方式会导致变量访问的线程安全问题。也就是说,Servlet对象并不是一个线程安全的对象。

Servlet线程安全
在传统的Web开发中,我们处理Http请求最常用的方式是通过实现Servlet对象来进行Http请求的响应。Servlet是J2EE的重要标准之一,规定了Java如何响应Http请求的规范。通过HttpServletRequest和HttpServletResponse对象,我们能够轻松地与Web容器交互。

当Web容器收到一个Http请求时,Web容器中的一个主调度线程会从事先定义好的线程池中分配一个当前工作线程,将请求分配给当前的工作线程,由该线程来执行对应Servlet对象中的service方法。如果这个工作线程正在执行的时候,Web容器收到另外一个请求,主调度线程会同样从线程池中选择另一个工作线程来服务新的请求。Web容器本身并不关心这个新的请求是否访问的是同一个Servlet实例。
因此,我们可以得出一个结论:对于同一个Servlet对象的多个请求,Servlet的service方法将在一个多线程的环境中并发执行。

Servlet中的service方法:
service() 方法是 Servlet 的核心。每当一个客户请求一个HttpServlet 对象,该对象的service() 方法就要被调用,而且传递给这个方法一个“请求”(ServletRequest)对象和一个“响应”(ServletResponse)对象作为参数。

servlet容器把所有请求发送到该方法,该方法默认行为是转发http请求到doXXX方法中,如果你重载了该方法,默认操作被覆盖,不再进行转发操作!

不管是post还是get方法提交过来的连接,都会在service中处理,然后,由service来交由相应的doPost或doGet方法处理,如果你重写了service方法,就不会再处理doPost或doGet了,如果重写service方法,最好加上super.service(HttpServletRequest req, HttpServletResponse resp)。

结论:service方法是访问servlet的接口,任何请求servlet的http请求都是从service方法进入的,然后再根据客户端的提交方式,决定使用doGet或doPost。一开始以为请求的servlet是直接调用doXXXX方法的,现在明白了,原来是从service方法入手的。
可以这样理解service方法做的工作:
public void service(request,response) {
if request.getMethod().equals( “POST “) {
doPost(request,response);
}else if request.getMethod().equals( “GET “) {
doGet(request,response);
}
}

注意:一般会在service()中实现对父类方法的同参数表重载,即写上super.service(req, resp);即使有了service()方法,也要把doGet()和doPost()方法标识出来!

Servlet线程安全实验:
要实现的效果:首先,我想保证产生的counter都是顺序执行的。
代码如下:
public class ThreadSafeServlet extends HttpServlet {
private int counter = 0;
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 输出当前Servlet的信息以及当前线程的信息
System.out.println(this + “:” + Thread.currentThread());
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getId() + “===Counter = “+ counter);
try {
Thread.sleep(500);
counter++;
} catch (InterruptedException exc) {
}
}
}

protected void service(HttpServletRequest req, HttpServletResponse resp)        throws javax.servlet.ServletException, java.io.IOException {    System.out.println("====enter service=====" + this);    super.service(req, resp);}

}

分别使用两种不同的浏览器同时访问该servlet,访问地址如下:
http://localhost:8080/ServletStudy/servlet/ThreadSafeServlet

后台打印效果,如下:
====enter service=====servlet.threadsafe.ThreadSafeServlet@bf015
servlet.threadsafe.ThreadSafeServlet@bf015:Thread[http-8080-1,5,main]
20===Counter = 0
20===Counter = 1
====enter service=====servlet.threadsafe.ThreadSafeServlet@bf015
servlet.threadsafe.ThreadSafeServlet@bf015:Thread[http-8080-2,5,main]
21===Counter = 1
20===Counter = 2
21===Counter = 3
20===Counter = 4
21===Counter = 5
20===Counter = 6
21===Counter = 7
21===Counter = 9

通过上面的输出,我们可以得出以下三个Servlet对象的运行特性:
1. Servlet对象是一个无状态的单例对象(Singleton),因为我们看到多次请求的this指针所打印出来的hashcode值都相同
2. Servlet在不同的线程(线程池)中运行,如http-8080-1,5,main和http-8080-2,5,main等输出值可以明显区分出不同的线程执行了同一段Servlet逻辑代码。
3. Counter变量在不同的线程中共享,而且它的值被不同的线程修改,输出时已经不是顺序输出。也就是说,其他的线程会篡改当前线程中实例变量的值,针对这些对象的访问不是线程安全的。

问题:
使用同一个了浏览器进行多次访问同一个servlet时,发现都是同一个线程?难道web容器根据session来判断,然后进行线程的分配,如果session相同的话,就是用同一个线程。

怎么样保证counter按照顺序输出呢?那么就要使用同步。

对代码的修改如下:
声明:private String mux = “”;
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getId() + “===Counter = “+ counter);
try {
Thread.sleep(500);
counter++;
} catch (InterruptedException exc) {

    }}

}

执行效果如下:
====enter service=====servlet.threadsafe.ThreadSafeServlet@166ce5f
servlet.threadsafe.ThreadSafeServlet@166ce5f:Thread[http-8080-1,5,main]
20===Counter = 0
====enter service=====servlet.threadsafe.ThreadSafeServlet@166ce5f
servlet.threadsafe.ThreadSafeServlet@166ce5f:Thread[http-8080-2,5,main]
20===Counter = 1
20===Counter = 2
20===Counter = 3
20===Counter = 4
21===Counter = 5
21===Counter = 6
21===Counter = 7
21===Counter = 8
21===Counter = 9

总结:
在传统的基于Servlet的开发模式中,Servlet对象内部的实例变量不是线程安全的。在多线程环境中,这些变量的访问需要通过特殊的手段进行访问控制。解决线程安全访问的方法很多,比较容易想到的一种方案是使用同步机制,但是出于对Web应用效率的考虑,这种机制在Web开发中的可行性很低,也违背了Servlet的设计初衷。因此,我们需要另辟蹊径来解决这一困扰我们的问题。解决Servlet中的线程安全问题:尽量将你的变量定义为局部变量,不要将变量变成全局变量。
补充:
先说明几个概念:
工作者线程Work Thread:执行代码的一组线程
调度线程Dispatcher Thread:每个线程都具有分配给它的线程优先级,线程是根据优先级调度执行的。

Servlet采用多线程来处理多个请求同时访问。servlet依赖于一个线程池来服务请求。线程池实际上是一系列的工作者线程集合。Servlet使用一个调度线程来管理工作者线程。

当容器收到一个Servlet请求,调度线程从线程池中选出一个工作者线程,将请求传递给该工作者线程,然后由该线程来执行Servlet的service 方法。当这个线程正在执行的时候,容器收到另外一个请求,调度线程同样从线程池中选出另一个工作者线程来服务新的请求,容器并不关心这个请求是否访问的是同一个Servlet。当容器同时收到对同一个Servlet的多个请求的时候,那么这个Servlet的service()方法将在多线程中并发执行。
Servlet容器默认采用单实例多线程的方式来处理请求,这样减少产生Servlet实例的开销,提升了对请求的响应时间,对于Tomcat可以在server.xml中通过元素设置线程池中线程的数目。

就实现来说:
调度者线程类所担负的责任如其名字,该类的责任是调度线程,只需要利用自己的属性完成自己的责任。所以该类是承担了责任的,并且该类的责任又集中到唯一的单体对象中。

而其他对象又依赖于该特定对象所承担的责任,我们就需要得到该特定对象。那该类就是一个单例模式的实现了。

“servlet 可以同时处理多个请求”
当多个request同时来请求一个servlet时,tomcat的工作原理是会对这多个请求分别创建线程,但是每个线程拿到的servlet实例是同一个servlet实例(单例模式),这样的话他们在使用service方法时就会可能出现同时使用,所以如果有需要更改实例状态(共享成员变量的)语句,就要加上锁-synchronized关键字。

1,变量的线程安全:这里的变量指字段和共享数据(如表单参数值)。
a,将参数变量本地化。多线程并不共享局部变量.所以我们要尽可能的在servlet中使用局部变量。例如:String user = “”;user = request.getParameter(“user”);
b,使用同步块Synchronized,防止可能异步调用的代码块。这意味着线程需要排队处理。
在使用同板块的时候要尽可能的缩小同步代码的范围,不要直接在sevice方法和响应方法上使用同步,这样会严重影响性能。

2,属性的线程安全:ServletContext,HttpSession,ServletRequest对象中属性
ServletContext:(线程是不安全的)
ServletContext是可以多线程同时读/写属性的,线程是不安全的。要对属性的读写进行同步处理或者进行深度Clone()。
所以在Servlet上下文中尽可能少量保存会被修改(写)的数据,可以采取其他方式在多个Servlet中共享,比方我们可以使用单例模式来处理共享数据。

HttpSession:(线程是不安全的)
HttpSession对象在用户会话期间存在,只能在处理属于同一个Session的请求的线程中被访问,因此Session对象的属性访问理论上是线程安全的。
当用户打开多个同属于一个进程的浏览器窗口,在这些窗口的访问属于同一个Session,会出现多次请求,需要多个工作线程来处理请求,可能造成同时多线程读写属性。
这时我们需要对属性的读写进行同步处理:使用同步块Synchronized和使用读/写器来解决。

ServletRequest:(线程是安全的)
对于每一个请求,由一个工作线程来执行,都会创建有一个新的ServletRequest对象,所以ServletRequest对象只能在一个线程中被访问。ServletRequest是线程安全的。
注意:ServletRequest对象在service方法的范围内是有效的,不要试图在service方法结束后仍然保存请求对象的引用。

3,使用同步的集合类:
使用Vector代替ArrayList,使用Hashtable代替HashMap。

4,不要在Servlet中创建自己的线程来完成某个功能。
Servlet本身就是多线程的,在Servlet中再创建线程,将导致执行情况复杂化,出现多线程安全问题。

5,在多个servlet中对外部对象(比方文件)进行修改操作一定要加锁,做到互斥的访问。
6,javax.servlet.SingleThreadModel接口是一个标识接口,如果一个Servlet实现了这个接口,那Servlet容器将保证在一个时刻仅有一个线程可以在给定的servlet实例的service方法中执行。将其他所有请求进行排队。

服务器可以使用多个实例来处理请求,代替单个实例的请求排队带来的效益问题。服务器创建一个Servlet类的多个Servlet实例组成的实例池,对于每个请求分配Servlet实例进行响应处理,之后放回到实例池中等待下此请求。这样就造成并发访问的问题。
此时,局部变量(字段)也是安全的,但对于全局变量和共享数据是不安全的,需要进行同步处理。而对于这样多实例的情况SingleThreadModel接口并不能解决并发访问问题。

解决servlet线程安全的方法:

1、实现 SingleThreadModel 接口
该接口指定了系统如何处理对同一个Servlet的调用。如果一个Servlet被这个接口指定,那么在这个Servlet中的service方法将不会有两个线程被同时执行,当然也就不存在线程安全的问题。这种方法只要继承这个接口就行了

public class XXXXX extends HttpServlet implements SingleThreadModel {
…………
}

2、同步对共享数据的操作
使用synchronized 关键字能保证一次只有一个线程可以访问被保护的区段,在本论文中可以通过同步块操作来保证Servlet的线程安全。同步后的代码如下:

Public class XXXXXX extends HttpServlet {
…………
synchronized (this){XXXX}

}

3、避免使用实例变量
线程安全问题还有些是由实例变量造成的,只要在Servlet里面的任何方法里面都不使用实例变量,那么该Servlet就是线程安全的。

对上面的三种方法进行测试,可以表明用它们都能设计出线程安全的Servlet程序。但是,如果一个Servlet实现了 SingleThreadModel接口,Servlet引擎将为每个新的请求创建一个单独的Servlet实例,这将引起大量的系统开销。 SingleThreadModel在Servlet2.4中已不再提倡使用;同样如果在程序中使用同步来保护要使用的共享的数据,也会使系统的性能大大 下降。这是因为被同步的代码块在同一时刻只能有一个线程执行它,使得其同时处理客户请求的吞吐量降低,而且很多客户处于阻塞状态。另外为保证主存内容和线程的工作内存中的数据的一致性,要频繁地刷新缓存,这也会大大地影响系统的性能。所以在实际的开发中也应避免或最小化Servlet 中的同步代码;在servlet中避免使用实例变量是保证Servlet线程安全的最佳选择。从Java 内存模型也可以知道,方法中的临时变量是在栈上分配空间,而且每个线程都有自己私有的栈空间,所以它们不会影响线程的安全。

小结
Servlet的线程安全问题只有在大量的并发访问时才会显现出来,并且很难发现,因此在编写Servlet程序时要特别注意。线程安全问题主要是由实例变量造成的,因此在 Servlet中应避免使用实例变量。如果应用程序设计无法避免使用实例变量,那么使用同步来保护要使用的实例变量,但为保证系统的最佳性能,应该同步可用性最小的代码路径。

0 0
原创粉丝点击