Java 多线程(二):内存模型与 Synchronized、Volatile、ReentrantLock

Java 内存模型

  • Java 内存模型即 Java Memory Model(JMM),JMM 定义了 Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式
  • 要想深入了解 Java 并发编程,要先理解好 Java 内存模型,Java 内存模型定义了多线程之间共享变量的可见性以及对共享变量的同步

并发编程

  • 在并发编程中,线程之间通过共享内存(线程之间通过读写公共属性来隐式通信)和消息传递来通信(线程之间通过明确发送消息来进行通信)
  • JAVA 的并发采用的是共享内存模型,JAVA 线程之间的通信总是隐式进行,通信过程对程序员完全透明,如果程序员不理解隐式通信的工作机制,便会出现内存可见性问题

内存模型抽象

  • 在 JAVA 中
    • 所有实例域(new创建的对象)、静态域、数组元素存储在堆内存中,堆内存在线程之间共享(共享变量)
    • 局部变量、方法定义参数和异常处理参数不会在线程之间共享,他们不会有内存可见性问题,也不受内存模型影响
  • JMM 决定一个线程共享变量的写入何时对另一个线程可见。线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本
  • JMM 会通过控制主内存与每个线程之间的本地内存之间的交互尽可能来提供内存可见性保证,如果A线程与B线程之间要通信的话,必须经历下面2个步骤:
    • 线程 A 把内存 A 中更新过的共享变量刷新到主内存中
    • 线程 B 从主内存中读取线程 A 之前已更新过的共享变量
内存可见性

重排序

  • 在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序,重排序在单线程中是安全的,但是在多线程访问共享变量时可能会有内存可见性问题,重排序分为三种:
    • 编译器优化的重排序
    • 指令级并行的重排序
    • 内存系统的重排序
  • 如果两个线程访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性,例如下面情况:
名称 代码示例 说明
写后读 a=1;b=a; 写一个变量之后,再读这个位置
写后写 a=1;b=2; 写一个变量之后,再写这个位置
读后读 a=b;b=1; 读一个变量之后,再写这个位置
  • 上面三种情况存在依赖关系,只要重排序两个操作的执行顺序,程序执行结果就会改变
  • 编译器和处理器在重排序时会遵守数据的依赖性,不会改变存在数据依赖关系的两个操作执行顺序(仅在单线程中安全)
  • as-if-serial 语义
    • as-if-serial 指编译器,runtime 和处理器无论怎么重排序,(单线程)程序的执行结果不能被改变
    • 为了遵守 as-if-serial 语义,编译器和处理器不会对存在依赖关系的操作做重排序
  • 例如下图中 A 与 C、B 与 C 存在依赖关系,但 A 与 B 不存在依赖关系,所以 A 与 B 的执行顺序是可以进行重排序的,这不影响最终结果

  • 当程序未正确同步时,就会存在顺序竞争,当代码中包含数据竞争时,程序的执行往往产生违反直觉的结果:
    • 在一个线程中写入一个变量
    • 在另外一个线程中读取同一个变量
    • 而写和读没有同步来排序
  • 数据一致性内存模型
    • 顺序一致性内存模型具有两个特征:
      • 一个线程中的所有操作必须按照程序的顺序执行
      • 所有线程只能看到一个单一的操作执行顺序,在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见
    • 顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程,同时,每一个线程必须按程序的顺序来执行内存读/写操作,在任意时间点最多只能有一个线程可以连接到内存。

可见性分析

  • 线程的不安全主要体现在共享变量的不可见,导致共享变量在线程之间不可见的原因:
    • 线程的交叉执行
    • 重排序结合线程交叉执行
    • 共享变量更新后的值没有在工作内存与主内存之间得到更新

synchronized 特性

  • 原子性:(同步)在任何时刻,只能有一个线程在执行锁内的代码,因此也解决了线程交叉执行与重排序(重排序再单个线程内不会有问题)等问题
  • 可见性
    • 线程解锁前,必须把共享变量的最新值刷新到主内存中
    • 线程加锁时,将清空工作内存中共享变量的值,从而线程内存变量需要重新从主内存中读取最新的值

Volatile 特性

  • 可见性:volatile 修饰的变量能保证其可见性,这是通过内存屏障来实现的:
    • 对 volatile 变量执行写操作时,会在写操作后加入一条 store 屏障指令,将变量值从工作内存刷新到主内存去
    • 对 volatile 变量执行读操作时,会在写操作后加入一条 load 屏障指令,将从主内存中读取共享变量的值到工作内存中
  • 不能保证原子性:volatile 不能保证 volatile 变量复合操作的原子性,例如
number = number + 1;
number++;
  • 它非原子操作,它分为三步,这可能会被重排序:
    • 读取 number 的值
    • 将 number 值加1
    • 写入最新的 number 值
  • 不保证同步:线程的交叉执行可能会影响程序的最终结果

案例分析

  • 下面代码中创建了两个线程,并执行了 write() 方法,随后 reader() 方法,它会存在下面几种代码执行顺序(不完全列举):
描述 顺序 输出结果
写线程执行 1.1 操作后,读线程抢占到了 CPU 资源执行了 2.1、2.2,然后写线程再执行了 1.2 1.1->2.1->2.2->1.2 3
写线程 1.1 跟 1.2 进行了冲排序,写线程先执行了 1.2 操作后,读线程抢占到了 CPU 资源执行了 2.1、2.2,然后写线程再执行了 1.1 1.2->2.1->2.2->1.1 0
读线程先抢占到 CPU 资源执行了 2.1,然后写线程再执行了 1.1、1.2 2.1->1.1->1.2 0
public class Demo {
    // 共享变量
    private int result = 0;
    private boolean flag = false;
    private int number = 1;

    // 写操作
    public void write() {
        flag = true;        // 1.1
        number = 2;        // 1.2
    }

    // 读操作
    public void read() {
        if (flag) {          // 2.1
            result = number * 3;      // 2.2
        }
        System.out.println("result 值为:" + result);
    }

    // 线程内部类
    private class ReadWriteThread extends Thread{
        // 根据构造方法中传入的 flag 确定线程是读操作还是写操作
        private boolean flag;

        public ReadWriteThread(boolean flag) {
            this.flag = flag;
        }

        @Override
        public void run() {
            if (flag)
                // flag 值为 true 执行写操作
                write();
            else
                // flag 值为 false 执行读操作
                read();
        }
    }

    public static void main(String[] args) {
        Demo demo = new Demo();
        // 启动线程执行写操作
        demo.new ReadWriteThread(true).start();
        // 启动线程执行读操作
        demo.new ReadWriteThread(false).start();
    }
}

案例优化

  • 利用 synchronized 保证可见性与同步
// 写操作
public synchronized void write() {
    flag = true;        // 1.1
    number = 2;        // 1.2
}
// 读操作
public synchronized void read() {
    if (flag) {          // 2.1
        result = number * 3;      // 2.2
    }
    System.out.println("result 值为:" + result);
}
  • 注意:此时结果仍然可能为 0,这是因为读线程比写线程优先抢占到了 CPU 资源,并先执行了读方法,这种情况与可见性无关
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容