原子性、可见性、有序性

1.原子性

1.1 原子性概念

  介绍原子性的概念之前,我们首先介绍下基本的化学概念——原子。原子是化学反应不可再分的基本微粒,在化学反应中不可分割。由此引申,原子性或者说原子操作是指一个操作时不可中断的,这个操作执行要么全部成功要不全部失败,不可能存在成功一部分,失败一部分的情况。即使是在并发场景里,原子操作一旦开始执行就不会受到其他线程的影响。

  在博客Java内存模型(JMM)第二节中线程工作内存与主内存的交互介绍到虚拟机实现了8种操作是原子的、不可再分的。即lock(锁定)、unlock(解锁)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)这8种操作是原子操作

  其中虚拟机要对于32位基本数据类型(boolean、byte、char、short、int、float)的操作read、load、use、assign、store、write是原子性的,对于64位基本数据类型(long、double)的read、load、use、assign、store、write操作不做要求,即long和double的非原子性协定。因为最开始的虚拟机实现规范中,只要求虚拟机一次性读取32位的数据,而64位的数据就需要分两次读取,是可以被分割的,即可能被中断。在多线程的情况下,线程有可能只能读到64位数据中的32位,造成原子性问题。但是,对于一般的商业化虚拟机,都会默认实现long和double的原子性操作。所以大致可以认为上述6种操作对于基本数据类型是原子性的。

  lock与unkock操作是为了保证更大范围的原子性操作,被lock和unlock操作包含了多个操作是原子性的,这些操作能够全部被执行。这两个操作映射到JVM指令中即为moniterenter和moniterexit指令,被moniterenter和moniterexit指令包含的指令可以全部被执行,代码层面上即表现为synchronized关键字,所以synchronized可以保证原子性。但是这里需要声明不是只有加锁才能保证原子性,还可以通过无锁操作(如比较并交换CAS、加载链接/条件存储LL/SC等)也能保证原子性,只要这么指令能保证操作不能被中断即可。

1.2为什么会有原子性问题

  显而易见的,a =10 是原子操作,是因为虚拟机内部实现做了保证。而a++不是原子操作,看起来只有一行代码,一个操作,但是a++等同于a = a + 1处理器执行过程中被分为了三个操作,大体可以看成:将a从内存中读到寄存器中,在寄存器中将a的值加1,将寄存器中的值写回到内存中。类比到虚拟机中,即为将将a从主存中读到线程工作内存中,在工作内存中将a的值加1,将工作内存中的值写回到主存。它是可分的,意味着是可中断的,所以a++不是原子操作。

  那么为什么会有原子性问题呢,在博客线程安全中我们提到了时间片的概念,其中时间片的轮转造成了原子性问题。时间片的轮转导致了操作被分割,一旦被分割,当前线程就失去了将操作全部完成的可能性,产生了原子性问题。

1.3 硬件支持

  原子性不可能由软件单独保证,必须需要硬件的支持,因此是和架构相关的。

  总线锁:在x86 平台上,CPU提供了在指令执行期间对总线加锁的手段。CPU芯片上有一条引线#HLOCK pin,如果汇编语言的程序中在一条指令前面加上前缀"LOCK",经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。

  缓存锁:是指内存区域中的数据如果已经被缓存在处理器的缓存行中,并且缓存行的数据在Lock操作期间被锁定时,那么当它执行锁操作到内存时,通过缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行数据时,会使缓存行无效。就整体来说,是当某块CPU对缓存中的数据进行操作了之后,就通知其他CPU放弃储存在它们内部的缓存,或者从主内存中重新读取。

  此外,我们常说的无锁操作保证原子性底层也是通过硬件保证的,如CAS是通过cmpxchg指令(IA64、x86架构)或casa指令(sparc-TSO架构)完成的、LL/SC是通过一对Idrex/strex指令(ARM、PowerPC架构)完成的。

2.可见性

2.1 可见性概念

  可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。在Java内存模型中,一个线程修改共享变量的值是在工作内存中完成的,而其他线程是无法访问该线程的工作内存的,修改完成后再将新的值同步回主内存中;其他线程需要从主内存读取变量的值才能获知修改,实现可见性。

2.2 为什么有可见性问题

  因为在修改成新的值再到同步回主内存中这两个动作不是连续发生的,所以导致其他线程不能够立即得知这个修改。当前线程修改工作内存中该共享变量的值,其他线程读取内存中该变量未修改前的值,当前线程再将修改后的值同步回内存中,于是就产生了可见性问题。

  在原子性中我们提到,synchronized底层通过lock和unlock实现原子性,synchronized同样可以保证可见性。因为在Java内存模型中我们提到:对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。而在对一个变量执行unlock操作之前,其他线程是无法访问该变量的。而在unlock结束,其他线程访问变量的值肯定是已经修改之后的值了。

  volatile同样可以保证可见性。因为在Java内存模型中我们提到:每次修改volatile变量的值后必须立刻同步回主内存,每次使用volatile变量前必须从主内存中刷新最新值。所以每次修改后的值在其他线程使用变量前(从主内存读取)都能被感知。

  final也可以保证可见性。被final修饰的字段在构造器中一旦初始化完成,并且没有把“this”的引用传递出去,那么在其他线程中就能看见final字段的值。也有的书籍中将final的可见性称为final的不变性,因为不变,那么不需要任何同步手段,就能保证任意线程看到的final字段。

3.有序性

3.1 有序性概念

  Java中天然的有序性是指:在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”和工作内存与主内存同步延迟的现象。所以,在同一个线程内,所有的操作都是串行的,有序的,但是多个线程并行的情况下,则不能保证其操作的有序性。

3.2 为什么有有序性问题

  在概念中已经提到原因:指令重排序和工作内存与主内存同步延迟。这两个原因造成程序的书写顺序(单线程下串行的有序)与实际CPU的执行顺序(多线程下顺序是不可预测的)是不一致的更深层次的原因就是硬件原因了:CPU为了优化性能,缓存与主内存的访问速度差异导致最终的顺序产生变化,最终造成有序性问题。

  synchronized可以保证有序性。因为因为在Java内存模型中我们提到:一个变量在同一时刻只允许一条线程对其进行lock操作。所以,线程实质上串行执行的。

  volatile同样可以保证有序性。volatile关键字禁止了指令重排序。

参考
  《深入理解Java虚拟机》
  https://www.cnblogs.com/xiaoxi/p/6392154.html
  https://blog.csdn.net/javazejian/article/details/72772461#

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值