Servlet体系结构是建立在Java多线程机制之上的它的生命周期是由Web容器负责的当客户端第一次请求某个Servlet 时Servlet容器将会根据webxml配置文件实例化这个Servlet类当有新的客户端请求该Servlet时一般不会再实例化该 Servlet类也就是有多个线程在使用这个实例 这样当两个或多个线程同时访问同一个Servlet时可能会发生多个线程同时访问同一资源的情况数据可能会变得不一致所以在用Servlet构建的Web应用时如果不注意线程安全的问题会使所写的Servlet程序有难以发现的错误
实例变量不正确的使用是造成Servlet线程不安全的主要原因下面针对该问题给出了三种解决方案并对方案的选取给出了一些参考性的建议
实现 SingleThreadModel 接口
该接口指定了系统如何处理对同一个Servlet的调用如果一个Servlet被这个接口指定那么在这个Servlet中的service方法将不会有两个线程被同时执行当然也就不存在线程安全的问题这种方法只要将前面的Concurrent Test类的类头定义更改为
Public class Concurrent Test extends HttpServlet implements SingleThreadModel {
…………
}
同步对共享数据的操作
使用synchronized 关键字能保证一次只有一个线程可以访问被保护的区段在本论文中的Servlet可以通过同步块操作来保证线程的安全同步后的代码如下
…………
Public class Concurrent Test extends HttpServlet { …………
Username = requestgetParameter (username);
Synchronized (this){
Output = responsegetWriter ();
Try {
Thread Sleep ();
} Catch (Interrupted Exception e){}
outputprintln(用户名:+Username+
);
}
}
}
避免使用实例变量
本实例中的线程安全问题是由实例变量造成的只要在Servlet里面的任何方法里面都不使用实例变量那么该Servlet就是线程安全的
修正上面的Servlet代码将实例变量改为局部变量实现同样的功能代码如下
……
Public class Concurrent Test extends HttpServlet {public void service (HttpServletRequest request HttpServletResponse
Response) throws ServletException IOException {
Print Writer output;
String username;
ResponsesetContentType (text/html; charset=gb);
……
}
}
对上面的三种方法进行测试可以表明用它们都能设计出线程安全的Servlet程序但是如果一个Servlet实现了 SingleThreadModel接口Servlet引擎将为每个新的请求创建一个单独的Servlet实例这将引起大量的系统开销 SingleThreadModel在Servlet中已不再提倡使用同样如果在程序中使用同步来保护要使用的共享的数据也会使系统的性能大大下降这是因为被同步的代码块在同一时刻只能有一个线程执行它使得其同时处理客户请求的吞吐量降低而且很多客户处于阻塞状态另外为保证主存内容和线程的工作内存中的数据的一致性要频繁地刷新缓存这也会大大地影响系统的性能所以在实际的开发中也应避免或最小化 Servlet 中的同步代码在Serlet中避免使用实例变量是保证Servlet线程安全的最佳选择从Java 内存模型也可以知道方法中的临时变量是在栈上分配空间而且每个线程都有自己私有的栈空间所以它们不会影响线程的安全
补充
servlet存在的多线程问题
实例变量: 实例变量是在堆中分配的并被属于该实例的所有线程共享所以不是线程安全的
JSP系统提供的个类变量:
JSP中用到的OUTREQUESTRESPONSESESSIONCONFIGPAGEPAGECONXT是线程安全的APPLICATION在整个系统内被使用所以不是线程安全的
局部变量: 局部变量在堆栈中分配因为每个线程都有它自己的堆栈空间所以是线程安全的
静态类: 静态类不用被实例化就可直接使用也不是线程安全的
外部资源: 在程序中可能会有多个线程或进程同时操作同一个资源(如:多个线程或进程同时对一个文件进行写操作)
此时也要注意同步问题 使它以单线程方式执行这时仍然只有一个实例所有客户端的请求以串行方式执行这样会降低系统的性能
对于存在线程不安全的类如何避免出现线程安全问题:
采用synchronized同步缺点就是存在堵塞问题
使用ThreadLocal(实际上就是一个HashMap)这样不同的线程维护自己的对象线程之间相互不干扰
ThreadLocal的设计
首先看看ThreadLocal的接口
Object get() ; // 返回当前线程的线程局部变量副本 protected Object
initialValue(); // 返回该线程局部变量的当前线程的初始值
void set(Object value); // 设置当前线程的线程局部变量副本的值
ThreadLocal有个方法其中值得注意的是initialValue()该方法是一个protected
的方法显然是为了子类重写而特意实现的该方法返回当前线程在该线程局部变量的初始值这个方法是一个延迟调用方法在一个线程第次调用get()或者set(Object)时才执行并且仅执行次ThreadLocal中的确实实现直接返回一个null
protected Object initialValue() { return null; }
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单在ThreadLocal类中有一个Map用于存储每一个线程的变量的副本比如下面的示例实现
public class ThreadLocal
{
private Map values = CollectionssynchronizedMap(new HashMap());
public Object get()
{
Thread curThread = ThreadcurrentThread();
Object o = valuesget(curThread);
if (o == null && !ntainsKey(curThread))
{
o = initialValue();
valuesput(curThread o);
}
return o;
}
public void set(Object newValue)
{
valuesput(ThreadcurrentThread() newValue);
}
public Object initialValue()
{
return null;
}
}
当然这并不是一个工业强度的实现但JDK中的ThreadLocal的实现总体思路也类似于此
ThreadLocal的使用
如果希望线程局部变量初始化其它值那么需要自己实现ThreadLocal的子类并重写该方法通常使用一个内部匿名类对ThreadLocal进行子类化比如下面的例子SerialNum类为每一个类分配一个序号
public class SerialNum
{
// The next serial number to be assigned
private static int nextSerialNum = ;
private static ThreadLocal serialNum = new ThreadLocal()
{
protected synchronized Object initialValue()
{
return new Integer(nextSerialNum++);
}
};
public static int get()
{
return ((Integer) (serialNumget()))intValue();
}
}
SerialNum类的使用将非常地简单因为get()方法是static的所以在需要获取当前线程的序号时简单地调用
int serial = SerialNumget(); 即可
在线程是活动的并且ThreadLocal对象是可访问的时该线程就持有一个到该线程局部变量副本的隐含引用当该线程运行结束后该线程拥有的所以线程局部变量的副本都将失效并等待垃圾收集器收集
ThreadLocal与其它同步机制的比较
ThreadLocal和其它同步机制相比有什么优势呢?ThreadLocal和其它所有的同步机制都是为了解决多线程中的对同一变量的访问沖突在普通的同步机制中是通过对象加锁来实现多个线程对同一变量的安全访问的这时该变量是多个线程共享的使用这种同步机制需要很细致地分析在什么时候对变量进行读写什么时候需要锁定某个对象什么时候释放该对象的锁等等很多所有这些都是因为多个线程共享了资源造成的ThreadLocal就从另一个角度来解决多线程的并发访问ThreadLocal会为每一个线程维护一个和该线程绑定的变量的副本从而隔离了多个线程的数据每一个线程都拥有自己的变量副本从而也就没有必要对该变量进行同步了ThreadLocal提供了线程安全的共享对象在编写多线程代码时可以把不安全的整个变量封装进ThreadLocal或者把该对象的特定于线程的状态封装进ThreadLocal
由于ThreadLocal中可以持有任何类型的对象所以使用ThreadLocal get当前线程的值是需要进行强制类型转换但随着新的Java版本()将模版的引入新的支持模版参数的ThreadLocal<T>类将从中受益也可以减少强制类型转换并将一些错误检查提前到了编译期将一定程度地简化ThreadLocal的使用
总结
当然ThreadLocal并不能替代同步机制两者面向的问题领域不同同步机制是为了同步多个线程对相同资源的并发访问是为了多个线程之间进行通信的有效方式而ThreadLocal是隔离多个线程的数据共享从根本上就不在多个线程之间共享资源(变量)这样当然不需要对多个线程进行同步了所以如果你需要进行多个线程之间进行通信则使用同步机制如果需要隔离多个线程之间的共享沖突可以使用ThreadLocal这将极大地简化你的程序使程序更加易读简洁
ThreadLocal常见用途
存放当前session用户
存放一些context变量比如webwork的ActionContext
存放session比如Spring hibernate orm的session
例子用 ThreadLocal 实现每线程 Singleton
线程局部变量常被用来描绘有状态单子(Singleton) 或线程安全的共享对象或者是通过把不安全的整个变量封装进 ThreadLocal或者是通过把对象的特定于线程的状态封装进 ThreadLocal例如在与数据库有紧密联系的应用程序中程序的很多方法可能都需要访问数据库在系统的每个方法中都包含一个 Connection 作为参数是不方便的 — 用单子来访问连接可能是一个虽然更粗糙但却方便得多的技术然而多个线程不能安全地共享一个 JDBC Connection如清单 所示通过使用单子中的 ThreadLocal我们就能让我们的程序中的任何类容易地获取每线程 Connection 的一个引用这样我们可以认为 ThreadLocal 允许我们创建每线程单子
例把一个 JDBC 连接存储到一个每线程 Singleton 中
public class ConnectionDispenser {
private static class ThreadLocalConnection extends ThreadLocal {
public Object initialValue() {
return DriverManagergetConnection(ConfigurationSingletongetDbUrl());
}
}
private ThreadLocalConnection conn = new ThreadLocalConnection();
public static Connection getConnection() {
return (Connection) connget();
}
}
注意
理论上来说ThreadLocal是的确是相对于每个线程每个线程会有自己的ThreadLocal但是上面已经讲到一般的应用服务器都会维护一套线程池因此不同用户访问可能会接受到同样的线程因此在做基于TheadLocal时需要谨慎避免出现ThreadLocal变量的缓存导致其他线程访问到本线程变量
一servlet容器如何同时处理多个请求
Servlet采用多线程来处理多个请求同时访问Servelet容器维护了一个线程池来服务请求
线程池实际上是等待执行代码的一组线程叫做工作者线程(Worker Thread)Servlet容器使用一个调度线程来管理工作者线程(Dispatcher Thread)
当容器收到一个访问Servlet的请求调度者线程从线程池中选出一个工作者线程将请求传递给该线程然后由该线程来执行Servlet的service方法
当这个线程正在执行的时候容器收到另外一个请求调度者线程将从池中选出另外一个工作者线程来服务新的请求容器并不关系这个请求是否访问的是同一个Servlet还是另外一个Servlet
当容器同时收到对同一Servlet的多个请求那这个Servlet的service方法将在多线程中并发的执行
二Servlet容器默认采用单实例多线程的方式来处理请求这样减少产生Servlet实例的开销提升了对请求的响应时间对于Tomcat可以在serverxml中通过<Connector>元素设置线程池中线程的数目
就实现来说
调度者线程类所担负的责任如其名字该类的责任是调度线程只需要利用自己的属性完成自己的责任所以该类是承担了责任的并且该类的责任又集中到唯一的单体对象中
而其他对象又依赖于该特定对象所承担的责任我们就需要得到该特定对象那该类就是一个单例模式的实现了
三如何开发线程安全的 Servlet
变量的线程安全这里的变量指字段和共享数据(如表单参数值)
a将 参数变量 本地化多线程并不共享局部变量所以我们要尽可能的在servlet中使用局部变量
例如String user = ;
user = requestgetParameter(user);
b使用同步块Synchronized防止可能异步调用的代码块这意味着线程需要排队处理
在使用同板块的时候要尽可能的缩小同步代码的范围不要直接在sevice方法和响应方法上使用同步这样会严重影响性能
属性的线程安全ServletContextHttpSessionServletRequest对象中属性
ServletContext(线程是不安全的)
ServletContext是可以多线程同时读/写属性的线程是不安全的要对属性的读写进行同步处理或者进行深度Clone()
所以在Servlet上下文中尽可能少量保存会被修改(写)的数据可以采取其他方式在多个Servlet中共享比方我们可以使用单例模式来处理共享数据
HttpSession(线程是不安全的)
HttpSession对象在用户会话期间存在只能在处理属于同一个Session的请求的线程中被访问因此Session对象的属性访问理论上是线程安全的
当用户打开多个同属于一个进程的浏览器窗口在这些窗口的访问属于同一个Session会出现多次请求需要多个工作线程来处理请求可能造成同时多线程读写属性
这时我们需要对属性的读写进行同步处理使用同步块Synchronized和使用读/写器来解决
ServletRequest(线程是安全的)
对于每一个请求由一个工作线程来执行都会创建有一个新的ServletRequest对象所以ServletRequest对象只能在一个线程中被访问ServletRequest是线程安全的
注意ServletRequest对象在service方法的范围内是有效的不要试图在service方法结束后仍然保存请求对象的引用
使用同步的集合类
使用Vector代替ArrayList使用Hashtable代替HashMap
不要在Servlet中创建自己的线程来完成某个功能
Servlet本身就是多线程的在Servlet中再创建线程将导致执行情况复杂化出现多线程安全问题
在多个servlet中对外部对象(比方文件)进行修改操作一定要加锁做到互斥的访问
四SingleThreadModel接口
javaxservletSingleThreadModel接口是一个标识接口如果一个Servlet实现了这个接口那Servlet容器将保证在一个时刻仅有一个线程可以在给定的servlet实例的service方法中执行将其他所有请求进行排队
服务器可以使用多个实例来处理请求代替单个实例的请求排队带来的效益问题服务器创建一个Servlet类的多个Servlet实例组成的实例池对于每个请求分配Servlet实例进行响应处理之后放回到实例池中等待下此请求这样就造成并发访问的问题
此时局部变量(字段)也是安全的但对于全局变量和共享数据是不安全的需要进行同步处理而对于这样多实例的情况SingleThreadModel接口并不能解决并发访问问题
SingleThreadModel接口在servlet规范中已经被废弃了