JMM之原子性、可见性、有序性(指令重排)

一、原子性

    原子性操作指相应的操作是单一不可分割的操作。在我们学化学这门课程的时候,对于里面讲到的原子性相信大家都非常明白,原子是微观世界中最小的不可再进行分割的单元,原子是最小的粒子。java里面的原子性操作也是如此,它代表着一个操作不能再进行分割是最小的执行单元,或者一系列操作要么全部成功执行,要么全部执行失败,不允许中间某一些成功失败,类比如事物控制,要么全部提交要么全部回滚。     下面根据几个粒子来分析下原子性操作:

i = 0;       //1
j = i ;      //2
i++;         //3
i = j + 1;   //4

上面四个操作,有哪个几个是原子操作,那几个不是?如果不是很理解,可能会认为都是原子性操作,其实只有1才是原子操作,其余均不是。

1在Java中,对基本数据类型的变量和赋值操作都是原子性操作; 
2中包含了两个操作:读取i,将i值赋值给j 
3中包含了三个操作:读取i值、i + 1 、将+1结果赋值给i; 
4中同三一样

在单线程环境下我们可以认为整个步骤都是原子性操作,但是在多线程环境下则不同,Java只保证了基本数据类型的变量和赋值操作才是原子性的(注:在32位的JDK环境下,对64位数据的读取不是原子性操作*,如long、double)。在多线程环境中,非原子操作可能会受其他线程的干扰,例如第3个操作,i在加1之后将结果赋值给i,在赋值给i回写主内存的时候可能会被其他线程抢先回写,导致此次执行失败丢失了本次计算结果(这里会涉及到原子性操作,下面会进行讲解)。

public class AtomicTest {

    private int i = 0;

    public void add() {
        i++;
    }

    public static void main(String[] args) {        
        for (int t = 0; t < 10; t++) {
            AtomicTest test = new AtomicTest();
            Thread[] threads = new Thread[10];           
            for (int i = 0; i < threads.length; i++) {
                threads[i] = new Thread(() -> {                    
                for (int k = 0; k < 1000; k++) {
                        test.add();
                    }
                });               
                threads[i].start();
            }            
            Arrays.stream(threads).forEach(th -> {                
            try {
                    th.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });            
            System.out.println("第" + (t + 1) + "次执行结果:" + test.i);
        }
    }
}
第1次执行结果:8987第2次执行结果:8970第3次执行结果:6820第4次执行结果:9841第5次执行结果:10000第6次执行结果:7766第7次执行结果:8105第8次执行结果:10000第9次执行结果:10000第10次执行结果:10000

最终的执行结果会是小于等于10000,在某些情况下与我们所期望的结果10000不符合,并发的情况下导致bug的产生。

要想在多线程环境下保证原子性,则可以通过锁、synchronized来确保。volatile是无法保证复合操作的原子性。

二、可见性

    可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。CPU在执行代码的时候,为了减少变量访问的时间消耗可能将代码中访问的变量的值缓存到该CPU缓存区中,因此,相应的代码再次访问该变量的时候,相应的值可能从CPU缓存中而不是主内存中读取的。同样的,代码对这些被缓存过的变量的值的修改也可能仅是被写入CPU缓存区,而没有写入主内存。由于每个CPU都有自己的缓存区,因此一个CPU缓存区中的内容对于其他CPU而言是不可见的。这就导致了在其他CPU上运行的其他线程可能无法看到其他线程对某个变量值的修改。


    对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

三、有序性(指令重排)


    有序性最终表述的现象是CPU是否按照既定代码顺序执行依次执行指令。编译器和CPU为了提高指令的执行效率可能会进行指令重排序,这使得代码的实际执行方式可能不是按照我们所认为的方式进行,在单线程的情况下只要保证最终执行结果正确即可。如下:

int i = 0;            //语句1  
boolean flag = false; //语句2
i = 1;                //语句3  
flag = true;          //语句4

上面代码最终执行结果是i=1、flag=true,在不影响这个结果的情况下语句2可能比语句1先执行,语句4可能比语句3先执行。此种指令重排之后单线程下不会有问题,单如果是在多线程的情况下呢?

public class SerialTest {
    static SerialTest serialTest;
    static boolean isInit = false;

    public static void main(String[] args) {        
    for(int i=0; i< 200;i++) {
            serialTest = null;
            isInit = false;            
            new Thread(()->{
                serialTest = new SerialTest();//语句1
                isInit = true;                //语句2
            }).start();            
            new Thread(()->{                
                if(isInit) {
                    serialTest.doSomething();
                }
            }).start();
        }
    }    
    public void doSomething() {        
        System.out.println("doSomething");
    }
}

运行上面代码执行的结果如下:

Exception in thread "Thread-283" java.lang.NullPointerException
	at com.cd.concurrent.SerialTest.lambda$main$1(SerialTest.java:25)
	at java.lang.Thread.run(Thread.java:748)
......
doSomething
doSomething
doSomething
doSomething
doSomething
Exception in thread "Thread-283" java.lang.NullPointerException
	at com.cd.concurrent.SerialTest.lambda$main$1(SerialTest.java:25)
	at java.lang.Thread.run(Thread.java:748)

我们所期望的结果应该是每次都会打印doSOmething,可是这里会报空指针异常,出现这种情况的原因就是因为指令重排导致,上面语句1和语句2最终执行顺序可能会变为语句2先执行,语句1还未执行,此时刚有有一个线程独到了isInit的值为true,此时通过对象取调用方法就报空指针,因为此时SerialTest对象还未被实例化。

指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。


happens-before原则


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

推荐阅读更多精彩内容