Java并发编程--原子性和可见性

原子性

竞态条件:当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。常见的竞态条件类型是先检查后执行操作,即通过一个可能失效的观测结果来决定下一步的动作。

要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是修改状态的过程中。

在java.util.concurrent.atomic包中包含了一些原子变量类,用于实现在数值和对象引用上的原子状态转换。

Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的(忽略long和double)。如果应用场景需要一个更大范围的原子性保证,还提供了lock和unlock的操作,就是同步块。

可见性

保证内存可见性就是希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。

在一个单线程程序中,如果首先改变一个变量的值,再读取该变量的值的时候,所读取到的值就是上次写操作写入的值。也就是说前面操作的结果对后面的操作是肯定可见的。但是在多线程程序中,如果不使用一定的同步机制,就不能保证一个线程所写入的值对另外一个线程是可见的。造成这种情况的原因可能有下面几个:

  1. CPU 内部的缓存:现在的CPU一般都拥有层次结构的几级缓存。CPU直接操作的是缓存中的数据,并在需要的时候把缓存中的数据与主存进行同步。因此在某些时刻,缓存中的数据与主存内的数据可能是不一致的。某个线程所执行的写入操作的新值可能当前还保存在CPU的缓存中,还没有被写回到主存中。这个时候,另外一个线程的读取操作读取的就还是主存中的旧值。
  2. CPU的指令执行顺序:在某些时候,CPU可能改变指令的执行顺序。这有可能导致一个线程过早的看到另外一个线程的写入操作完成之后的新值。
  3. 编译器代码重排:出于性能优化的目的,编译器可能在编译的时候对生成的目标代码进行重新排列。

事实上,在没有同步的情况下,编译器、处理器以及运行时都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得到正确的结论。

Java内存模型通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性。volatile保证新值能立即同步到主内存,每次使用前立即从主内存刷新。volatile,sychronized和final都可以实现可见性。synchronized可见性是通过对一个变量执行unlock操作之前,必须把变量同步回主内存实现的。final修饰的字段在构造器一旦初始化完成个,并且构造器没有把this应用传递出去,其他线程就能看见final字段的值。

volatile变量

关键字volatile是java虚拟机提供的最轻量级的同步机制。变量被定义为volatile之后,具备两种特性,第一是保证此变量对所有线程的可见性。在各个线程的工作内存中,volatile变量也可以存在不一致的情况,但是由于每次使用之前都要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题。但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的。第二个是禁止指令重排序优化。所谓的重排序优化是机器级的优化操作,提前执行是指这句话对应的汇编代码被提前执行。volatile操作相当于一个内存屏障,重排序时不能把后面的指令重排序到内存屏障之前的位置。

大多数场景下,volatile的总开销仍然比锁低,读取volatile变量的开销只比读取非volatile变量的开销略高一点。我们在volatile与锁之中选择的唯一依据仅仅是volatile的语义能否满足使用场景的需求。

当且仅当以下所有条件满足时,才应该使用volatile变量:

1)对变量的写入操作不依赖变量的当前值,或者保证只有单个线程更新变量

2)该变量不会与其他状态变量一起纳入不变性条件中

3)在访问变量时不需要加锁

阅读更多
个人分类: java并发编程
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭