Java内存模型和并发机制

来源:互联网 发布:腾讯游戏网络加速器 编辑:程序博客网 时间:2024/06/07 02:19
 

这里讲的是关于Java并发机制的基础模块及如何设计合理的并发机制的抽象思维和设计模式。


有这么几个知识点:

1          “先行发生”的次序(happens-before ordering

2            volatile”修饰符的使用

3                    线程安全的延迟初始化

4                    Final”字段

5                    关于Java并发机制的一些建议

 

happens-before ordering

 

当我们在Java里谈起互斥锁定mutualexclusion lock)时,通常都指当我首先进入了一个互斥锁定,其他人试图获得这个同样的互斥锁定时,必须在我释放了之后才可以。这是JavaC++里关于互斥锁定的最重要的属性。但事实上,这不是互斥锁定唯一的属性。还有一个属性是可见性(visibility),它和次序属性(ordering)紧密相关。

 

当一个线程使用一个锁定时,它决定了其他线程何时可以看到该线程在锁定后所做的更新。当一个线程对一个变量进行写的操作时,这个写的操作是否会被其他线程看到将取决于该线程使用的是何种锁定。

 

下面是一个小测验。有下面这段程序:

x=y=0;

//now start threads

//thread 1

x=1;

j=y;

//thread 2

y=1;

i=x;

问题是,在线程12执行完毕后,有没有可能ij都等于0

 

我们知道,如果ij结果都为0的话,对y的读(在jy里用到)一定比对y的写先发生,类似地,对x的读一定比对x的写先发生?那么,这可能吗?

 

答案是肯定的。事实上,编译器和处理器都可能对上述程序重新排序,尤其在使用多个处理器,赋值并没有在主内存里同步时。现代的java内存模型使上述现象成为可能。上面的程序显然是错误的未经同步的代码,因为它没有使用锁定。当不同的线程需要读写同一个数据时,必须使用锁定的技术。

 

再看看下面一段非常关键的代码。可以说,这段代码是全篇演讲的核心。

thread 1:

ref1.x = 1;

lock M;

glo = ref1;

unlock M;

 

thread 2:

lock M;

ref2 = glo;

unlock M;

j = ref2.x;

 

thread1里有几个写的操作,在对glo变量进行写的操作之前,它首先对对象M进行了锁定。在thread2里,当thread1释放了对M锁定之后,它过得了对M锁定,并开始对glo的读操作。问题是,在thread1里的写操作,thread2进行读操作时,可以看到吗?

 

答案是肯定的,原因是thread1里对M对象的释放和thread2里对同一个对象M的获得,形成了一个配对。可以这样想,当Mthread1里被释放后,在thread1里所作的更新就被推出(到主内存),随后的在thread2里对M的获得,就会抓取所有在thread1里所作的更新。作为thread2能得到在thread1里的更新,这就是happensbefore的次序。

 

一个释放的操作和相匹配的之后发生的获得操作就会建立起业已发生的次序。在同一个线程里的执行次序也会建立起业已发生的次序(后有例子会涉及到在同一线程里的执行次序问题) 业已发生的次序是可以转换的。

 

如果同时有两笔对同一个内存地址的访问,其中一笔是写的操作,并且内存地址不是volatile的,那么这两笔访问在VM里的执行次序就会按照“先行发生”的规则来排。

 

下面举一些例子来说明问题。请看下面的程序:

int z = o.field1;

//block until obtain lock

synchronized(o){

            //get main memory value of field1 and field2

            int x = o.field1;

            int y = o.field2;

            o.field3 = x+y;

            //commit value of field3 to main memory

}

//release lock

moreCode();

 

像你从这个程序的注释里读到的一样,你会期望看到,在锁定发生后,xy会被从主要内存里读到的field1field2赋值,field3被赋值后在锁定释放后被推到主内存里,这样,其他线程应该由此得到最近的更新。

 

想起来是蛮符合逻辑的。实际所发生的可能不一定如此,下面一些特殊情况会造成happensbefore的次序失效。

1 如果o是本地线程的对象?因为锁定的是本地线程里的对象,在其他线程里不可能获得一个相匹配的锁定,所以对本地线程对象的锁定不起作用,

2 是否有现有对o锁定还未被释放?如果此前已有一个对象的锁定,在该锁定被释放之前,对同一个对象的再锁定不起作用。

 

Volatile修饰符

 

当一个字段被多个线程同时访问,至少其中一个访问是进行写操作,我们可以采用的手段有以下两种:

1 采用锁定来避免同时访问

2 volatile来定义该字段,这样做有两个作用,一是增强程序的可读性,让读者知道这是一个将被多线程访问操作的字段;另外一个作用是在JVM对该字段的处理上,可以得到特殊的保证。

 

volatilejava里除锁定之外的重要同步手段。首先,volatile字段的读和写都直接进主内存,而不会缓存在寄存器中;其次,volatile字段的读和写的次序是不能更改的;最后,字段的读和写实质上变成了锁定模型里的获得和释放。

 

对一个volatile字段的写总是要happensbefore对它的读;对它的写类似于对锁定的释放;对它的读类似于进入一个锁定。

 

volatile修饰符对可见性的影响,让我们看看下面的代码:

 

class Animator implements Runnable {

            private volatile boolean stop = false;

            public void stop () { stop = true;}

            public void run() {

                        while (!stop){

                                    oneStep();

                                    try { Thread.sleep(100);} …;

                      }

            }

            private void oneStep() { /*…*/ }

 

}

 

这段程序里主要有两个线程,一个是stop,一个是run。注意,如果不用volatile来修饰stop变量,happensbefore的次序就不会得到体现,stop线程里对stop变量的写操作不会影响其他线程,所以编译器不会去主内存里读取stop线程对stop变量的改变。这样,在run线程里就会出现死循环,因为在run线程里从始至终使用的只是stop变量初始化时的值。

 

由于编译器优化的考虑,如果没有volatile来修饰stop变量,run线程永远都不会读到其他线程对stop变量的改变。

 

volatile对执行次序保证的作用,我们看看下面的代码:

 

class Future {

            private volatile boolean ready;

            private Object data;

            public Object get() {

                        if (!ready)

                                    return null;

                        return data;

            }

            public synchronized  void setOnce(Object o){

                        if (ready) throw…;

                        data = o;

                        ready = true;

            }

}

 

首先一点还是由于volatile的使用使得happensbefore的次序得以体现,setOnce方法对ready变量的写操作的结果一定会被get方法中的读操作得到。

 

其次,更重要的,如果ready变量不被volatile来修饰,当线程A叫到setOnce方法时,可能按照data=o; ready=true;的次序来执行程序,但是另一个线程B叫到setOnce方法时,可能会按照ready=true;data=o;的次序来执行。可能发生的一个情况是当线程B执行完ready=true时,线程A正在检查ready变量,结果造成data未有写操作的情况下就完成了方法。data可能是垃圾值,旧值,或空值。

 

有关volatile的另外一点是被volatile修饰的变量的非原子操作化。比如,执行volatile value++;的命令时,如果在对value1后要写回value时,另外一个线程对value做写的操作,之前加和的操作就会被影响到。

 

JVM而言,对volatile变量的读操作是没有额外成本的,写操作会有一些。

 

 

线程安全的延迟初始化

 

首先有下面一段代码:

 

Helper helper;

 

Helper getHelper() {

            if (helper == null)

                        synchronized(this){

                                    if (helper ==null)

                                                helper = new Helper();

                      }

            return helper;

}

 

这段代码是典型的延迟初始化的产物。它有两个目的:一是让初始化的结果能被多线程共用;一是一旦对象初始化完毕,为了提高程序的效率,就不再使用同步锁定。如果不是由于第二点,实施对整个方法的同步其实是最保险的,而不是如本段代码中的只是对段的同步。

 

这段代码的问题是,对helper的写操作锁定是存在的,但是却没有相匹配的获得锁定来读helper,因此,happens-before的关系没有建立起来,进入同步段来初始化helper的唯一可能是helper==null。如果一个线程过来检查是否helper == null,如果碰巧不是的话,它却不能得到其他线程对helper的更新(因为没有happens-before的关系),所以最后它返回的很可能是一个垃圾值。

 

在这里建立happensbefore的关系的方法很简单,就是对helper加上volatile的修饰符,volatile Helper helper;

 

线程安全的immutable对象

 

基本原则是尽可能的使用immutable对象,这样做会有很多优点,包括减少对同步机制的需要;

在类里,可以将所有变量定义为final,并且在构建完成前,不要让其他线程看到正在构建的对象。

 

举个例子,线程1新建了一个类的实例;线程1在没有使用同步机制的情况下,将这个类的实例传递给线程2;线程2访问这个实例对象。在这个过程中,线程2可能在线程1对实例构建完毕之前就得到对实例的访问权,造成了在同步机制缺失的情况下的数据竞争。

 

关于Java并发机制的一些有益建议

 

尽可能的使用已经定义在java.util.concurrent里的类来解决问题,不要做得太底层。增强对java内存模型的理解,搞懂在特定环境下释放和获得锁定的意义,在你需要自己去构想和实施并发机制时,这些都会用得上。

 

在一个单线程的环境下使用并发类可能会产生可观的开销,比如对Vector每一次访问的同步,每一笔IO操作等等。在单线程环境下,可以用ArrayList来代替Vector。也可以用bulk I/O java.nio来加快IO操作。

 

看看下面一段代码:

 

ConcurrentHashMap<String,ID> h;

ID getID(String name){

            ID x = h.get(name);

            if (x==null){

                        x=new ID();

                        h.put(name,x);

            }         

           return x;

}

 

如果你只调用get(),或只调用put()时,ConcurrentHashMap确实是线程安全的。但是,在你调用完get后,调用put之前,如果有另外一个线程调用了h.put(name,x),你再执行h.put(name,x),就很可能把前面的操作覆盖掉了。所以,即使在线程安全的情况下,你还有有可能违法原子操作的规则。

 

减少同步机制的开销:

1 避免在多线程间共用可变对象

2 避免使用旧的,线程不安全的数据结构,如VectorHashtable

3 使用bulk IOjava.nio里的类

 

在使用锁定时,减少锁定的范围和持续时间。

 

关于java内存模型和并发机制,以下是一些有用的参考信息:

1 http://www.cs.umd.edu/~pugh/java/memoryModel

2 订阅mailing listhttp://altair.cs.oswego.edu/mailman/listinfo/concurrency-interest

3 参考书目: Java Concurrency in Practice

原创粉丝点击