java 同步的三种方式:volatile、锁、final

转载:http://www.cnblogs.com/leesf456/p/5291484.html


1、volatile

1.1 介绍

关键字volatile是Java虚拟机提供的最轻量级的同步机制。
当一个变量定义为volatile时,它将具备两种特性:(1)可见性;(2)禁止指令重排序。

  1. 可见性
    当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即获得的,但是基于volatile变量的操作并不是安全的(如自增操作),不能保证原子性。
  2. 禁止指令重排序
    不允许对volatile操作指令进行重排序。

1.2 volatile 的 happens - before 关系

在volatile变量与happens - before 之间是什么关系呢,我们通过一个示例说明:

class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;          //1
        flag = true;    //2
    }

    public void reader() {
        if (flag) {         //3
            int i = a;     //4
        }
    }
}

说明:假定线程A先执行writer方法,线程B后执行reader方法,那么根据happens - before关系,我们可以知道:

  1. 根据程序顺序规则,1 happens before 2; 3 happens before 4。
  2. 根据 volatile变量规则,2 happens before 3。
  3. 根据 happens before 的传递性,1 happens before 4。

具体的happens - before图形化如下:


这里写图片描述

1.3 volatile 读写内存语义

  1. 读内存语义。当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程之后将从主内存中读取共享变量。

  2. 写内存语义。当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。这样就保证了volatile的内存可见性。

volatile读写内存语义总结为如下三条:

  1. 线程 A 写一个 volatile 变量,实质上是线程 A 向接下来将要读这个 volatile 变量的某个线程发出了(其对共享变量所在修改的)消息。

  2. 线程 B 读一个 volatile 变量,实质上是线程 B 接收了之前某个线程发出的(在写这个 volatile 变量之前对共享变量所做修改的)消息。

  3. 线程 A 写一个 volatile 变量,随后线程 B 读这个 volatile 变量,这个过程实质上是线程 A 通过主内存向线程 B 发送消息。

1.4 volatile 内存语义的实现

前面讲到,volatile变量会禁止编译器、处理器重排序。下面是volatile具体的排序规则表:


这里写图片描述

为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,JMM 采取保守策略。下面是基于保守策略的 JMM 内存屏障插入策略:

  1. 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。

  2. 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障(对volatile写、普通读写实现为不允许重排序,可能会影响性能)。

  3. 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。

  4. 在每个 volatile 读操作的后面插入一个 LoadStore 屏障(普通读写、volatile读实现为不允许重排序,可能会影响性能)。

下面通过一个示例展示volatile的内存语义:

class VolatileBarrierExample { 
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;

    void readAndWrite() {
        int i = v1;      // 第一个 volatile 读
        int j = v2;      // 第二个 volatile 读
        a = i + j;       // 普通写
        v1 = i + 1;      // 第一个 volatile 写
        v2 = j * 2;      // 第二个 volatile 写
    }
}

根据程序和插入屏障的规则,最后的指令序列如下图所示:


这里写图片描述

说明:编译器、处理器会根据上下文进行优化,并不是完全按照保守策略进行插入相应的屏障指令。


2、锁

2.1 介绍

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

2.2 锁的 happens - before 关系

下面一个示例展示了锁的使用:

class MonitorExample {
    int a = 0;

    public synchronized void writer() {    // 1 
        a++;     // 2
    }               // 3

    public synchronized void reader() { // 4 
        int i = a;    // 5
    }                   // 6
}

说明:假设线程 A 执行 writer()方法,随后线程 B 执行 reader()方法。该程序的happens - before关系如下:

  1. 根据程序顺序规则,1 happens before 2, 2 happens before 3; 4 happens before 5, 5 happens before 6。

  2. 根据监视器锁规则,3 happens before 4。

  3. 根据传递性,2 happens before 5。

图形化表示如下:


这里写图片描述

2.3 锁的内存语义

1. 当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中,以确保之后的线程可以获取到最新的值。

2. 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量。

锁的释放与获取总结为如下三条:

  1. 线程 A 释放一个锁,实质上是线程 A 向接下来将要获取这个锁的某个线程发出了(线程 A 对共享变量所做修改的)消息。

  2. 线程 B 获取一个锁,实质上是线程 B 接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。

  3. 线程 A 释放锁,随后线程 B 获取这个锁,这个过程实质上是线程 A 通过主内存向线程 B 发送消息。

2.4 锁内存语义的实现

锁的内存语义的具体实现借助了volatile变量的内存语义的实现。


3.final

3.1 介绍

对于 final 域,编译器和处理器要遵守两个重排序规则:

  1. final 写:“构造函数内对一个final域的写入”,与“随后把这个被构造对象的引用赋值给一个引用变量”,这两个操作之间不能重排序。

  2. final 读:“初次读一个包含final域的对象的引用”,与“随后初次读对象的final域”,这两个操作之间不能重排序。

如下面示例展示了final两种重排序规则:

public final class FinalExample {
    final int i;
    public FinalExample() {
        i = 3;     // 1
    }
    
    public static void main(String[] args) {
        FinalExample fe = new FinalExample();    // 2
        int ele = fe.i;                          // 3
    }
}

说明: 操作1与操作2符合重排序规则1,不能重排,操作2与操作3符合重排序规则2,不能重排。

由下面的示例我们来具体理解final域的重排序规则。

public class FinalExample {
    int i;                      // 普通变量
    final int j;                // final变量
    static FinalExample obj;    // 静态变量

    public void FinalExample () { // 构造函数 
        i = 1;    // 写普通域
        j = 2;    // 写final域
    }

    public static void writer () { // 写线程A执行 
        obj = new FinalExample();
    }

    public static void reader () {   // 读线程B执行
        FinalExample object = obj;   // 读对象引用
        int a = object.i;            // 读普通域
        int b = object.j;            // 读final域
    }
}

说明:假设线程A先执行writer()方法,随后另一个线程B执行reader()方法。下面我们通过这两个线程的交互来说明这两个规则。

3.2 写 final 域 重排序规则

写 final 域的重排序规则禁止把 final 域的写重排序到构造函数之外。这个规则的实现包含下面两个方面:

  1. JMM 禁止编译器把 final 域的写重排序到构造函数之外。

  2. 编译器会在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 屏障。这个屏障禁止处理器把 final 域的写重排序到构造函数之外。

writer方法的obj = new FinalExample();其实包括两步,首先是在堆上分配一块内存空间创建FinalExample对象,然后将这个对象的地址赋值给obj引用。假设线程 B 读对象引用与读对象的成员域之间没有重排序,则可能的时序图如下:


这里写图片描述

说明:写普通域的操作被编译器重排序到了构造函数之外,读线程 B 错误的读取了普通变量 i 初始化之前的值。而写 final 域的操作,被写 final 域的重排序规则 “限定”在了构造函数之内,读线程 B 正确的读取了 final 变量初始化之后的值。写 final 域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的 final 域已经被正确初始化过了,而普通域不具有这个保障。以上图为例,在读线程 B “看到”对象引用 obj 时,很可能 obj 对象还没有构造完成(对普通域 i 的写操作被重排序到构造函数外,此时初始值 2 还没有写入普通域 i)。

3.3 读 final 域 重排序规则

读 final 域的重排序规则如下:

在一个线程中,"初次读对象引用"与"初次读该对象包含的 final 域",JMM 禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。
编译器会在读 final 域操作的前面插入一个 LoadLoad 屏障。
初次读对象引用与初次读该对象包含的 final 域,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此编译器不会重排序这两个操作。大多数处理器也会遵守间接依赖,大多数处理器也不会重排序这两个操作。但有少数处理器允许对存在间接依赖关系的操作做重排序(比如 alpha 处理器),这个规则就是专门用来针对这种处理器。

reader方法包含三个操作:① 初次读引用变量 obj。② 初次读引用变量 obj 指向对象的普通域 i。③ 初次读引用变量 obj 指向对象的 final 域 j。

假设写线程 A 没有发生任何重排序,同时程序在不遵守间接依赖的处理器上执行,下面是一种可能的执行时序:


这里写图片描述

3.4 如果 final域 是引用类型

上面我们的例子中,final域是基本数据类型,如果final与为引用类型的话情况会稍微不同。对于引用类型,写 final 域的重排序规则对编译器和处理器增加了如下约束:

在构造函数内对一个 final 引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

3.5 final 逸出

写 final 域 的重排序规则可以确保:

在引用变量为任意线程可见之前,该引用变量指向的对象的 final 域已经在构造函数中被正确初始化过了。

其实要得到这个效果,还需要一个保证:

在构造函数内部,不能让这个被构造对象的引用为其他线程可见,也就是对象引用不能在构造函数中“逸出”。

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

推荐阅读更多精彩内容