`
qqdwll
  • 浏览: 136713 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

正确使用 Volatile 变量

    博客分类:
  • Java
阅读更多
Java™ 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量。这两种机制的提出都是为了实现代码线程的安全性。其中 Volatile 变量的同步性较差(但有时它更简单并且开销更低),而且其使用也更容易出错。

Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。

锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。

关于volatile和synchronized

volatile是一个变量修饰符,而synchronized是一个方法或块的修饰符。所以我们使用这两种关键字来指定三种简单的存取变量的方式。

         int i1;                       int geti1() {return i1;}

volatile int i2;                       int geti2() {return i2;}

          int i3;          synchronized int geti3() {return i3;}

geti1()在当前线程中立即获取在i1变量中的值。线程可以获得变量的本地拷贝,而所获得的变量的值并不一定与其他线程所获得的值相同。特别是,如果其他的线程修改了i1的值,那么当前线程获得的i1的值可能与修改后的值有所差别。实际上,Java有一种主内存的机制,使用一个主内存来保存变量当前的正确的值。线程将变量的值拷贝到自己独立的内存中,而这些线程的内存拷贝可能与主内存中的值不同。所以实际当中可能发生这样的情况,在主内存中i1的值为1,线程1和线程2都更改了i1,但是却没把更新的值传回给主内存或其他线程中,那么可能在线程1中i1的值为2,线程2中i1的值却为3。

另一方面,geti2()可以有效的从主内存中获取i2的值。一个volatile类型的变量不允许线程从主内存中将变量的值拷贝到自己的存储空间。因此,一个声明为volatile类型的变量将在所有的线程中同步的获得数据,不论你在任何线程中更改了变量,其他的线程将立即得到同样的结果。由于线程存取或更改自己的数据拷贝有更高的效率,所以volatile类型变量在性能上有所消耗。

那么如果volatile变量已经可以使数据在线程间同步,那么synchronizes用来干什么呢?两者有两方面的不同。首先,synchronized获取和释放由监听器控制的锁,如果两个线程都使用一个监听器(即相同对象锁),那么监听器可以强制在一个时刻只有一个线程能处理代码块,这是最一般的同步。另外,synchronized还能使内存同步。在实际当中,synchronized使得所有的线程内存与主内存相同步。所以geti3()的执行过程如下:

1.    线程从监听器获取对象的锁。(这里假设监听器非锁,否则线程只有等到监听器解锁才能获取对象锁)

2.    线程内存更新所有的变量,也就是说他将读取主内存中的变量使自己的变量保证有效。(JVM会使用一个“脏”标志来最优化过程,使得仅仅具有“脏”标志变量被更新。详细的情况查询JAVA规范的17.9)

3.    代码块被执行(在这个例子中,设置返回值为刚刚从主内存重置的i3当前的值。)

4.    任何变量的变更将被写回到主内存中。但是这个例子中geti3()没有什么变化。

5.    线程释放对象的锁给监听器。

所以volatile只能在线程内存和主内存之间同步一个变量的值,而synchronized则同步在线程内存和主内存之间的所有变量的值,并且通过锁住和释放监听器来实现。显然,synchronized在性能上将比volatile更加有所消耗。


volatile的特点

为什么使用volatile ? 比同步代价更低?
同步的代价, 主要由其覆盖范围决定, 如果可以降低同步的覆盖范围, 则可以大幅提升程序性能.

而volatile的覆盖范围仅仅变量级别的. 因此它的同步代价很低.

volatile原理是什么?
volatile的语义, 其实是告诉处理器, 不要将我放入工作内存, 请直接在主存操作我.(工作内存详见java内存模型)

因此, 当多核或多线程在访问该变量时, 都将直接操作主存, 这从本质上, 做到了变量共享.

volatile的有什么优势?
1, 更大的程序吞吐量
2, 更少的代码实现多线程
3, 程序的伸缩性较好
4, 比较好理解, 无需太高的学习成本

volatile有什么劣势?
1, 容易出问题
2, 比较难设计

volatile运算存在脏数据问题

volatile仅仅能保证变量可见性, 无法保证原子性.

volatile的race condition示例:

public class TestRaceCondition {
    private volatile int i = 0;
    
    public void increase() {
       i++;
    }

    public int getValue() {
       return i;
    }
}


当多线程执行increase方法时, 是否能保证它的值会是线性递增的呢?

答案是否定的.

原因:
这里的increase方法, 执行的操作是i++, 即 i = i + 1;
针对i = i + 1, 在多线程中的运算, 本身需要改变i的值.
如果, 在i已从内存中取到最新值, 但未与1进行运算, 此时其他线程已数次将运算结果赋值给i.
则当前线程结束时, 之前的数次运算结果都将被覆盖.

即, 执行100次increase, 可能结果是 < 100.
一般来说, 这种情况需要较高的压力与并发情况下, 才会出现.

如何避免这种情况?
解决以上问题的方法:
一种是 操作时, 加上同步. 比如在写上加同步, 读不加。

第二种方式是, 使用硬件原语(CAS), 实现非阻塞算法
从CPU原语上,  支持变量级别的低开销同步.

CPU原语-比较并交换(CompareAndSet),实现非阻塞算法

什么是CAS?
cas是现代CPU提供给并发程序使用的原语操作. 不同的CPU有不同的使用规范.

在 Intel 处理器中,比较并交换通过指令的 cmpxchg 系列实现。
PowerPC 处理器有一对名为“加载并保留”和“条件存储”的指令,它们实现相同的目地;
MIPS 与 PowerPC 处理器相似,除了第一个指令称为“加载链接”。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)

什么是非阻塞算法?

一个线程的失败或挂起不应该影响其他线程的失败或挂起.这类算法称之为非阻塞(nonblocking)算法

对比阻塞算法:
如果有一类并发操作, 其中一个线程优先得到对象监视器的锁, 当其他线程到达同步边界时, 就会被阻塞.
直到前一个线程释放掉锁后, 才可以继续竞争对象锁.(当然,这里的竞争也可是公平的, 按先来后到的次序)

CAS 原理:
我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。

CAS使用示例(jdk 1.5 并发包 AtomicInteger类分析)

 /**
     * Atomically sets to the given value and returns the old value.
     *
     * @param newValue the new value
     * @return the previous value
     */
    public final int getAndSet(int newValue) {
        for (;;) {
            int current = get();
            if (compareAndSet(current, newValue))
                return current;
        }
    }

    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }


这个方法是, AtomicInteger类的常用方法, 作用是, 将变量设置为指定值, 并返回设置前的值.
它利用了cpu原语compareAndSet来保障值的唯一性.

另, AtomicInteger类中, 其他的实用方法, 也是基于同样的实现方式.
比如 getAndIncrement, getAndDecrement, getAndAdd等等.


CAS语义上存在的"ABA 问题"

什么是ABA问题?

假设, 第一次读取V地址的A值, 然后通过CAS来判断V地址的值是否仍旧为A, 如果是, 就将B的值写入V地址,覆盖A值.

但是, 语义上, 有一个漏洞, 当第一次读取V的A值, 此时, 内存V的值变为B值, 然后在未执行CAS前, 又变回了A值.
此时, CAS再执行时, 会判断其正确的, 并进行赋值.

这种判断值的方式来断定内存是否被修改过, 针对某些问题, 是不适用的.

为了解决这种问题, jdk 1.5并发包提供了AtomicStampedReference(有标记的原子引用)类, 通过控制变量值的版本来保证CAS正确性.


其实, 大部分通过值的变化来CAS, 已经够用了.

jdk1.5原子包介绍(基于volatile)
包的特色:
1, 普通原子数值类型AtomicInteger, AtomicLong提供一些原子操作的加减运算.

2, 使用了解决脏数据问题的经典模式-"比对后设定", 即 查看主存中数据是否与预期提供的值一致,如果一致,才更新.

3, 使用AtomicReference可以实现对所有对象的原子引用及赋值.包括Double与Float,
但不包括对其的计算.浮点的计算,只能依靠同步关键字或Lock接口来实现了.

4, 对数组元素里的对象,符合以上特点的, 也可采用原子操作.包里提供了一些数组原子操作类
AtomicIntegerArray, AtomicLongArray等等.

5, 大幅度提升系统吞吐量及性能.


直接使用volatile的情形。

IBM的java 理论与实践 中介绍了一些直接使用的情况。  请参阅: http://www.ibm.com/developerworks/cn/java/j-jtp06197.html

原子操作都是线程安全的?
java中原子操作是线程安全的论调经常被提到。根据定义,原子操作是不会被打断地的操作,因此被认为是线程安全的。实际上有一些原子操作不一定是线程安全的。

这个问题出现的原因是尽量减少在代码中同步关键字。同步会损害性能,虽然这个损失因JVM不同而不同。另外,在现代的JVM中,同步的性能正在逐步提高。尽管如此,使用同步仍然是有性能代价的,并且程序员永远会尽力提高他们的代码的效率,因此这个问题就延续了下来。

在java 中,32位或者更少位数的赋值是原子的。在一个32位的硬件平台上,除了double和long型的其它原始类型通常都是使用32位进行表示,而 double和long通常使用64位表示。另外,对象引用使用本机指针实现,通常也是32位的。对这些32位的类型的操作是原子的。

这些原始类型通常使用32位或者64位表示,这又引入了另一个小小的神话:原始类型的大小是由语言保证的。这是不对的。java语言保证的是原始类型的表数范围而非JVM中的存储大小。因此,int型总是有相同的表数范围。在一个JVM上可能使用32位实现,而在另一个JVM上可能是64位的。在此再次强调:在所有平台上被保证的是表数范围,32位以及更小的值的操作是原子的。

那么,原子操作在什么情况下不是线程安全的?主要的一点是他们也许确实是线程安全的,但是这没有被保证!java线程允许线程在自己的内存区保存变量的副本。允许线程使用本地的私有拷贝进行工作而非每次都使用主存的值是为了提高性能。考虑下面的类:

class RealTimeClock 
{ 
 private int clkID; 
 public int clockID() 
 { 
  return clkID; 
 } 
 public void setClockID(int id) 
 { 
  clkID = id;
 } 
//... 
} 



现在考虑RealTimeClock的一个实例以及两个线程同时调用setClockID和clockID,并发生以下的事件序列:

T1 调用setClockID(5)
T1将5放入自己的私有工作内存
T2调用setClockID(10)
T2将10放入自己的私有工作内存
T1调用clockID,它返回5
5是从T1的私有工作内存返回的

对clockI的调用应该返回10,因为这是被T2设置的,然而返回的是5,因为读写操作是对私有工作内存的而非主存。赋值操作当然是原子的,但是因为JVM允许这种行为,因此线程安全不是一定的,同时,JVM的这种行为也不是被保证的。

两个线程拥有自己的私有拷贝而不和主存一致。如果这种行为出现,那么私有本机变量和主存一致必须在以下两个条件下 (其实就是要加上  可见性):

1、变量使用volatile声明
2、被访问的变量处于同步方法或者同步块中

如果变量被声明为volatile,在每次访问时都会和主存一致。这个一致性是由java语言保证的,并且是原子的,即使是64位的值。(注意很多JVM没有正确的实现volatile关键字。你可以在www.javasoft.com找到更多的信息。)另外,如果变量在同步方法或者同步块中被访问,当在方法或者块的入口处获得锁以及方法或者块退出时释放锁是变量被同步。
使用任何一种方法都可以保证ClockID返回10,也就是正确的值。变量访问的频度不同则你的选择的性能不同。如果你更新很多变量,那么使用volatile可能比使用同步更慢。记住,如果变量被声明为volatile,那么在每次访问时都会和主存一致。与此对照,使用同步时,变量只在获得锁和释放锁的时候和主存一致。但是同步使得代码有较少的并发性。

如果你更新很多变量并且不想有每次访问都和主存进行同步的损失或者你因为其它的原因想排除并发性时可以考虑使用同步。
分享到:
评论

相关推荐

    java入门教程:数据类型_Java理论与实践如何正确使用Volatile变量.docx

    ### Java入门教程:数据类型与正确使用Volatile变量 #### 概述 在Java编程语言中,`volatile`关键字提供了一种轻量级的同步机制,用于确保共享变量的可见性和一定程度上的线程安全性。相比于传统的锁机制如`...

    Java 理论与实践: 正确使用 volatile 变量 线程同步

     使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。  由于使用volatile屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此...

    volatile变量详解

    在编程中,需要正确地理解 volatile 变量的作用和应用场景,以确保程序的正确性和效率。 此外,需要注意的是,volatile 变量的解释不应该是“易变的”,而是“直接存取原始内存地址”。“易变”是因为外在因素引起...

    stm32 volatile变量的正确使用

    ### STM32中Volatile变量的正确使用 #### 概述 在嵌入式系统编程中,`volatile`关键字的正确使用对于确保程序的稳定性和可靠性至关重要。它主要用于标记那些可能在程序运行过程中被外部因素(如硬件中断、多线程...

    Java线程:volatile关键字

    正确使用 volatile 变量的条件是:对变量的写操作不依赖于当前值,以及该变量没有包含在具有其他变量的不变式中。在实际应用中,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量...

    volatile 变量的说明

    3. **volatile变量的错误使用示例**: ``` int square(volatile int *ptr) { return *ptr * *ptr; } ``` 这个函数有问题,因为它没有考虑到volatile变量的特性。在多线程环境下,*ptr的值可能在两次读取之间...

    详解java如何正确使用volatile

    这使得volatile变量在多线程环境下具有了一定程度的线程安全性。 然而,值得注意的是,`volatile`并不保证原子性。这意味着如果一个操作包含多个步骤(如`t++`操作),即使这个操作涉及的变量是volatile的,也不能...

    深入探讨Java多线程中的volatile变量

    Java多线程中的volatile变量是...理解和正确使用volatile变量,可以有效地提高多线程程序的性能和正确性。然而,需要注意的是,它不能完全替代锁或其他同步手段,开发者需要根据具体的应用场景选择合适的并发控制策略。

    volatile变量的的概念和使用

    本文将深入探讨`volatile`变量的概念、作用以及如何在实际编程中使用。 `volatile`变量的主要概念是它表明该变量的值可能在编译器无法预测的情况下发生变化。这种变化可能是由操作系统、硬件中断、多线程环境中的...

    深入探讨Java多线程中的volatile变量共6页.pd

    本资料《深入探讨Java多线程中的volatile变量》将带你深入理解这个概念,全面解析其工作原理和实际应用。 volatile关键字在Java中主要用于解决多线程环境下的可见性和有序性问题。它确保了被volatile修饰的变量对...

    volatile的使用方法

    由于我们使用了 volatile 关键字,所以编译器不会对变量 var 进行优化,从而确保了变量 var 的值是正确的。 volatile 关键字的使用方法可以分为两种情况:一是用来指定变量的存储类别,二是用来避免编译器的优化。...

    Java并发编程(5)volatile变量修饰符-意料之外

    在Java编程语言中,`volatile`关键字是一个非常重要的并发控制机制,它被用来修饰类的成员变量,确保这些变量在多线程环境下的可见性和有序性。然而,使用`volatile`并非总是如我们所期望的那样简单,有时会出现一些...

    Java Volatile 变量详解及使用方法

    例如,`NumberRange`类中的`lower`和`upper`就不能仅仅通过`volatile`保证线程安全,还需要使用`synchronized`关键字或`AtomicInteger`等原子类来保证不变式的正确性。 总结来说,`volatile`变量在Java多线程编程中...

    volatile和原子操作有没有关系,我的实验+别人论文

    在单核环境中,使用volatile关键字并不能确保变量的值是正确的,而使用原子操作可以确保数据的一致性。 volatile关键字和原子操作都是多线程编程中的重要概念。volatile关键字用于告诉编译器不要对变量进行优化,而...

    Java中Volatile的作用实例解析

    正确使用 volatile 变量的条件您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件: 1. 对变量的写操作不依赖于当前值。 2. 该变量没有包含在...

    Linux C中多线程与volatile变量

    在Linux C编程中,多线程编程是一种常见的并发执行方式,允许程序同时执行多个任务,提高系统的效率...理解`volatile`的关键在于它是如何影响编译器的行为,以及如何在多线程环境中正确地使用它来实现有效的数据同步。

    Volatile详解,深入学习Volatile

    - 防止编译器优化:编译器通常会优化掉被认为不变的变量的读取,但volatile变量告诉编译器不要做这样的优化,每次使用时都从内存中读取。 - 多线程同步:在多线程环境中,当一个线程修改了volatile变量,其他线程...

    Java并发编程之volatile变量介绍

    在使用volatile变量时,有三个关键的使用条件: 1. **写入操作不依赖于当前值**:如果一个线程更新volatile变量的值,并不依赖于变量当前的值,那么可以使用volatile。例如,设置一个标志位来标记某个任务是否完成...

    ios多线程编程文档(翻译版)

    使用原子操作和锁是常见的同步手段,而正确使用Volatile变量也是保证线程安全的重要技巧。 RUNLOOPS章节介绍了RunLoop的内部结构,包括RunLoop模式和输入源。RUNLOOP对象用于配置、启动和退出RunLoop,以及配置源...

Global site tag (gtag.js) - Google Analytics