Java的volatile到底该如何理解?(上)

简介: Java的volatile到底该如何理解?(上)

volatile 的实现维度

级别 实现
Java 代码 volatile int i
ByteCode 字节码 ACC_VOLATILE
JVM 虚拟机规范 JVM 内存屏障
HotSpot 实现 汇编语言调用
CPU 级别 MESI 原语支持总线锁

可见性问题

让一个线程对共享变量的修改,能够及时的被其他线程看到。

根据JMM中规定的happen before和同步原则:

对某个volatile字段的写操作happens- before每个后续对该volatile字段的读操作。

对volatile变量v的写入,与所有其他线程后续对v的读同步


要满足这些条件,所以volatile关键字就有这些功能:


禁止缓存;

volatile变量的访问控制符会加个ACC_VOLATILE

对volatile变 量相关的指令不做重排序

volatile 变量可以被看作是一种 "轻量的 synchronized,可算是JVM提供的最轻量级的同步机制。


当一个变量定义为volatile后,可以保证此变量对所有线程的可见性。

原子性(Atomicity)

一次只允许一个线程持有某锁,一次只有一个线程能使用共享数据

由JMM直接保证的原子性变量操作包括read、load、use、assign、store和write六个,大致可以认为基础数据类型的访问读写是原子性的


如果应用场景需要一个更大范围的原子性保证,JMM还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock与unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐匿地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块synchronized关键字,因此在synchronized块之间的操作也具备原子性

可见性(Visibility)

当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。

由于现代可共享内存的多处理器架构可能导致一个线程无法马上看到另一个线程操作产生的结果。所以 Java 内存模型规定了 JVM 的一种最小保证:什么时候写入一个变量对其他线程可见。


在现代可共享内存的多处理器体系结构中每个处理器都有自己的缓存,并周期性的与主内存协调一致。假设线程 A 写入一个变量值 V,随后另一个线程 B 读取变量 V 的值

在下列情况下,线程 B 读取的值可能不是线程 A 写入的最新值:


执行线程 A 的处理器把变量 V 缓存到寄存器中。

执行线程 A 的处理器把变量 V 缓存到自己的缓存中,但还没有同步刷新到主内存中去。

执行线程 B 的处理器的缓存中有变量 V 的旧值。

JMM通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式实现可见性,无论是普通变量还是volatile变量都是如此。


普通变量与volatile变量的区别:

volatile保证新值能立即同步到主内存,以及每使用前立即从内存刷新。

因此volatile保证了线程操作时变量的可见性,而普通变量则不保证。


除了volatile,Java还有两个关键字能实现可见性:

synchronized

由“对一个变量执行unlock前,必须先把此变量同步回主内存中(执行storewrite)”这条规则获得的

final

被final修饰的字段,在构造器一旦初始化完成,并且构造器没有把this引用传递出去(this引用逃逸是一件危险事情,其它线程可能通过该引用访问到初始化了一半的对象),那在其他线程中就能看见final字段值。

final在该对象的构造器设置对象的字段,当线程看到该对象时,将始终看到该对象的final字段的正确构造版本。

伪代码示例:

f = new finalDemo();

读取到的 f.x 一定最新,x为final字段。

若在构造器设置字段后发生读取,则会看到该final字段分配的值,否则它将看到默认值;

伪代码示例:

public finalDemo() {
  x=1;
  y=x;
};

y会等于1。

读取该共享对象的final成员变量之前,先要读取共享对象。

伪代码示例:

r = new ReferenceObj(); 
k = r.f; 

这两个操作不能重排序

通常static final是不可修改的字段,然而System.in、System.out和System.err 都是static final字段,遗留原因,必须允许通过set方法改变,我们将这些字段称为写保护,以区别于普通final字段:

9.png

10.png

11.png

12.png

必须确保释放锁之前对共享数据做出的更改,对于随后获得该锁的另一个线程可见,对域中的值做赋值和返回的操作通常是原子性的,但递增/减并不是。


volatile对所有线程立即可见,对volatile变量所有的写操作都能立即返回到其它线程,即volatile变量在各个线程中是一致的,但并非基于volatile变量的运算在并发下是安全的。


volatile变量在各线程的工作内存中不存在一致性问题(在各个线程的工作内存中volatile变量可存在不一致,但由于每次使用前都要先刷新,执行引擎看不到不一致的情况,因此可认为不存在一致性问题),但Java里的运算并非原子操作,导致volatile变量的运算在并发下一样不安全:

public class Atomicity {
  int i;
  void f(){
    i++;
  }
  void g(){
    i += 3;
  }
}

编译后文件

void f();
    0  aload_0 [this]
    1  dup
    2  getfield concurrency.Atomicity.i : int [17]
    5  iconst_1
    6  iadd
    7  putfield concurrency.Atomicity.i : int [17]
 // Method descriptor #8 ()V
 // Stack: 3, Locals: 1
 void g();
    0  aload_0 [this]
    1  dup
    2  getfield concurrency.Atomicity.i : int [17]
    5  iconst_3
    6  iadd
    7  putfield concurrency.Atomicity.i : int [17]
}

每个操作都产生了一个 get 和 put ,之间还有一些其它指令。因此在获取和修改之间,另一个线程可能会修改这个域。所以,这些操作不是原子性的。

再看下面这个例子是否符合上面的描述:

public class AtomicityTest implements Runnable {
    private int i = 0;
    public int getValue() {
      return i;
    }
    private synchronized void evenIncrement() {
      i++;
      i++;
    }
    public void run() {
      while(true)
        evenIncrement();
    }
    public static void main(String[] args) {
      ExecutorService exec = Executors.newCachedThreadPool();
      AtomicityTest at = new AtomicityTest();
      exec.execute(at);
      while(true) {
        int val = at.getValue();
        if(val % 2 != 0) {
          System.out.println(val);
          System.exit(0);
        }
      }
    }
}
output:
1

该程序将找到奇数值并终止。尽管return i原子性,但缺少同步使得其数值可以在处于不稳定的中间状态时被读取。由于 i 不是 volatile,存在可见性问题

getValue() 和 evenIncrement() 必须synchronized。


对于基本类型的读/写操作被认为是安全的原子性操作

但当对象处于不稳定状态时,仍旧很有可能使用原子性操作来访问他们

最明智的做法是遵循同步的规则


volatile 变量只保证可见性

在不符合以下条件规则的运算场景中,仍需加锁(使用synchronized或JUC原子类)保证原子性:


运算结果不依赖变量的当前值,或者能确保只有单一的线程修改变量的值

变量不需要与其它的状态变量共同参与不可变类约束

基本上,若一个域可能会被多个任务同时访问or这些任务中至少有一个是写任务,那就该将此域设为volatile

当一个域定义为 volatile 后,将具备


1.保证此变量对所有的线程的可见性,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,其它线程每次使用前立即从主内存刷新

但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成

2.禁止指令重排序。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置)

这些操作的目的是用线程中的局部变量维护对该域的精确同步

目录
相关文章
|
8月前
|
缓存 人工智能 安全
4. Java 的线程安全机制之`volatile`
4. Java 的线程安全机制之`volatile`
|
8月前
|
存储 Java
「Java面试」工作3年竟然回答不出如何理解Reentrantlock实现原理
一个3 年工作经验的小伙伴,在面试的时候被这样一个问题。”谈谈你对ReentrantLock实现原理的理解“,他当时零零散散的说了一些。但好像没有说关键点。希望我分享一下我的理解。
69 0
|
4天前
|
存储 缓存 安全
Java并发基础之互斥同步、非阻塞同步、指令重排与volatile
在Java中,多线程编程常常涉及到共享数据的访问,这时候就需要考虑线程安全问题。Java提供了多种机制来实现线程安全,其中包括互斥同步(Mutex Synchronization)、非阻塞同步(Non-blocking Synchronization)、以及volatile关键字等。 互斥同步(Mutex Synchronization) 互斥同步是一种基本的同步手段,它要求在任何时刻,只有一个线程可以执行某个方法或某个代码块,其他线程必须等待。Java中的synchronized关键字就是实现互斥同步的常用手段。当一个线程进入一个synchronized方法或代码块时,它需要先获得锁,如果
26 0
|
4天前
|
存储 安全 Java
[Java]volatile关键字
[Java]volatile关键字
31 0
|
4天前
|
存储 缓存 Java
Java volatile关键字-单例模式的双重锁为什么要加volatile
Java volatile关键字--单例模式的双重锁为什么要加volatile
54 10
|
4天前
|
Java 编译器 程序员
Volatile:Java并发编程的隐形英雄
Volatile:Java并发编程的隐形英雄
26 0
|
4天前
|
缓存 人工智能 安全
4. Java 的线程安全机制之`volatile`
4. Java 的线程安全机制之`volatile`
|
4天前
|
Java 编译器
Java多线程:什么是volatile关键字?
Java多线程:什么是volatile关键字?
32 0
|
4天前
|
SQL 缓存 安全
java中volatile关键字
java中volatile关键字
42 0
java中volatile关键字
|
4天前
|
Java
多线程与并发编程:解释什么是死锁,并给出一个在Java中发生死锁的例子。描述一下Java中的volatile关键字的作用,以及它与synchronized的区别。
多线程与并发编程:解释什么是死锁,并给出一个在Java中发生死锁的例子。描述一下Java中的volatile关键字的作用,以及它与synchronized的区别。
29 0