xylz,imxylz

关注后端架构、中间件、分布式和并发编程

   :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  111 随笔 :: 10 文章 :: 2679 评论 :: 0 Trackbacks

在这个小结里面重点讨论原子操作的原理和设计思想。

由于在下一个章节中会谈到锁机制,因此此小节中会适当引入锁的概念。

Java Concurrency in Practice中是这样定义线程安全的:

当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替运行,并且不需要额外的同步及在调用方代码不必做其他的协调,这个类的行为仍然是正确的,那么这个类就是线程安全的。

显然只有资源竞争时才会导致线程不安全,因此无状态对象永远是线程安全的

原子操作的描述是: 多个线程执行一个操作时,其中任何一个线程要么完全执行完此操作,要么没有执行此操作的任何步骤,那么这个操作就是原子的。

枯燥的定义介绍完了,下面说更枯燥的理论知识。

指令重排序

Java语言规范规定了JVM线程内部维持顺序化语义,也就是说只要程序的最终结果等同于它在严格的顺序化环境下的结果,那么指令的执行顺序就可能与代码的顺序不一致。这个过程通过叫做指令的重排序。指令重排序存在的意义在于:JVM能够根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥机器的性能。

程序执行最简单的模型是按照指令出现的顺序执行,这样就与执行指令的CPU无关,最大限度的保证了指令的可移植性。这个模型的专业术语叫做顺序化一致性模型。但是现代计算机体系和处理器架构都不保证这一点(因为人为的指定并不能总是保证符合CPU处理的特性)。

我们来看最经典的一个案例。

package xylz.study.concurrency.atomic;

public class ReorderingDemo {

   
static int x = 0, y = 0, a = 0, b = 0;

   
public static void main(String[] args) throws Exception {

       
for (int i = 0; i < 100; i++) {
            x
=y=a=b=0;
            Thread one
= new Thread() {
               
public void run() {
                    a
= 1;
                    x
= b;
                }

            }
;
            Thread two
= new Thread() {
               
public void run() {
                    b
= 1;
                    y
= a;
                }

            }
;
            one.start();
            two.start();
            one.join();
            two.join();
            System.out.println(x
+ " " + y);
        }

    }
 

}



在这个例子中one/two两个线程修改区x,y,a,b四个变量,在执行100次的情况下,可能得到(0 1)或者(1 0)或者(1 1)。事实上按照JVM的规范以及CPU的特性有很可能得到(0 0)。当然上面的代码大家不一定能得到(0 0),因为run()里面的操作过于简单,可能比启动一个线程花费的时间还少,因此上面的例子难以出现(0,0)。但是在现代CPU和JVM上确实是存在的。由于run()里面的动作对于结果是无关的,因此里面的指令可能发生指令重排序,即使是按照程序的顺序执行,数据变化刷新到主存也是需要时间的。假定是按照a=1;x=b;b=1;y=a;执行的,x=0是比较正常的,虽然a=1在y=a之前执行的,但是由于线程one执行a=1完成后还没有来得及将数据1写回主存(这时候数据是在线程one的堆栈里面的),线程two从主存中拿到的数据a可能仍然是0(显然是一个过期数据,但是是有可能的),这样就发生了数据错误。

在两个线程交替执行的情况下数据的结果就不确定了,在机器压力大,多核CPU并发执行的情况下,数据的结果就更加不确定了。

Happens-before法则

Java存储模型有一个happens-before原则,就是如果动作B要看到动作A的执行结果(无论A/B是否在同一个线程里面执行),那么A/B就需要满足happens-before关系。

在介绍happens-before法则之前介绍一个概念:JMM动作(Java Memeory Model Action),Java存储模型动作。一个动作(Action)包括:变量的读写、监视器加锁和释放锁、线程的start()和join()。后面还会提到锁的的。

happens-before完整规则:

(1)同一个线程中的每个Action都happens-before于出现在其后的任何一个Action。

(2)对一个监视器的解锁happens-before于每一个后续对同一个监视器的加锁。

(3)对volatile字段的写入操作happens-before于每一个后续的同一个字段的读操作。

(4)Thread.start()的调用会happens-before于启动线程里面的动作。

(5)Thread中的所有动作都happens-before于其他线程检查到此线程结束或者Thread.join()中返回或者Thread.isAlive()==false。

(6)一个线程A调用另一个另一个线程B的interrupt()都happens-before于线程A发现B被A中断(B抛出异常或者A检测到B的isInterrupted()或者interrupted())。

(7)一个对象构造函数的结束happens-before与该对象的finalizer的开始

(8)如果A动作happens-before于B动作,而B动作happens-before与C动作,那么A动作happens-before于C动作。

volatile语义

到目前为止,我们多次提到volatile,但是却仍然没有理解volatile的语义。

volatile相当于synchronized的弱实现,也就是说volatile实现了类似synchronized的语义,却又没有锁机制。它确保对volatile字段的更新以可预见的方式告知其他的线程。

volatile包含以下语义:

(1)Java 存储模型不会对valatile指令的操作进行重排序:这个保证对volatile变量的操作时按照指令的出现顺序执行的。

(2)volatile变量不会被缓存在寄存器中(只有拥有线程可见)或者其他对CPU不可见的地方,每次总是从主存中读取volatile变量的结果。也就是说对于volatile变量的修改,其它线程总是可见的,并且不是使用自己线程栈内部的变量。也就是在happens-before法则中,对一个valatile变量的写操作后,其后的任何读操作理解可见此写操作的结果。

尽管volatile变量的特性不错,但是volatile并不能保证线程安全的,也就是说volatile字段的操作不是原子性的,volatile变量只能保证可见性(一个线程修改后其它线程能够理解看到此变化后的结果),要想保证原子性,目前为止只能加锁!

volatile通常在下面的场景:

 

volatile boolean done = false;



   
while( ! done ){
        dosomething();
    }

 

应用volatile变量的三个原则:

(1)写入变量不依赖此变量的值,或者只有一个线程修改此变量

(2)变量的状态不需要与其它变量共同参与不变约束

(3)访问变量不需要加锁

 

这一节理论知识比较多,但是这是很面很多章节的基础,在后面的章节中会多次提到这些特性。

本小节中还是没有谈到原子操作的原理和思想,在下一节中将根据上面的一些知识来介绍原子操作。

 

参考资料:

(1)Java Concurrency in Practice

(2)正确使用 Volatile 变量

 



©2009-2014 IMXYLZ |求贤若渴
posted on 2010-07-03 20:40 imxylz 阅读(36851) 评论(16)  编辑  收藏 所属分类: J2EEJava Concurrency

评论

# re: 深入浅出 Java Concurrency (4): 原子操作 part 3 2010-07-04 00:41 滴水
讲到这篇开始有点意思了,呵呵,加油。  回复  更多评论
  

# re: 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则 2010-07-23 16:37 thebye85
“volatile并不能保证线程安全的,也就是说volatile字段的操作不是原子性的”
既然volatile保证变量在主内存,多线程操作的应该在同一内存,能说下为什么多线程下不是原子性的吗?谢谢  回复  更多评论
  

# re: 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则 2010-07-23 16:56 xylz
@thebye85
比如主存中是i=10,两个线程同时读取到i=10,都需要++,那么调用完成后可能i=11,而我们的目标是i=12,所以不是一致的。如果一个加一个减结果就更加难以预料了。  回复  更多评论
  

# re: 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则 2010-07-23 22:36 thebye85
@xylz
谢谢回复。再请教下两个线程中的i++后的中间临时结果(在没写入到i之前),是保存在哪的,是在主存中而新开辟的内存吗?  回复  更多评论
  

# re: 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则 2010-07-23 23:07 xylz
@thebye85

Java内存分为堆和栈。而栈通常是线程独有的,堆是线程共享的,通常对于一个变量而言,JVM尽可能的保存在栈中,因为栈通常是寄存器、CPU缓存的高速设备,所以栈一般比较小,而堆比较大一般在内存中。对于一个volatile变量,JMM(Java存储模型)保证对所有线程是共享,所以一定不会存在寄存器、CPU缓存等栈上,我查了英文本的《The Java Language Specification, Third Editon》,上面没有说具体存放于何处,我猜就可能存放于主存上,也就是堆上。

所以通常而言,操作一个volatile变量要比非volatile变量开销要大,但是这个差别很小,相对于锁机制来说可以忽略不计。  回复  更多评论
  

# re: 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则 2010-07-31 12:53 Johnny Jian
@thebye85
应该是操作栈吧  回复  更多评论
  

# re: 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则 2010-08-12 15:20 thebye85
@Johnny Jian
我是说假如变量i声明为volatile,多线程操作i都是在共享内存(堆)上吧?
保存各自线程栈的话,怎么保证i的可见性?  回复  更多评论
  

# re: 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则 2010-08-12 15:27 Johnny Jian
@thebye85
i++是非原子操作,你只能通过其他手段来保证咯  回复  更多评论
  

# re: 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则 2011-07-09 23:24 ieye
@xylz
我觉得操作的中间结果应该还是在线程栈中的,这个计算结果又没有必要对其他线程可见,只有写回主存的时候才对其他线程可见。个人理解  回复  更多评论
  

# re: 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则 2011-08-02 08:36 喜乐
@ieye
并且写回主存的时候不是原子的, 这就是问题所在。 比如一个LONG, 在32位机上可以写高位也可以先写低位。  回复  更多评论
  

# re: 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则 2011-08-09 14:07 那时花开
在多核的环境中,高速缓存有多个,如果这个变量不在主存存放的话,那么线程读到的值就可能是错的,未及时同步的。  回复  更多评论
  

# re: 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则 2011-11-24 17:03 duanjb
我认为局部变量才是保持在栈里面的,对于全局变量则应该是放在堆里面了!要不然没法保证可见性。
volatile boolean done = false;



while( ! done ){
dosomething();
}

对于这个问题,<<effective java>中说明如果不加volatile,程序会被jvm优化为
if(!done)
{
while(true)
{
dosomething();
}
}
这意味一旦进入到while中就会死循环。。。。。。
但是在多核cpu下,我的测试是就算进入了while循环也会停止掉!
http://www.iteye.com/topic/1118193
这个帖子有不少人讨论  回复  更多评论
  

# re: 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则 2012-01-19 10:35 jasonlmq
申明为volatile的属性变量,是存储在堆的主存中。无论任何线程访问此变量都是直接从主内存中读取,不是从线程自己的堆中取,更不是从线程的栈中读取。  回复  更多评论
  

# re: 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则 2012-10-25 12:34 vitamin.x
@thebye85
虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操作需要使 x 的值在操作期间保持不变,而 volatile 变量无法实现这点。  回复  更多评论
  

# re: 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则 2013-01-08 18:26 mgampkay
对volatile变量的读会从内存读入寄存器,写会把寄存器内容写回内存。
可见性就是指我修改后你再读,你读到的就是我修改后的内容。

例如有一个volatile变量a,初值为0,依次执行a = 1; a = 2;这样就有两次对内存的写入。在a = 1后,如果有线程读a,就会从内存里都到1。

对非volatile变量b, 依次执行b = 1; b = 2; 第一次修改可能只是对寄存器修改,而没有写回内存。这样执行b = 1后,另一个线程读b就会都到内存中的0。

所以说volatile变量可以保证可见性。  回复  更多评论
  

# re: 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则[未登录] 2013-03-03 14:34 teasp
博主关于hanppens-before规则的第5条有错误。  回复  更多评论
  


只有注册用户登录后才能发表评论。


网站导航:
 

©2009-2014 IMXYLZ