并发编程之 Java 内存模型 + volatile 关键字 + Happen-Before 规则

简介: 并发编程之 Java 内存模型 + volatile 关键字 + Happen-Before 规则

1. Java 内存模型到底是什么玩意?

内存模型可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。不同架构的CPU 有不同的内存模型。

Java 作为跨平台语言,肯定要屏蔽不同CPU内存模型的差异,构造自己的内存模型,这就是Java 的内存模型。实际上,根源来自硬件的内存模型。

还是看这个图片,Java 的内存模型和硬件的内存模型几乎一样,每个线程都有自己的工作内存,类似CPU的高速缓存,而 java 的主内存相当于硬件的内存条。

Java 内存模型也是抽象了线程访问内存的过程。

JMM(Java 内存模型)规定了所有的变量都存储在主内存(这个很重要)中,包括实例字段,静态字段,和构成数据对象的元素,但不包括局部变量和方法参数,因为后者是线程私有的。不会被共享。自然就没有竞争问题。

什么是工作内存呢?每个线程都有自己的工作内存(这个很重要),线程的工作内存保存了该线程使用到的变量和主内存副本拷贝,线程对变量的所有操作(读写)都必须在工作内存中进行。而不能直接读写主内存中的变量。不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。

总结一下,Java 内存模型定义了两个重要的东西,1.主内存,2.工作内存。每个线程的工作内存都是独立的,线程操作数据只能在工作内存中计算,然后刷入到主存。这是 Java 内存模型定义的线程基本工作方式。

2. Java 内存模型定义了哪些东西?

实际上,整个 Java 内存模型围绕了3个特征建立起来的。这三个特征是整个Java并发的基础。

原子性,可见性,有序性

原子性(Atomicity)

什么是原子性,其实这个原子性和事务处理中的原子性定义基本是一样的。指的是一个操作是不可中断的,不可分割的。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

我们大致可以认为基本数据类型的访问读写是具备原子性的(但是,如果你在32位虚拟机上计算 long 和 double 就不一样了),因为 java 虚拟机规范中,对 long 和 double 的操作没有强制定义要原子性的,但是强烈建议使用原子性的。因此,大部分商用的虚拟机基本都实现了原子性。

如果用户需要操作一个更到的范围保证原子性,那么,Java 内存模型提供了 lock 和 unlock (这是8种内存操操作中的2种)操作来满足这种需求,但是没有提供给程序员这两个操作,提供了更抽象的 monitorenter 和 moniterexit 两个字节码指令,也就是 synchronized 关键字。因此在 synchronized 块之间的操作都是原子性的。

可见性(Visibility)

可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改,Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值,这种依赖主内存作为传递媒介的方式来实习那可见性的。无论是普通变量还是 volatile 变量都是如此。他们的区别在于:volatile 的特殊规则保证了新值能立即同步到主内存,以及每次是使用前都能从主内存刷新,因此,可以说 volatile 保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。

除了 volatile 之外, synchronized 和 final 也能实现可见性。同步块的可见性是由 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存种(执行 store, write 操作)。

有序性(Ordering)

有序性这个问题我们在最上面说硬件的时候说过,CPU 会调整指令顺序,同样的 Java 虚拟机同样也会调整字节码顺序,但这种调整在单线程里时感知不到的,除非在多线程程序中,这种调整会带来一些意想不到的错误。

Java 提过了两个关键字来保证多个线程之间操作的有序性,volatile 关键字本身就包含了禁止重排序的语义,而 synchronized 则是由 “一个变量同一时刻只允许一条线程对其进行 lock 操作”这个规则获得的。这条规则决定了同一个锁的两个同步块只能串行的进入。

好了,介绍完了 JMM 的三种基本特征。不知道大家有没有发现,volatile 保证了可见性和有序性,synchronized 则3个特性都保证了,堪称万能。而且 synchronized 使用方便。但是,仍然要警惕他对性能的影响。

3. Java内存模型引出的 Happen-Before 原则是什么?

说到有序性,注意,我们说有序性可以通过 volatile 和 synchronized 来实现,但是我们不可能所有的代码都靠这两个关键字。实际上,Java 语言已对重排序或者说有序性做了规定,这些规定在虚拟机优化的时候是不能违背的。

1. 程序次序原则:一个线程内,按照程序代码顺序,书写在前面的操作先发生于书写在后面的操作。

2. volatile 规则:volatile 变量的写,先发生于读,这保证了 volatile 变量的可见性。

3. 锁规则:解锁(unlock) 必然发生在随后的加锁(lock)前。

4. 传递性:A先于B,B先于C,那么A必然先于C。

5. 线程的 start 方法先于他的每一个动作。

6. 线程的所有操作先于线程的终结。

7. 线程的中断(interrupt())先于被中断的代码。

8. 对象的构造函数,结束先于 finalize 方法。

4. Happen-Before 引出的 volatile 又是什么?

我们在前面,说了很多的 volatile 关键字,可见这个关键字非常的重要,但似乎他的使用频率比 synchronized

少多了,我们知道了这个关键字可以做什么呢?

volatile 可以实现线程的可见性,还可以实现线程的有序性。但是不能实现原子性。

我们还是直接写一段代码吧!

package cn.think.in.java.two;
/**
 * volatile 不能保证原子性,只能遵守 hp 原则 保证单线程的有序性和可见性。
 */
public class MultitudeTest {
  static volatile int i = 0;
  static class PlusTask implements Runnable {
    @Override
    public void run() {
      for (int j = 0; j < 10000; j++) {
//        plusI();
        i++;
      }
    }
  }
  public static void main(String[] args) throws InterruptedException {
    Thread[] threads = new Thread[10];
    for (int j = 0; j < 10; j++) {
      threads[j] = new Thread(new PlusTask());
      threads[j].start();
    }
    for (int j = 0; j < 10; j++) {
      threads[j].join();
    }
    System.out.println(i);
  }
//  static synchronized void plusI() {
//    i++;
//  }
}

 

我们启动了10个线程分别对一个 int 变量进行 ++ 操作,注意,++ 符号不是原子的。然后,主线程等待在这10个线程上,执行结束后打印 int 值。你会发现,无论怎么运行都到不了10000,因为他不是原子的。怎么理解呢?

i++ 等于 i = i + 1;

虚拟机首先读取 i 的值,然后在 i 的基础上加1,请注意,volatile 保证了线程读取的值是最新的,当线程读取 i 的时候,该值确实是最新的,但是有10个线程都去读了,他们读到的都是最新的,并且同时加1,这些操作不违法 volatile 的定义。最终出现错误,可以说是我们使用不当。

楼主也在测试代码中加入了一个同步方法,同步方法能够保证原子性。当for循环中执行的不是i++,而是 plusI 方法,那么结果就会准确了。

volatile 确实不能保证原子性,但是能保证有序性和可见性。那么是怎么实现的呢?

怎么保证有序性呢?实际上,在操作 volatile 关键字变量前后的汇编代码中,会有一个 lock 前缀,根据 intel IA32 手册,lock 的作用是 使得 本 CPU 的Cache 写入了内存,该写入动作也会引起别的CPU或者别的内核无效化其Cache,别的CPU需要重新获取Cache。这样就实现了可见性。可见底层还是使用的 CPU 的指令

如何实现有序性呢?同样是lock 指令,这个指令还相当于一个内存屏障(大多数现代计算机为了提高性能而采取乱序执行,这使得内存屏障成为必须。语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障),指的是,重排序时不能把后面的指令重排序到内存屏障之前的位置。只有一个CPU访问内存时,并不需要内存屏障;但如果有两个或者更多CPU访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证了。

因此请不要随意使用 volatile 变量,这会导致 JIT 无法优化代码,并且会插入很多的内存屏障指令,降低性能。

5.使用volatile关键字的场景

class Singleton{
    private volatile static Singleton instance = null;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

至于为何需要这么写请参考:《Java 中的双重检查(Double-Check)》  http://www.iteye.com/topic/652440

6. 总结

首先 JMM 是抽象化了硬件的内存模型(使用了多级缓存导致出现缓存一致性协议),屏蔽了各个 CPU 和操作系统的差异。

Java 内存模型指的是:在特定的协议下对内存的访问过程。也就是线程的工作内存和主存直接的操作顺序。

JMM 主要围绕着原子性,可见性,有序性来设置规范。

synchronized 可以实现这3个功能,而 volatile 只能实现可见性和有序性。final 也能是实现可见性。

Happen-Before 原则规定了哪些是虚拟机不能重排序的,其中包括了锁的规定,volatile 变量的读与写规定。

而 volatile 我们也说了,不能保证原子性,所以使用的时候需要注意。volatile 底层的实现还是 CPU 的 lock 指令,通过刷新其余的CPU 的Cache 保证可见性,通过内存栅栏保证了有序性。

volatile关键字内存栅栏有两层意思:

  1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

  2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

下面这篇文章从实战角度专门探索 volatile 关键字原理

并发编程之volatile 关键字白话文解读_击水三千里的专栏-CSDN博客


相关文章
|
13天前
|
存储 Java 编译器
Java内存模型(JMM)深度解析####
本文深入探讨了Java内存模型(JMM)的工作原理,旨在帮助开发者理解多线程环境下并发编程的挑战与解决方案。通过剖析JVM如何管理线程间的数据可见性、原子性和有序性问题,本文将揭示synchronized关键字背后的机制,并介绍volatile关键字和final关键字在保证变量同步与不可变性方面的作用。同时,文章还将讨论现代Java并发工具类如java.util.concurrent包中的核心组件,以及它们如何简化高效并发程序的设计。无论你是初学者还是有经验的开发者,本文都将为你提供宝贵的见解,助你在Java并发编程领域更进一步。 ####
|
8天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
29 6
|
12天前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
33 2
|
13天前
|
存储 安全 Java
什么是 Java 的内存模型?
Java内存模型(Java Memory Model, JMM)是Java虚拟机(JVM)规范的一部分,它定义了一套规则,用于指导Java程序中变量的访问和内存交互方式。
35 1
|
1月前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
36 2
|
4月前
|
存储 安全 Java
Java面试题:深入探索Java内存模型,Java内存模型中的主内存与工作内存的概念,Java内存模型中的happens-before关系,volatile关键字在Java内存模型中的作用
Java面试题:深入探索Java内存模型,Java内存模型中的主内存与工作内存的概念,Java内存模型中的happens-before关系,volatile关键字在Java内存模型中的作用
37 1
|
4月前
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
45 0
|
4月前
|
缓存 安全 Java
Java面试题:解释volatile关键字的作用,以及它如何保证内存的可见性
Java面试题:解释volatile关键字的作用,以及它如何保证内存的可见性
77 4
|
4月前
|
设计模式 缓存 安全
Java面试题:工厂模式与内存泄漏防范?线程安全与volatile关键字的适用性?并发集合与线程池管理问题
Java面试题:工厂模式与内存泄漏防范?线程安全与volatile关键字的适用性?并发集合与线程池管理问题
57 1
|
5月前
|
缓存 安全 Java
深入理解java中的volatile关键字
深入理解java中的volatile关键字
96 1