前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >java 内存模型与 volatile 的实现

java 内存模型与 volatile 的实现

作者头像
用户3147702
发布2022-06-27 12:37:15
2100
发布2022-06-27 12:37:15
举报
文章被收录于专栏:小脑斧科技博客

1. 概述

上文中,我们介绍了线程同步机制。 我们提到了 volatile、synchronized 关键字与 java.util.concurrent.locks.ReentrantLock 类,本问我们来详细讲解一下 volatile 的用法。

2. java 内存模型与一致性

要想深入理解以上提到的并发环境同步工具的意义和用法,就必须先深入了解 java 的内存管理,即 JMM (java memory model),这是 java 最核心的设计理念。 众所众知,现代计算机存储设备的读写性能与处理器的运算性能有几个数量级的差距,所以现代计算机不得不引入多级高速缓存,来作为内存与CPU之间的缓冲,这虽然解决了处理器与内存处理速度的矛盾,但也为计算机系统带来巨大的复杂度,如何保证缓存一致性成为了一个新引入的问题,当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致,为了解决这个问题,不同的软件系统规定了不同的协议,Java 虚拟机规范中就定义了 JMM 规范来解决各种存储访问带来的问题,JDK1.5 实现的 JSR-133 协议是相对非常成熟和完善的。 对于线程间共享的变量,如上述所说,在线程并发环境中可能产生并发条件,JMM 规定,java 虚拟机将存储设备抽象为主内存(Main Memory)与工作内存(Working Memory),所有的变量都存储在主内存中,每条线程都拥有自己的工作内存,与处理器高速缓存类似,线程的工作内存中保存被该线程使用到的变量主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,不同的线程也无法相互访问对方的工作内存,线程间数据的传递必须通过主内存来完成。 JMM 定义了内存的八种基本操作,并且要求他们必须是原子性的。

JMM 内存的八种操作

操作

名称

作用域

描述

lock

锁定

主内存

将一个变量标识为线程独占

unlock

解锁

主内存

释放标识为线程独占的变量为全局可见

read

读取

主内存

将一个变量的值从主内存传输到线程工作内存中

load

载入

工作内存

将 read 操作从主内存中得到的变量放入工作内存变量副本中

use

使用

工作内存

将工作内存中的变量值传递给执行引擎,让虚拟机可以使用该变量

assign

赋值

工作内存

将一个执行引擎接受到的值赋值给工作内存中的变量

store

存储

工作内存

把工作内存中的一个变量的值传递到主内存

write

写入

主内存

将 strore 操作后工作内存传递到主内存的变量的值写入主内存的变量中

以上八个操作中,read 与 load、store 与 write 分别必须是成对出现的,不允许单独出现,也不允许任何一个线程丢弃他最近 assign 操作后的变量值,同时,assign 是唯一出发线程工作内存同步回主内存的动作。 所有的变量,都必须在主内存中诞生和初始化,也就是说,对一个变量第一次执行 use 和 store 操作前必须先执行 assign 与 load 操作。 这些复杂的原则就是先行发生原则(happens-before)的一部分。

3. 先行发生原则(happens-before)

先行发生原则是一系列规则的总和,它规定了 java 虚拟机必须遵循的内存模型的顺序规则,根据这些规则,可以很轻易的判断出两个操作是否有顺序保障。 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 5. start规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作 6. join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

4. volatile 变量

说到这里,其实就不需要对 volatile 关键字进行解释了,JRS 中已经明确规定了 volatile 变量的规则。

  • volatile变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读

当一个变量被定义为 volatile 之后,他将具备两种特性: 1. 保证此变量对所有线程都可见 2. 任何一个线程修改了该变量的值,其他线程将立即得知该新值 volatile 变量是通过线程的每次读取都强制从主内存刷新到工作内存实现的,同时,所有更改都强制立即从工作内存同步到主内存中,因此,volatile 变量并不能保证其并发安全性。

5. volatile 的使用场景

显而易见,使用 volatile 的代码复杂度要比使用锁简单得多,而通常 volatile 变量的同步机制要比锁的性能更高一些,尤其在读操作远远多于写操作的环境中,这是因为 volatile 变量的写操作性能要低一些。 以下五个场景是 volatile 非常适合的应用场景:

5.1. 状态标志

在多线程环境中,某个线程为主线程或调度线程,只有该线程可以更改状态标志,从而实现对其他线程的调度和控制,所有工作线程读取状态标志来判断当前所需要执行的工作。 由于 volatile 能够保证每次读取到的值都是强制从主内存刷新到工作内存中的值,从而保证了所有工作线程都能够立即读取到正确的指令。

5.2. 一次性安全发布

考虑下面一段代码:

代码语言:javascript
复制
public class BackgroundFloobleLoader {
    public Flooble theFlooble;

    public void initInBackground() {
        // do lots of stuff
        theFlooble = new Flooble();  // this is the only write to theFlooble
    }
}

public class SomeOtherClass {
    public void doWork() {
        while (true) { 
            // do some stuff...
            // use the Flooble, but only if it is ready
            if (floobleLoader.theFlooble != null) 
                doSomething(floobleLoader.theFlooble);
        }
    }
}

上面的代码中,假设两个类分别在不同的线程中执行,那么 SomeOtherClass 中 doSomething 方法获取到的对象有可能是未完全初始化完成的对象,从而可能带来完全无法预知的错误出现。 将 BackgroundFloobleLoader 类的 theFlooble 成员设置为 volatile 就可以避免并发环境中读取到初始化了一半的对象的问题了。

5.3. 独立观察

对于统计信息的收集程序,往往收集线程需要将信息发布出来,其他线程需要获取到最新的数据。 比如天气情况的收集,这个保存天气情况的值或对象是实时变化的,volatile 保证了他是最新的。

5.4. volatile bean

代码语言:javascript
复制
@ThreadSafe
public class Person {
    private volatile String firstName;
    private volatile String lastName;
    private volatile int age;

    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }

    public void setFirstName(String firstName) { 
        this.firstName = firstName;
    }

    public void setLastName(String lastName) { 
        this.lastName = lastName;
    }

    public void setAge(int age) { 
        this.age = age;
    }
}

上述代码中的类是一个应用于并发环境的 JavaBean,他可以保证任何成员读取的正确性。

5.5. 计数操作

++x 操作实际上是三种操作的组合:读、加1、存储,因此他是非线程安全的。 如果多个线程试图同时使用计数器,那么必须通过 volatile 保证读取的准确性,通过加锁实现整个操作的原子性。

代码语言:javascript
复制
@ThreadSafe
public class CheesyCounter {
    // Employs the cheap read-write lock trick
    // All mutative operations MUST be done with the 'this' lock held
    @GuardedBy("this") private volatile int value;

    public int getValue() { return value; }

    public synchronized int increment() {
        return value++;
    }
}

6. 如何判断是否需要使用锁

同时满足下面的两个条件,可以通过 volatile 来保证线程安全性,如果不满足,就必须要使用锁来保证并发环境下的安全了。 1. 对变量的写操作不依赖于当前值 2. 该变量没有包含在具有其他变量的不变式中

7. 参考资料

《深入理解Java虚拟机 —— JVM高级特性与最佳实践》。 https://www.ibm.com/developerworks/cn/java/j-jtp06197.html。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-07-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小脑斧科技博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 概述
  • 2. java 内存模型与一致性
  • 3. 先行发生原则(happens-before)
  • 4. volatile 变量
  • 5. volatile 的使用场景
    • 5.1. 状态标志
      • 5.2. 一次性安全发布
        • 5.3. 独立观察
          • 5.4. volatile bean
            • 5.5. 计数操作
            • 6. 如何判断是否需要使用锁
            • 7. 参考资料
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档