Java内存模型小析之原子性和可见性(二)

在上篇文章中我们简单的说了一下jvm的内存布局(点这里查看),在这篇文章中我们继续java内存模型方面的东西。

原子性

注意这里的原子性不是数据库事务中的原子性。这里原子性的定义是这样的:
一个操作或一系列是不可中断的。即使是在多个线程一起执行的时候,这些操作一旦开始,就不会被其他线程干扰

什么操作具有原子性

  1. 正确同步的程序:临界区内代码的执行具有原子性。
  2. 单个volatile变量的读/写具有原子性。
  3. 任意的单个变量的读操作具有原子性。
  4. 使用CAS对一个共享变量执行操作具有原子性。

什么操作不具有原子性

  1. i++复合操作。
  2. 多线程中未正确同步的代码。
  3. JMM中不保证:在32位的处理器上对64位的long型和double型变量的写操作具有原子性。(当JVM在这种处理器上运行时,可能会把一个64位long/double型变量的写
    操作拆分为两个32位的写操作来执行)。

处理器对原子操作的实现

上面我们说过i++的复合操作不具有原子性。那么i++操作为什么不具有原子性呢?因为i++这个操作至少可以分为下面这三个步骤:先读i的i值,对i的值进行加1的操作,然后将修改过i的值写入到内存中。即读–改–写操作。这样在并发编程中就会出现问题了,如下所示:
《Java内存模型小析之原子性和可见性(二)》CPU1和CPU2同时从各自的缓存中读取变量i,分别进行加1的操作,然后分别写入系统内存中。我们之前可能的期望可能是:CPU1+1,CPU2+1结果应该是+2,但是可能的结果确实+1。但是处理器可以通过总线锁定和缓存锁定来解决这个问题。简单说就是:CPU1在读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。

总线锁定

所谓总线锁定就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。但是总线锁定会有性能问题,因为在同一时刻,我们只需保证对某个内存地址的操作是原子性即可,但是总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据。这个性能上的损耗是非常大的。

缓存锁定

缓存一致性协议

    在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线  上传播的数据来检查自己缓存的值是不是过期了,当处理器
发现自己缓存行对应的内存地址被修改,就会将当前 处理器得到缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到
处理器
缓存行中。
频繁使用的内存会缓存在处理器的L1、L2和L3高速缓存里,那么如果我们要操作的数据在同一个缓存行里,那么我们就可以直接对这个缓存行进行锁定就行了。所以缓存锁定是值内存区域如果被缓存在处理器的和缓存行中,并且在LOCK操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改缓存行的内存地址,并允许它的缓存一致性机制来保证操作的原子性。因为缓存一致性机制会阻止同时修改由两个以上处理器缓存行的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使和缓存行无效。

临界区

临界区用来表示一种公共资源或者说是共享数据,它是可以被多个线程使用。
但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待

共享变量

java中的并发采用的是共享内存模型的方式。共享内存是什么呢?我们知道JVM中堆内存和方法区(这里区分开来了)是在线程之间共享的。  那么这部分内存都包含什么东西呢?包含实例域、静态域、数组元素等。它们又可以称为
共享变量。  java并发线程之间的通信是由java内存模型(JMM)控制的,JMM决定了一个线程对共享变量的写入何时对另一个线程可见(
在默认的情况下,JVM并不要求每个变量在任意时刻都保持同步!)。JMM可以做这样的一个抽象:  线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(或者成为工作内存),本地内存中存储了该线程读-写共享变量的副本。本地内存是JMM的一个抽象概念,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。 如下图所示:

《Java内存模型小析之原子性和可见性(二)》
《Java内存模型小析之原子性和可见性(二)》

可见性

可见性是指当一个线程修改了某一个共享变量的值,其他线程能够
立即知道这个修改。我们在并发编程遇到了一个大的问题是可见性问题,即一个线程对一个共享变量修改的值,另外一个线程读不到这个修改后的值。引起可见性问题的原因有很多,像编译器优化,处理器优化等等。

写缓冲区

 现代处理器使用写缓冲区临时保存向内存写入的数据,写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。同时,通过以批处理的方式 刷新写缓冲区,以及合并写缓冲区对同一内存地址的多次写,减少对内存总线的占用。每个处理器上的写缓冲区,仅仅对它所在的处理器可见(会引起可见性问题)。这个特性会导致处理器对内存的读/写操作的执行顺序  不一定与内存实际发生的读/写操作顺序一致。

我们在下一章中会介绍重排序的内容。

参考资料:
Java并发编程的艺术。
缓存一致性协议:
http://www.infoq.com/cn/articles/cache-coherency-primer/
http://blog.csdn.net/muxiqingyang/article/details/6615199

    原文作者:java内存模型
    原文地址: https://blog.csdn.net/zknxx/article/details/72836270
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞