从线程安全到 JMM(Java Memory Model)

一、什么是线程安全

多个线程不管以何种方式访问共享变量,并且不需要进行同步,都能表现正确的行为,就是线程安全。

呃,这和 JMM(Java Memory Model)有什么联系呢?

这里就需要知道为什么会产生线程不安全。

发生线程不安全的本质实际上是主内存和工作内存中的数据不一致,或者发生了重排序所导致

什么是主内存,什么是工作内存,什么又是重排序呢?这就牵涉到 JMM 了。

二、Java 内存模型——JMM

Java 内存模型(Java Memory Model,JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例变量、静态变量和构成数组对象的元素)的访问方式。

需要注意的是别把 JMM 和 JVM 搞混了,JVM 是 Java 虚拟机(Java Virtual Machine)。

JMM

JMM 定义了每个线程和主内存之间的抽象关系:线程之间的共享变量存储在“主内存”中,而每个线程都有一个私有的“工作内存”,工作内存中存储了该线程读/写共享变量的副本。

上图描述了一个多线程执行场景。

线程 A 和线程 B 分别对主内存的变量进行读写操作。其中主内存中的变量为共享变量,也就是说此变量只此一份,多个线程间共享。

JMM 规定:线程不能直接读写主内存的共享变量,每个线程都有自己私有的工作内存,线程需要读写主内存的共享变量时,首先需要将该变量拷贝一份副本到自己工作内存,然后在自己的工作内存中对该变量进行操作,完成操作之后再将结果同步至主内存。

这么说起来有点抽象,下面举一个例子。

例如 2 条线程,循环 1000 次,分别给 x 值加 1,若不加任何处理,其最终结果很可能会小于 2000。

A、B 线程分别读取主内存中 x 的值,并把 x 的值复制到自己的工作内存中,此时,A、B 线程的工作内存的值都是 0。

然后,每条线程对自己工作内存中的值进行自加操作,操作完成之后再写回主内存,这时候就出问题了。A、B 线程只把自己的值写回主内存,而没有考虑到其他线程在该期间内也对主内存中的值做了修改。这就是线程不安全的原因之一。

三、volatile

为了解决上述问题,volatile 关键字就闪亮登场了。

被 volatile 关键字描述变量的操作具有可见性有序性(禁止指令重排)。

1、可见性

针对 volatile 修饰的变量 Java 虚拟机有特殊的约定:

当一条线程读取被 volatile 修饰的变量时,JMM 会把该线程工作内存中对应的变量值置为无效,必须从主内存中读取;

当一条线程写入被 volatile 修饰的变量时,JMM 会把该线程的工作内存中对应的值立即刷新到主内存;

从而避免出现数据脏读的现象,保证数据的“可见性”。

2、有序性

在执行程序时,为了提高性能,编译器和处理器会对指令进行重排序。例如下面代码:

double pi = 3.14             // A
double r = 1.0               // B
double area = pi * r * r     // C

这是一个计算圆面积的代码,由于 A、B 两句之间没有任何关系,对最终结果也不会存在关系,它们之间执行顺序可以重排序。

因此可以执行顺序可以是 A->B->C 或者 B->A->C 执行最终结果都是 3.14,即 A 和 B 之间没有数据依赖性。但是 C 一定在 A、B 执行完之后才能执行。因此,重排序有以下两个特点:

  1. 重排序操作不会对存在数据依赖关系的操作进行重排序。

  2. 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。

例如下面的代码,A 线程执行changeStatus(),B 线程执行run(),能保证输出一定等于 3 吗?

public class TestVolatile {
    int a = 1;
    boolean status = false;

    public void changeStatus() {
        a = 2;          // 1
        status = true;  // 2
    }

    public void run() {
        while (true) {
            if (status) {        // 3
                int b = a + 1;   // 4
                System.out.println(b);
                break;
            }
        }    
    }
}

不一定,因为 1 和 2 之间不存在数据依赖关系,因此编译器和处理器可能会对指令进行重排序,若 A 线程先执行了 2,还未执行 1,此时 B 线程执行了 3 和 4,这就会时输出的值等于 2。

将变量 a 使用 volatile 关键字修饰共享变量便可以禁止这种重排序。若用 volatile 修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

3、不保证原子性

在访问 volatile 变量时不会执行加锁操作,也就不会使执行线程阻塞,因此 volatile 是一种比 sychronized 更轻量级的同步机制,可以保证可见性,保证不被重排序,但是,不能保证线程安全,也不保证原子性。

为什么 volatile 不能保证原子性?

以 i++ 为例,其包括读取、加操作、赋值三个操作,下面是两个线程的操作顺序。

假如线程 A 在做了 i+1,但未赋值的时候,线程 B 就开始读取 i,当线程 A 赋值 i=1,并回写到主内存,而此时线程 B 已经不再需要读取 i 的值了,而是正在做 +1 操作,于是当线程 B 执行完并回写到主内存,i 的值仍然是 1,而不是预期的 2。

也就是说,volatile 缩短了普通变量在不同线程之间执行的时间差,但仍然存有漏洞,依然不能保证原子性。

四、总结

所以,结论是 volatile 是一种轻量级的同步机制,可以保证共享变量对所有线程的可见性,禁止指令重排序优化,但不保证原子性,像 num++ 这种复合操作,volatile 无法保证其原子性

当然,像 num++ 这种操作可以通过 CAS 的方式来保证原子性。

如想保证线程安全和原子性,还是需要使用锁。锁可以保证可见性、有序性、原子性

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容