众所周知,Servlet为单实例多线程,非线程安全的。
若一个Servlet对应多个URL映射,那么将会生成一个还是多个Servlet实例呢?
最好的办法就是动手体验一下
@WebServlet({ "/demoServlet1", "/demoServlet2" })
public class DemoServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
PrintWriter out = response.getWriter();
out.println(request.getServletPath() + "[" + this + "]");
out.flush();
out.close();
}
}
输出结果:
/demoServlet1[com.learn.servlet3.thread.DemoServlet@1320a41]
/demoServlet2[com.learn.servlet3.thread.DemoServlet@9abce9]
输出结果可以看到映射/demoServlet1和/demoServlet2对应Servlet实例是不同的。
结果证明:Servlet将为每一个URL映射生成一个实例;一个Servlet可能存在多个示例,但每一个实例都会对应不同的URL映射。
下面讨论单个Servlet、多线程情况下保证数据线程同步的几个方法。
- synchronized:代码块,方法
大家都会使用的方式,不用详细介绍了。
建议优先选择修饰方法。
- volatile
轻量级的锁,可以保证多线程情况单线程读取所修饰变量时将会强制从共享内存中读取最新值,但赋值操作并非原子性。
一个具有简单计数功能Servlet示范:
/**
* 使用Volatile作为轻量级锁作为计数器
*
* @author yongboy
* @date 2011-3-12
* @version 1.0
*/
@WebServlet("/volatileCountDemo")
public class VolatileCountServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private volatile int num = 0;
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
addOne();
response.getWriter().write("now access num : " + getNum());
}
/**
* 读取开销低
*/
private int getNum() {
return num;
}
/**
* 其写入为非线程安全的,赋值操作开销高
*/
private synchronized void addOne() {
num ++;
}
}
我们在为volatile修饰属性赋值时,还是加把锁的。
- ThreadLocal
可以保证每一个线程都可以独享一份变量副本,每个线程可以独立改变副本,不会影响到其它线程。
这里假设多线程环境一个可能落显无聊的示范,初始化一个计数,然后循环输出:
@WebServlet("/threadLocalServlet")
public class ThreadLocalServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static ThreadLocal threadLocal = new ThreadLocal() {
@Override
protected Integer initialValue() {
return 0;
}
};
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
response.setHeader("Connection", "Keep-Alive");
PrintWriter out = response.getWriter();
out.println("start... " + " [" + Thread.currentThread() + "]");
out.flush();
for (int i = 0; i < 20; i++) {
out.println(threadLocal.get());
out.flush();
threadLocal.set(threadLocal.get() + 1);
}
// 手动清理,当然随着当前线程结束,亦会自动清理调
threadLocal.remove();
out.println("finish... ");
out.flush();
out.close();
}
}
若创建一个对象较为昂贵,但又是非线程安全的,在某种情况下要求仅仅需要在线程中独立变化,不会影响到其它线程。选择使用ThreadLocal较好一些,嗯,还有,其内部使用到了WeakHashMap,弱引用,当前线程结束,意味着创建的对象副本也会被垃圾回收。
Hibernate使用ThreadLocal创建Session;Spring亦用于创建对象会使用到一点。
嗯,请注意这不是解决多线程共享变量的钥匙,甚至你想让某个属性或对象在所有线程中都保持原子性,显然这不是解决方案。
- Lock
没什么好说的,现在JDK版本支持显式的加锁,相比synchronized,添加与释放更加灵活,功能更为全面。
@WebServlet("/lockServlet")
public class LockServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static int num = 0;
private static final Lock lock = new ReentrantLock();
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try{
lock.lock();
num ++;
response.getWriter().println(num);
}finally{
lock.unlock();
}
}
}
必须手动释放锁,否则将会一直锁定。
- wait/notify
较老的线程线程同步方案,较之Lock,不建议再次使用。
- 原子操作
原子包装类,包括一些基本类型(int, long, double, boolean等)的包装,对象属性的包装等。
@WebServlet("/atomicServlet")
public class AtomicServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final AtomicInteger num = new AtomicInteger(0);
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
PrintWriter out = response.getWriter();
out.println(num.incrementAndGet());
out.flush();
out.close();
}
}
包装类提供了很多的快捷方法,比如上面的incrementAndGet方法,自身增加1,然后返回结果值,并且还是线程安全的,省缺了我们很多手动、笨拙的编码实现。
更多原子类,请参见 java.util.concurrent.atomic包。
- 一些建议
尽量不要在Servlet中单独启用线程
使用尽可能使用局部变量
尽可能避免使用锁
- 数据结构小结
在平常环境下使用的一些数据结构,在多线程并发环境下可选择使用java.util.concurrent里内置替代品。下面有一个小结,仅供参考。
非线程安全 | 工具版 | JUC版本 |
HashMap |
Collections.synchronizedMap |
ConcurrentHashMap |
ArrayList |
Collections.synchronizedList |
CopyOnWriteArrayList |
HashSet |
Collections.synchronizedSet |
synchronizedSet |
Queue |
|
ConcurrentLinkedQueue |
Servlet线程安全有很太多的话题要说,以上仅仅为蜻蜓点水,真正需要学习和实践的还有一段长路要学习。另,话说,这里已和Servlet版本无关,是知识积累和分享。