JAVA并发编程之可见性,原子性与有序性

344 阅读5分钟

可见性,原子性与有序性

这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战

前言

上文讲解了java的JMM模型,不了解的小伙伴可以点击这里JAVA并发之JMM模型,在并发中,有三个比较重要的特性是我们需要了解的,有序性,原子性,可见性,接下来将主要讲解这三个特性!

原子性

原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响!

什么样的操作不属于原子性?

    假如现在使用的是32位的虚拟机,当在操作long或者double时候,假设A线程操作完前32位的原子操作,接着B恰好读取了后32位的数据!

    也就是说这样可能会读取到一个既非原值又不是线程修改值的变量,它可能是“半个变量”的数值,即64位数据被两个线程分成了两次读取,因此不能成为原子性,但是目前使用的虚拟机几乎都是64位,几乎都把64位的数据读写操作为原子操作,因此了解即可。

X=10; //原子性(简单的读取、将数字赋值给变量)
Y = x; //变量之间的相互赋值,不是原子操作

有序性

    有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,这样的理解并没有毛病,毕竟对于单线程而言确实如此,但对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致,要明白的是,在Java程序中:

倘若在本线程内,所有操作都视为有序行为

如果是多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的,前半句指的是单线程内保证串行语义执行的一致性,后半句则指指令重排现象和工作内存与主内存同步延迟现象

指令重排

    java在JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。

指令重排序的意义?

    让JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。

从源码到最终执行的指令序列示意图

image.png

as-if-serial语义

    as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

    为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

可见性

    理解了指令重排现象后,可见性容易了,可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。对于串行程序来(单线程)说,可见性是不存在的,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。

    但在多线程环境中可就不一定了,前面我们分析过,由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,因此会有以下这种情况:

  • 线程A修改了共享变量x的值,还未写回主内存

  • 线程B又对主内存中同一个共享变量x进行操作

    上述情况中A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同延迟现象就造成了可见性问题,另外指令重排以及编译器优化也可能导致可见性问题,通过前面的分析,我们知道无论是编译器优化还是处理器优化的重排现象,在多线程环境下,确实会导致程序轮序执行的问题,从而也就导致可见性问题。

小结

    本篇为大家介绍了什么是原子性,可见性与有序性,以及java内部的指令重排,后续将为大家讲解下JMM如何解决上述问题的!