并发系列之volatile

加州大学圣地亚哥分校(美国)校训:“愿知识之光普照大地。”


夏季,收获的季节,可看着A股大盘一直趴着,又是徒劳了半年,不免稍有伤神。罢了罢了,聊聊可爱的技术吧,这东西才是养家糊口的根本。这年头,任何技能必须具有强大的变现能力,如果NO,请换道。可能很多朋友们不认同,俗话说:三百六十行行行出状元。OK,比如送外卖的小哥,送出刘翔的速度,送出吉尼斯纪录,也是肉眼可见的天花板吧。哈哈,上述纯扯淡,正事开干。


这篇文章我不想简单地告诉你volatile的语义作用,JMM如何保证可见性等,咱们其实可以系统聊聊,前世今生还是很有必要的。那就从硬件系统架构/多核CPU主存可见性/JVM的底层实现讲讲,来龙去脉了解清楚了,那才叫真的理解,而不是简单地死记硬背,这才算是科学地学习。

一 硬件系统架构演变

我们知道,运行在计算机上的程序,指令是由CPU执行的,数据是存储在主存中的,CPU从主存中读取数据执行指令,再回写到主存中。CPU执行指令的速度是非常快的,但读取写入主存相对较慢的,有人拖后腿了,怎么办呢?大家有没有听说过CPU高速缓存,就是来解决拖后腿问题的。
1/CPU高速缓存
CPU高速缓存为单个CPU所有,只有运行在这个CPU上的线程才能访问。缓存系统是以缓存行为单位存储的,一般是64个字节。按级分为L1 cache / L2 cache / 多核心共享L3 cache。执行流程如下:
a/首先CPU使用自己的寄存器,然后使用速度更快的L1,其中L1D缓存数据,L1I缓存指令;
b/L1缓存和次快的L2做数据同步,L2缓存和L3做数据同步;
c/L3为多个CPU共享的,与主内存做数据同步;
2/缓存写入主存
a/直写(write-through)
直写是透过本级缓存,直接把数据写到下一级缓存中,或直接写到主存中,同时更新缓存中的数据,缓存行永远和它对应的内存内容相匹配。
b/回写(write-back)
缓存并不会立即把写操作传递到下一级,而是仅修改本级缓存中的数据,并且把对应的缓存数据标记为脏数据,脏数据会触发回写,即把里面的内容写到对应的内存或下一级缓存中,回写后,脏数据就变干净了。
3/CPU缓存一致性方案
a/通过在总线上加LOCK#锁的方式
这是一种独占式的方式,在同一时刻只能运行一个CPU,效率较为低下;
b/通过缓存一致性协议,保证多核CPU对共享数据的可见性,主要有:
窥探技术:
所有内存传输都发生在一条共享的总线上,对所有CPU可见;每个CPU不停地窥探总线上发生的数据交换,并追踪其他缓存在做什么。缓存是CPU独享的,内存是CPU共享的,缓存访问内存是需要仲裁的,即在同一个指令周期中,只有一个缓存可以读写内存。
MESI协议:是缓存行四种状态的缩写,如下
已修改缓存行(Modified),该缓存行已经被所属的CPU修改了,其他CPU持有的该缓存行的拷贝也会变成失效状态;
独占缓存行(Exclusive),和主存内容保持一致的拷贝,其他CPU持有的这份内容的拷贝变成失效状态;
共享缓存行(Shared),和主存内容保持一致的拷贝,其他CPU也可持有拷贝,但只能读取,不允许写入;
无效缓存行(Invalid),CPU中的缓存行无效了;
总结来看,只有某个CPU独占了这个缓存行,才能够写入,即处于M或E的状态;当CPU想读取该缓存行,该缓存行必须是共享状态。

二 指令重排序

我们知道,CPU在执行指令的时候为了提升性能,会有一定的指令重排。执行结果与预期结果一致,则重排一定是基于规则,volatile能够提供一定的有序性,禁止一定的指令重排。这里介绍下不同级别的重排序,如下:
1/编译器优化的重排序
编译器在不改变单线程程序的语义前提下,可重新安排语句的执行顺序;
2/指令级并行的重排序
如果不存在数据依赖性,处理器可以改变指令的执行顺序,采用的是指令级并行技术,将多条指令重叠执行;
3/内存系统的重排序
由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去是乱序执行。
小结:就Java程序而言,从java代码到CPU执行序列,也要经过上述的重排序。分析来看,重排序会造成内存可见性问题。要想解决问题,需要了解重排序遵循的准则,才能找到对应的方案。
这里介绍下指令重排中单线程和多线程遵循的准则:
1/as-if-serial
只针对于单线程运行的程序,不管怎么怎么重排序,都不会改变执行结果。就是这么硬气,编译器/运行时和处理器重排序必须遵循。这里通俗理解下:主要是对指令之间数据具有依赖性禁止重排序。满足as-if-serial基准,单线程程序看起来像是按顺序执行的,避免了内存可见性问题。
2/happen-before
happen-before原则是Java内存模型对指令重排的约束,如A happen-before B,则A 操作的结果将对B可见,但实际中A 的执行顺序未必在B之前,只要执行结果与预期一致即可。JMM基于happen-before 原则保证多线程的内存可见性,具体准则如下:
a/程序顺序规则:一个线程中的每个操作,happen-before于该线程中任意后续的操作;
b/监视器锁规则:对一个监视器的解锁,happen-before于随后对这个监视器的加锁;
c/传递性:if A happen-before B,B happen-before C,则A happen-before C;
d/线程的start方法happen-before于线程的后续所有操作;
e/线程上的所有操作happen-before于其他线程在该线程上join返回成功后的操作;
f/volatile变量规则:对一个volatile域的写,happen-before于任意后续对这个域的读。

三 volatile原理

1/基本语义
volatile用来修饰变量或对象,有两层语义:
a/保证可见性,但不保证原子性;
b/提供一定程度的有序性(happen-before),禁止指令重排序。
2/原理剖析
jvm是通过内存屏障来实现volatile的,内存屏障有四种类型,如下:
a/LoadLoad:指令形如Load1;LoadLoad;Load2,其语义是Load1的装载要先于Load2的装载;
b/LoadStore:指令形如Load1;LoadStore;Store2,其语义是Load1的装载要先于Store2存储指令刷新到内存;
c/StoreLoad:指令形如Store1;StoreLoad;Load2,其语义是Store1存储指令刷新到内存要先于Load2的装载;
d/StoreStore:指令形如Store1;StoreStore;Store2,其语义是Store1存储指令刷新到内存要先于Store2的存储;
举例,以两个操作读写为例看下插入的内存屏障,如下:

操作 普通读 普通写 volatile读 volatile写
普通读 LoadStore
普通写 StoreStore
volatile读 LoadLoad LoadStore LoadLoad LoadStore
volatile写 StoreLoad StoreStore

demo世界不孤单,请阅:

/**
 * @author 阿伦故事
 * @Description:测试volatile的作用
 * 对比去掉全局变量num的volatile的修饰看下测试结果
 * */
@Slf4j
public class VolatileTest {
    //声明volatile全局变量
    private volatile int num = 0;

    public static void main(String[] args) {
        VolatileTest volatileTest = new VolatileTest();
        //创建一个用于volatile写的线程并启动
        new Thread(()->{
            log.info("Thread name:"+Thread.currentThread().getName()+"--sleep");
            try {
                Thread.sleep(500);
                volatileTest.num = 5;
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("Thread name:"+Thread.currentThread().getName()+"--dead");
        }).start();
        //创建一个用于volatile读的线程并启动
        new Thread(()->{
            log.info("Thread name:"+Thread.currentThread().getName()+"--num="+volatileTest.num);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("Thread name:"+Thread.currentThread().getName()+"--dead--num="+volatileTest.num);
        }).start();
    }
}

特此声明:
分享文章有完整的知识架构图,将从以下几个方面系统展开:
1 基础(Linux/Spring boot/并发)
2 性能调优(jvm/tomcat/mysql)
3 高并发分布式
4 微服务体系
如果您觉得文章不错,请关注阿伦故事,您的支持是我坚持的莫大动力,在此受小弟一拜!


每篇福利:

评论区打出车型.jpg

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