`
sw1982
  • 浏览: 511258 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

Java内存模型和并发机制

阅读更多

原文链接:http://www.yeeyan.com/articles/view/2091/974

正文

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

 
有这么几个知识点:

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

2            volatile ”修饰符的使用

3                    线程安全的延迟初始化

4                    Final ”字段

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

 

happens-before ordering

 

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

 

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

 

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

x=y=0;

//now start threads

//thread 1

x=1;

j=y;

//thread 2

y=1;

i=x;

问题是,在线程 1 2 执行完毕后,有没有可能 i j 都等于 0

 

我们知道,如果 i j 结果都为 0 的话,对 y 的读(在 j y 里用到)一定比对 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 的获得,形成了一个配对。可以这样想,当 M thread1 里被释放后,在 thread1 里所作的更新就被推出(到主内存),随后的在 thread2 里对 M 的获得,就会抓取所有在 thread1 里所作的更新。作为 thread2 能得到在 thread1 里的更新,这就是 happens before 的次序。

 

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

 

如果同时有两笔对同一个内存地址的访问,其中一笔是写的操作,并且内存地址不是 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();

 

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

 

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

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

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

 

Volatile 修饰符

 

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

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

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

 

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

 

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

 

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 变量, happens before 的次序就不会得到体现, 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 的使用使得 happens before 的次序得以体现, 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++ ;的命令时,如果在对 value 1 后要写回 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 的关系),所以最后它返回的很可能是一个垃圾值。

 

在这里建立 happens before 的关系的方法很简单,就是对 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 避免使用旧的,线程不安全的数据结构,如 Vector Hashtable

3 使用 bulk IO java.nio 里的类

 

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

 

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

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

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

3 参考书目: Java Concurrency in Practice

分享到:
评论

相关推荐

    Java 内存模型

    值得注意的是,Java内存模型的讨论和开发过程异常详细和技术化,它涉及了多个学术话题的深入见解和进展。关于这一规范的讨论和开发的档案,可以在Java内存模型的网站上找到,该网站提供了额外的信息,有助于理解该...

    java_cpu 内存模型和java内存模型.pdf

    Java程序员了解CPU相关知识对于...这不仅能帮助他们更好地理解Java内存模型和并发机制,还能使他们编写出更加高效的代码,提升程序的整体性能。在多核处理器日益普及的今天,这种跨学科的知识整合能力显得尤为重要。

    cpu 内存模型和java内存模型

    Java程序员了解CPU以及相关的内存模型,对于深入理解...通过分析具体的编程问题,比如Java锁的不同实现方式、CPU缓存的工作机制等,可以帮助程序员更好地理解Java内存模型,在多线程环境下写出更加健壮和高效的代码。

    深入理解Java内存模型

    Java内存模型是并发编程中一个至关重要的概念,它定义了共享变量的访问规则,以及这些变量如何在多线程环境下进行读写操作。...Java内存模型的规则和机制能够帮助程序员确保在并发环境下程序的正确执行和数据的一致性。

    深入理解Java内存模型 pdf 超清版

    深入理解Java内存模型,不仅能够帮助我们编写出高效、线程安全的代码,还能在面临并发问题时提供有力的分析和解决手段。通过阅读《深入理解Java内存模型》这本书,开发者可以进一步掌握Java并发编程的核心技术,提升...

    深度剖析java内存模型

    在并发编程中,理解Java内存模型对于编写正确的多线程程序至关重要。 首先,线程之间的同步指的是程序控制不同线程之间操作发生相对顺序的机制。在Java的共享内存并发模型中,同步是显式进行的,程序员需要显式地...

    Java内存模型的历史变迁

    Java内存模型(Java Memory Model,简称JMM)作为Java并发机制的核心,其设计理念直接影响到程序的性能与可靠性。本文将探讨Java内存模型从早期版本到JDK 5的重大变革,并重点介绍这一变迁背后的动机及其对Java开发...

    java内存模型与并发技术.ppt

    Java内存模型与并发技术是Java开发中的核心概念,它们直接影响着多线程程序的正确性和性能。本讲座主要探讨了Java内存模型(JMM)的基础理论以及并发编程的关键点。 首先,内存模型是一个抽象的概念,它描述了一个...

    java内存模型文档

    Java内存模型,简称JMM(Java Memory Model),是Java编程语言规范的一部分,它定义了线程如何共享和访问内存,以及在并发编程中如何处理数据一致性的问题。理解JMM对于编写高效、线程安全的Java代码至关重要。 1. ...

    深入理解 Java 内存模型_程晓明_InfoQ_java_内存模型_

    Java内存模型,简称JMM(Java Memory ...总之,Java内存模型是Java多线程编程的基础,它为程序员提供了强大的工具来控制和保证并发代码的正确性。掌握JMM的相关知识,能够帮助我们写出更健壮、更高效的Java应用程序。

    深入理解java内存模型

    这本书"深入理解Java内存模型"显然是为了帮助读者深入探讨这个主题,以便更好地理解和解决并发编程中的问题。 Java内存模型主要涉及以下几个核心概念: 1. **主内存**:所有线程共享的数据存储区域,包括类的静态...

    java内存模型.pdf

    Java内存模型的主要目标是解决并发编程中的可见性、原子性和有序性问题。 1. JMM简介: - 内存模型概述:JMM规定了程序中各个线程如何访问和更新共享变量,包括实例域、静态域和数组元素。在多处理器或多线程环境...

    深入Java内存模型:揭秘并发编程的基石

    本文将详细介绍Java内存模型的基本概念、其重要特性和如何通过实际代码来确保并发环境下的程序正确性。 ## Java内存模型概述 Java内存模型规定了所有线程对共享变量的操作(包括读取和写入)都必须通过主内存来...

    java内存模型详解--非常经典

    在并发编程中,Java内存模型提供了一些内置的同步机制,如volatile关键字、synchronized关键字以及final修饰符。这些机制确保了在多线程环境下,对共享变量的访问具有一定的可见性和有序性。 - volatile关键字:...

    深入理解 Java 内存模型

    Java 内存模型(Java Memory Model,简称 JMM)是 Java 平台中关于线程如何访问共享变量的一套规则,它定义了线程之间的内存可见性、数据一致性以及指令重排序等关键概念,对于多线程编程和并发性能优化至关重要。...

    java内存模型详解

    理解Java内存模型对于编写多线程并发程序至关重要,因为它直接影响到程序的正确性和性能。 在Java中,内存分为以下几个区域: 1. **程序计数器**:每个线程都有自己的程序计数器,用于存储当前线程执行的字节码...

    Java内存模型分析与其在编程中的应用.pdf

    Java内存模型规定了对内存和并发操作的高级抽象,其目的是为了提供给开发者一个一致的内存访问语义,隐藏各种平台下的内存访问细节。这是通过一系列规则来实现的,比如happens-before规则,它定义了一种偏序关系来...

Global site tag (gtag.js) - Google Analytics