有关于Servlet 的线程安全,首先需要知道的是,在一般情况下,每个Servlet 在容器里都只有一个实例(instance), 而每当有用户访问该Servlet 时,容器都会产生一个线程。
这是比较基本的概念了。一般我们还知道,Java 有一个Marker Interface 叫做SingleThreadModel, 这个接口一旦被继承,就意味着容器一般对一个instance 只维护一个线程。当时设计这个接口的用意自然是为了线程安全的问题。而现在,这个接口已经被废弃了。(然而,偶尔有些用老版本的系统会涉及到)。看一下API 文档中对此的说明:
Ensures that servlets handle only one request at a time. ... If a servlet implements this interface, you are guaranteed that no two threads will execute concurrently in the servlet's service
method.
然而很显然,这样做必然会极大损失效率。所以,很多容器会 maintaining a pool of servlet instances 去挽回一些损失。
本来行文到此应该结束了。因为笔者正好想到有关JVM结构的一篇文章,所以有了以下的思考
对于实例,线程,说得最多的必然是线程安全的话题。线程安全本文从三个方面来讲:
1、 虚拟机。虚拟机的一个特点就是它真实地模拟了计算机硬件对于程序的处理。虚拟机有多个重要部件,我们这里牵涉到的主要是方法域,堆,程序计数器以及Java 栈。
对于实例来讲,其与方法域和堆是一一对应的。也就是说,每个实例都有其唯一的方法域和堆。而对于程序计数器以及Java 栈,他们对应的则是线程。(即每个实例的线程都有相应的程序计数器以及Java 栈)。并且,对于实例,是可以有多个线程共享的,也就是说,实例的方法域和堆是可以由该实例的多个线程共享的。
堆的共享,一方面是Java中线程通讯的基础,一方面也是产生线程问题的一个基本原因。
2。Java语言的内存模型。在Java 中,有很多种变量。从类型上讲,基本类型的(primitive),或者对象类型的。从范围上讲,实例变量,类变量,函数内部声明的变量;从修饰符讲,有 final, volatile 等。
对于基本类型,他们往往是放在栈中。笔者认为,其(访问)地址是在编译期间就获得的。而对象类型的稍微麻烦一点,堆栈里往往放的是对象的引用(当然,对堆栈的访问地址也是在编译期间决定),而实际的对象则在堆中。(这有些类似于汇编语言的间接访问,笔者认为这也是内存可以动态分配的根本)。
对于函数内部的普通变量,不会有线程安全的问题。为什么呢?因为这些变量的引用(或者可以解释为入口)都是保存在Java 栈里的,而Java 栈都是线程独立的。(关于这个,笔者的一个假设是,在获得引用地址之前,JVM 无法访问到对象)。对于实例变量, 可能稍微麻烦一些。因为其对应的实例是线程共享的,也就是说,实例变量也会共享。这就是线程安全问题的由来。对于类变量,其在线程安全方面的用法类似实例变量(当然,其实是有不同的)。
而volatile 和final 变量。volotile 修饰符的意义一般被解释是为了告知编译器此变量将经常变化,不需要编译器对其优化使用寄存器。而很少有说到的是,这个“经常变化”的变量,往往是线程共享的一些变量。在一份文档里有这样一小段程序:如果在多个线程中运行上面的函数foo和bar,这上面的变量stop 和num 就是刚才所说的“经常变化”的变量。而volatile 还有另外一个作用。
private boolean stop = false ;
private int num = 0 ;
public void foo() {
stop = true ;
// ...
}
public void bar() {
if (stop)
num += num; // num can be 0
}
// ...}
在JSR133中说到,一般jvm 会对其认为不会影响上下文的程序做适当优化。这其中的一个优化是:re-order. 也就是说,在上面的程序foo 函数中,两个语句的执行顺序可能被交换。这就是上面注释中num can be 0 的由来。这与volatile 有什么关系呢?事实是,如果对上面的两个变量加上volatile修饰符,上述的re-order就不会发生。
According to the JLS, because stop
and num
are declared volatile
, they should be sequentially consistent. This means that if stop
is ever true
, num
must have been set to 100
. However, because many JVMs do not implement the sequential consistency feature of volatile
, you cannot count on this behavior.
再说一个final. final 和线程安全产生关系的理由是,它可以较好的预防此类问题。一般来说,final 修饰的实例变量或者类变量,它的值在所在类的构造函数完成之后就固定了。“所在类的构造函数完成”有两种写法:一种是在声明field时就赋值,一种是在构造函数中赋值。使用final之后,只要能够保证在构造函数的时候处理好线程问题就可以了。当然,final 之后不能被修改,注定它只适用一小部分情况。
3、线程问题的解决。一般说到线程安全,大家都会想到synchronized. synchronized 的使用分很多种,包括直接在方法签名中声明,使用synchronized 块等。另外,需要说明的还有两点:
首先是关于synchronized 的原理。在JSR133 中说到,synchronized 的函数表示其获取了某个monitor,这个monitor 通常是某个对象的引用。而后synchronized 对monitor 加锁,写成程序像:
synchronized (monitor){ ...//...}
要访问同一对象的synchronized 方法,必须要获得该monitor 并加锁才可以。我们以前说,锁的target 是对象本身。其实是因为,这个monitor 往往是这个对象的某个实例变量或者干脆就是this. 另外,很多人在程序里写synchronized(lock), 这类命名,其实是不太合适的。
第二,对于static 的方法中使用synchronized 块时,选择的被锁定对象是Object.class. 比如对于上面的例子,就是Test.class。(不是Test.getClass).
关于线程安全先写到这里,jsr133还没看完,今天看到的一篇关于singleton 和double check 的文章,有些想法,以后再写上来吧。