前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >内存模型与轻量级同步机制volatile

内存模型与轻量级同步机制volatile

作者头像
搬砖俱乐部
发布2019-06-15 17:33:33
4690
发布2019-06-15 17:33:33
举报
文章被收录于专栏:BanzClubBanzClub

Java中为了线程通信的安全性(数据一致性),除了提供内置锁synchronized和显示锁ReentrantLock,还提供了另外一种线程同步机制——volatile,是一种轻量级同步机制。不过,通常很难轻易的理解volatile的真正意义。下面通过一个例子来认识一下volatile(摘自《深入理解Java虚拟机》):

代码语言:javascript
复制
public volatile static int race = 0;

private static void increase() {
    race++;
}

private static final int THREADS_COUNT = 20;

public static void main(String[] args) {
    Thread[] threads = new Thread[THREADS_COUNT];
    for (int i = 0; i < THREADS_COUNT; i++) {
        threads[i] = new Thread(() -> {
            for (int i1 = 0; i1 < 10000; i1++) {
                increase();
            }
        });
        threads[i].start();
    }
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(race);
}

通过运行这个例子,我们发现race变量最后的值,肯定会小于200000,这是为什么呢?由于volatile只能保证可见性,不能保证原子性。而race++不是原子操作,所以需要在increase方法加锁来保证原子性。而在increase方法加锁后,即使race不用volatile修饰也能得到期望值。看完之后还是一头雾水,没有真正理解其中的意义。

下面我们通过Java内存模型分析,来真正理解volatile的含义以及使用场景:

CPU缓存一致性

我们知道现代计算机为了解决存储设备与处理器处理速度的差距,在CPU和主存之间增加了多层高速缓存,每个CPU都会有一套高速缓存,那么多核计算机就有多套高速缓存。这些高速缓存与内存之间进行读写访问的过程,在不同处理器有不一样的实现方式,也就是不同处理器的内存模型是不一致的(图a,Intel的共享L2缓存;图b,AMD的独享L2缓存)。

当程序运行过程中,会将运算所需要的数据从主存中复制一份到高速缓存中,各个处理器都操作对应的高速缓存的数据,而不直接操作主存,当运算结束后,将高速缓存中的最新数据刷到主存中。这样可以极大的提升CPU的吞吐量,但也引入了新的问题,也就是缓存一致性问题。Intel的MESI协议的提出,就是为了保证每个缓存中使用的共享数据都是一致的,它的具体思想是,每个缓存(Cache)不仅知道自己的读写操作,而且也监听其他Cache的读写操作。当进行读操作时,Cache可以从主存中或者其他Cache中读取数据;当进行写数据时,不仅将Cache的数据反向更新到主存中,也要通知其他Cache该数据无效,需要其他Cache重新从主存中读取数据。

Java内存模型

Java虚拟机也有自己的Java内存模型(Java Memory Model JMM),用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现各种平台下Java内存访问效果的一致。Java内存模型规定了所有共享数据的存储都在Java虚拟机的主内存(堆)中,每个线程拥有自己的工作内存(栈),用来保存被该线程使用到的数据的主内存副本。线程对数据的操作都必须在工作内存中进行,而不能直接操作主内存,也不能直接读写其他线程工作内存的数据,线程间的数据传递都需要通过主内存来完成。

Java内存模型的特性
原子性

Java内存模型定义了8种原子性的操作,来保证数据在主内存和工作内存中的交互,包括:锁定(lock)、解锁(unlock)、读取(read)、载入(load)、使用(use)、赋值(assign)、存储(store)。

在Java语言中,对基本数据类型的变量的读取赋值操作也是原子性的(long、double的非原子性协定),对引用类型变量的读取和赋值操作也是原子性的,但对于一些++、x=y这种运算赋值不是原子性的,也就是多个原子性操作组合在一起就不是原子性的操作了。Java还提供了更大范围的原子性操作,通过synchronized和显式锁ReentrantLock来实现。

可见性

可见性是指当一个线程修改了共享数据的值时,其他线程能理解得知这个变化。volatile能保证新值能立即同步到主内存中,普通变量不能保证立即,所以普通变量不能保证可见性。另外同步机制synchronized与显式锁ReentrantLock也能保证可见性,因为每次只允许一个线程操作共享数据,只有等当前线程操作完,其他线程才能获得权限,所以同步机制也能保证可见性。

有序性

Java线程的天然有序性可以一句话来总结:如果在本线程内观察,所有的操作都是有序的,如果在一个线程中观察另一个线程,所有操作都是无序的。由于Java编译器可以对Java代码进行指令重排序,也就是单线程下,指令重排序不会影响结果,但多线程情况下,会影响执行结果的。Java内存模型通过happens-before原则,来推导两个操作的执行顺序,否则编译器将无法保证有序性。所以为了保证代码执行的有序性,Java提供volatile和内置锁synchronized来完成。

重新认识volatile

特点一:volatile保证可见性

特点二:volatile禁止指令重排序

volatile的使用场景

再看开始的例子,volatile修饰的race只能保证可见性和顺序性,不能保证race这个非原子性操作的原子性,所以不能得到期望值200000。

在并发编程中,volatile只是对共享数据的一种优化方式,并不能代替synchronized,只有在一些特殊的情景下,才能使用它:

  • 对变量的写操作不依赖于当前值
  • 变量不需要与其他的状态变量共同参与不便约束

所以volatile很适合做某些状态值,如用volatile修饰初始化标识,加载一些初始化配置信息后,其他线程可以继续初始化的后续工作;还可以用volatile可以当做一些通知变量和数据,比如一些只能通过设置来配置的共享数据,其他线程不会修改,只会依赖其值的变化,做特定的操作。


  1. 《Java高并发编程详解-多线程与架构设计》
  2. 《Java并发编程艺术》
  3. 《深入理解Java虚拟机》
  4. https://blog.csdn.net/muxiqingyang/article/details/6615199
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-12-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 BanzClub 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • CPU缓存一致性
  • Java内存模型
  • Java内存模型的特性
  • 原子性
  • 可见性
  • 有序性
  • 重新认识volatile
  • volatile的使用场景
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档