线程安全及其他

来源:互联网 发布:淘宝兼职工资单反馈 编辑:程序博客网 时间:2024/05/03 18:19

有关于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 还有另外一个作用。

class Test{ 
 
private boolean stop = false;  
private int num = 0;  

public void foo()  {    
num = 100;  
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 的文章,有些想法,以后再写上来吧。

原创粉丝点击