Java 内存模型

Java 内存模型(Java Memory Model, JMM) 是 Java 为了屏蔽不同硬件和操作系统的内存访问差异, 让 Java 程序在各种平台下都有一致的内存访问效果.

Java 内存模型的主要目标是定义程序中共享变量的访问规则

由于处理器的速度跟主内存的速度不是一个数量级的, 因此处理器读取内存中的数据时, 会读取一段数据并缓存, 处理器不会直接跟主内存交互而是直接使用并修改缓存上的数据.

当同一块主内存的数据在多个线程 (多个处理器) 中有多份拷贝时, 其中一个线程对其缓存的变量修改之后, 不会马上写会到主内存, 即使写回了主内存, 其他的线程不知道这个值已经改变了, 而是继续使用缓存的数据, 因此会有数据不一致的问题(线程不安全).

虚拟机保证以下原子操作:

  1. lock(锁定): 作用于主内存变量, 标识为线程独有
  2. unlock(解锁): 作用于主内存变量, 把变量释放
  3. read(读取): 作用于主内存变量, 把变量传输到工作内存
  4. load(载入): 作用于工作内存变量, 把read得到的变量放入到工作内存的变量副本
  5. use(使用): 作用于工作内存变量, 把工作副本的变量传递给执行引擎
  6. assign(赋值): 作用于工作内存变量, 把执行引擎的值赋值到工作内存
  7. store(存储): 作用于工作内存, 把工作内存的值送到主内存中
  8. write(写入): 作用于主内存, 把工作内存中得到的值放入主内存中
Java 内存模型

lock, unlock, read, load, store, write, assign 和 use 规则如下:

  1. 不允许 read 和 load, store 和 write 操作之一单独出现, 必须同时出现
  2. 工作内存中的值改变(assign)了必须同步到主内存中
  3. 工作内存中的值没有改变(assign)不允许同步回主内存中
  4. 一个新的变量只能在主内存中创建, 不能使用未初始化(load 或 assign)的变量, 对一个变量执行 use,store 之前, 必须先执行过了 assign和load
  5. 同一个变量, 同一时刻, 只能一个线程对其 lock 操作, lock 操作可以被同一个线程执行多次, 只有执行相同次数的 unlock 变量才能被解锁
  6. 对一个变量执行 lock 操作, 会清空工作内存中此变量的值, 其他执行引擎需要重新 load 或 assign 初始化这个变量
  7. 一个变量没有被 lock 锁定, 不允许 unlock, 不允许 unlock 其他线程锁住的变量
  8. 对变量执行 unlock 之前, 必须先把此变量同步回主内存中(store, write)

volatile 特殊规则

volatile 保证变量的可见性

volatile 修饰的变量, 每次这个值在一个线程中被修改时, 会立即同步到主内存中, 并且使其他换处理器缓存了这个变量的缓存全部失效, 使用这个变量时, 需要重新从主内存中获取, 因此能保证 volatile 修饰的变量在所有线程中是一致的

但是 volatile 的运算不一定是线程安全的, 因为运算不一定是线程安全的.

下面的例子, 开启了20个线程, 每个线程执行 10000 次 race++, 实际运行结果会小于 200000.

volatile static int race = 0;
for (int i=0; i<20; i++) {
    threads[i] = new Thread(() -> {
            for (int i=0; i<10000; i++) race ++;
        });
    threads[i].start();
}

为了方便理解, 这里举一个例子特殊说明为啥最终值会比200000小. 假设有两个线程都缓存了变量 race, 值设为63. 都执行了 race++ 操作, 并且都得到了结果(结果都是64), 当其中一个线程把值(64)写入到主内存后, 另一个线程不会使用新的race值再次计算一遍, 而是直接把64写回了主内存中, 因此经过两次 race++, 值只增加了1.

只有满足下面两个规则才能使用 volatile:

  1. 运算结果不依赖变量当前的值, 或者或者能确保只有一个单一的线程修改变量的值
  2. 变量不需要与其他的状态共同参与不变约束

volatile 禁止指令的重排优化

对于普通的变量, 虚拟机只保证单线程中运行出来的结果是正确的, 不能保证指令的执行顺序(在单线程中, 感知不到指令发生重排)

给 volatile 变量赋值之后, 会执行 lock addl $0x0, 把数据写入到主内存, 并让其他线程的缓存无效. 对与普通变量, 为了提高效率 CPU 会将指令乱序, 只保正最终结果一样.

大多数情况下 volatile 比锁的开销低, volatile 变量的读取跟普通变量一样, 写入的时候要用到内存屏障(后面的指令无法跑到屏障之前的位置), 需要更多的时间

对于 long 和 double 变量的特殊规则

Java规范允许没有声明为 volatile 的 long, double 型变量分两次操作(不是原子操作), 但是也强烈建议不这么做, 基本上的机器都当成原子操作

原子性, 可见性, 有序性

  • 原子性: 由 Java 内存模型包装原子性 read, load, assign, use, store, write, lock, unlock 都是原子的
  • 可见性: 变量的值被修改之后, 都会同步到主内存, 使用 volatile 保证修改之后马上同步会主内存
  • 有序性: 在一个线程中, 所有的操作都是有序的, 在另一个线程中观察, 所有的操作都是无序的
  1. 内存模型提供了 lock, unlock 来满足用户更大范围的原子操作需求, 使用字节码 monitorenter, monitorexit 完成操作, Java代码中用 synchronized 实现
  2. 同步块(synchronized 和 final)的可见性, 在执行 unlock 之前, 必须把此变量返回到主内存中 (把锁的对象返回到主内存)
  3. final 修饰的字段, 一旦对象把 this 传递出去, 其他线程就能看到 final 的值, 如果没有使用 final 修饰, 可能这个对象传递出去之后, 未初始化完, 其他线程调用可能出问题. (没有使用 final 修饰的字段, 可能对象创建之后, 对象的字段未初始化)

先行发生原则(happens-before)

内存模型中紧靠 volatile 和 synchronized 会使操作变得繁琐, 因此有了先行发生的原则, 先行发生规则无需任何同步就能保障成立.

  1. 程序次序规则: 在一个线程内在前面的代码先行发生于后面的代码
  2. 管程锁定规则: 一个 unlock 先行发生于同一个锁的 lock
  3. volatile 规则: 一个 volatile 的写操作先行发生于这个变量的读操作, 这里指时间顺序
  4. 线程启动规则: Thread 对象的 start() 先行发生于此线程的每一个操作
  5. 线程终止规则: 线程中所有操作都先行于此线程的终止检测(检查到线程已经终止, 肯定操作都已经结束)
  6. 线程中断规则: 对线程 interrupt() 方法的调用先行于被中断线程的代码检测到中断事件的发生
  7. 传递性: A 先行与 B, B 先行于 C, 则 A 先行于 C
  1. 两个线程操作一个普通共享变量, 不符合上面的任何一个规则, 因此是不安全的
  2. 先行发生不等于时间上一定先发生, 同一个线程中的, 可能指令重排序

参考《深入理解 Java 虚拟机》
多线程编程 深入理解DCL的安全性
从DCL的对象安全发布谈起

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

推荐阅读更多精彩内容