第三章_Java 内存模型

Happens - before 原则

happens-before 原则用来阐述操作之间的内存可见性,在 JMM 中,如果一个操作执行的结果需要对另外一个操作可见,那么这两个操作之间要存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

该原则有以下几种情况:

  1. 程序顺序规则

    一个线程中的每个操作,happens-before 于该线程中的任意后续操作。

  2. 监视器锁规则

    对一个锁的解锁,happens-before 与随后对这个锁的加锁。

  3. volatile 变量规则

    对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。

  4. 传递性

    如果 A happens-before B,且 B happens-before C,那么 A happens-before C。

注意:

两个操作之间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行。happens-before 仅仅要求前一个操作的执行结果对后一个操作可见,且前一个操作按顺序排在第二个操作之前。

重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

数据依赖性

但是这种重排序也不是为所欲为的,它需要遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

那么什么是数据依赖性呢?很简单,举个例子:

int a = 0;
a = 1;  //A
a = 2;  //B
System.out.println(a);

显然,如果 A 与 B 因为指令重排序导致操作颠倒,那么输出结果肯定变了,这里操作 A 与 B 就存在数据依赖性,这种情况下,编译器和处理器就不会重排序这两者的之间的操作。常见的存在数据依赖性的操作有:读后写、写后写、写后读。

As-if-serial 语义

该语义的意思是:不管怎么重排序,单线程程序的执行结果不能被改变,编译器和处理器都必须遵守 as-if-serial 语义。

为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

as-if-serial 语义把单线程程序保护了起来,遵守 as-if-serial 语义的编译器和处理器给我们创造了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial 语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。

volatile 的内存语义

volatile 变量自身具有以下特性:

  • 可见性

    对一个 volatile 变量的读,总能看到对这个 volatile 变量最后的写入。

  • 原子性

    对任意单个 volatile 变量的读写具有原子性,但类似于 volatile++ 这种符合操作不具有原子性。

volatile 变量所具备的可见性同样依托 happens-before 原则,但是,如果用内存模型来解释就更加通俗易懂:

当写一个 volatile 变量的时候,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。

由于 volatile 仅仅保证了对单个 volatile 变量的读写具有原子性和可见性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上,锁比 volatile 更强大,但在可伸缩性和执行性能上,volatile 更有优势。

锁的内存语义

锁是 Java 并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发生消息。

当线程释放锁时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存。

final 域的内存语义

不可变对象天生就是线程安全的。

但是需要注意 final 引用不能从构造函数内溢出。例子如下:

public class FinalDemo {
    final int anInt;
    static FinalDemo finalDemo;

    public FinalDemo() {
        this.anInt = 1;     //写final域
        finalDemo = this;   //this引用在此溢出
    }

    public static void writer() {
        new FinalDemo();
    }

    public static void reader() {
        if (finalDemo != null) {
            int temp = finalDemo.anInt;
            System.out.println(temp);
        }
    }
}

在构造函数返回前,被构造的对象的引用不能为其他线程所见,因为此时的 final 域可能还没有被初始化。在构造函数返回后,任意线程都能保证看到 final 域正确初始化之后的值。

错误的 DCL 写法的问题

public class SingleTon {
    private static SingleTon singleTon;

    public static SingleTon getInstance() {
        if (singleTon == null) {
            synchronized (SingleTon.class) {
                if (singleTon == null) {
                    singleTon = new SingleTon();    //问题所在
                }
            }
        }
        return singleTon;
    }

}

singleTon = new SingleTon() 可以分解如下伪代码:

memory = allocate();    //1.分配对象的内存空间
ctorInstance(memory);   //2.初始化对象
instance = memory;      //3.设置instance指向刚分配的内存地址
...                     //4.访问对象

我们说过,编译器和处理器会对指令序列进行重排序,但是必须建立在不能改变程序结果的条件下。

很显然,操作 2 和 3 可能会被重排序。

时间 线程 A 线程 B
T 1 A 1:分配对象内存空间
T 2 A3:设置 instance 指向内存空间
T 3 B1:判断 instance 是否为空
T 4 B2:由于不为空,线程 B 将访问 instance 引用的对象
T 5 A2:初始化对象
T 6 A3:访问 instance 引用的对象

可以看出,由于操作 2 和 3 的重排序,会导致线程 B 访问到一个还未初始化的对象。

有两种思路来解决这个问题:

  1. 不允许操作 2 和 3 重排序
  2. 允许操作 2 和 3 重排序,但不允许其他线程 “ 看到 ” 这个重排序。
基于 volatile 的解决方案

volatile 禁止指令重排序。所以只需把 singleTon 声明为 volatile 即可。

基于类初始化的解决方案

JVM 在类的初始化阶段,会执行类的初始化。在执行类的初始化期间,JVM 会去获取一个锁,这个锁可以同步多个线程对同一个类的初始化。

基于这个特性,可以实现另外一种线程安全的初始化方案:静态内部类。

public class StaticInstance {
    public StaticInstance getInstance() {
        return ClassHolder.instance;
    }

    public static class ClassHolder {
        static StaticInstance instance = new StaticInstance();
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351

推荐阅读更多精彩内容