Java并发_01_JMM_02_内存模型
一、 前言
1. 关于CPU缓存
- 现代计算机都可支持多任务处理,因为如果只让CPU去单任务处理的话,由于CPU的运算速度和它的存储和通信子系统速度差距太大,大多的时间都浪费在磁盘I/O、网络通信或者数据库访问上,这让计算机的性能大大降低。
- 引入 多任务,则可以让CPU同时去处理多个任务,在一个任务进行I/O时,大可不必理会它,则去执行其他任务。
- 其次现代计算机都在硬件上添加一层读写速度借鉴处理器运算素的的 高速缓存 (Cache)来作为内存和处理器之间的缓存,将运算需要的数据复制一份到缓冲中。
- 带来的问题:
- 多个处理器 一起协同工作时,各自的缓冲区数据如何保证一致?
- 这时就需要,各个处理器在访问缓存时,都遵循 缓存一致性协议, 每个处理器通过嗅探在总线上传播的数据来检查自己的缓存是否过期,当处理器发现自己缓存过期,就会将当前处理器的缓存行设置为无效状态,在处理器要对这个数据进行操作时,会强制重新从系统内存中把数据读到处理器缓存里。
2. 关于Java的内存模型
- Java内存模型如同处理器的缓存一样
- JMM规定了所有的变量都应存储主存当中,每个线程都有自己的工作内存。
- 线程对变量的所有操作都是在自己的工作内存中进行的,而不能直接对主存进行操作,并且每个线程不能访问其它线程的工作内存。
- Java线程之间的通信由 JMM 通过使用lock/unlock/read/load/use/assign/store/write 操作来进行控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
二、原子性、可见性、有序性
- 对于并发而言,要想正确的执行程序,必须保证 原子性、可见性 和 有序性。 只要有一个没有被保证,就可能会导致 程序运行不正确。
1. 原子性
- 一个或者多个操作要么全部执行且执行过程不可被任何因素打断,要么就不执行。
- 可以通过 synchronize 、 Lock 或者 CAS 来实现 原子性
- JMM提供了 8 种原子性变量的操作:( luck、unlock )、( read、load )、( assign、use )、(store、write )
- 其中两两一对使用,有前一个操作必须有后一个操作。
- 其中 luck 和 unlock 操作也能保证原子性,但是虚拟机没有将这两个操作暴露给用户,但是却提供了更高层次的 monitorenter 和 monitorexit 来隐式的使用这两个操作。
- 在Java中,只有对 除 long 和 double 外的基本类型进项简单的赋值(如 int a = 1 ; int a = b;不是原子性的,因为要读取b的值)或读取操作,才是原子的。
- 如果JVM为32位的,则JVM不保证64位的long和double型 的操作的原子性
- 在JDK1.5之前,对 long 和 double 的写操作和读操作都不保证是原子性的。
- 在JDK1.5之后,保证了读操作的原子性,但是写操作还是分为2步去写。
- 64位的JVM,long 和 double 读写都是原子操作。
- 要解决 long 和 double 的非原子性操作,可以在类型前加上 volatile ,操作就是原子性的了。
- JVM 不保证 long 和double在 32位机下 写操作的原子性,但是可以保证 volatile 修饰的 long 和double 写操作的原子性。
- 如果JVM为32位的,则JVM不保证64位的long和double型 的操作的原子性
- Java 提供了13中原子类:https://blog.csdn.net/qq_39541319/article/details/89851725
2. 可见性
- 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量,那么其他线程能够立即得知这个修改。
- 可以通过 synchronized 、Lock 、final 或者 volatile 来实现。
- volatile 保证可见性 ,它保证新值能够立即同步到主存中,而且每次使用前立即从主存刷新。
- volatile的实现原理:https://blog.csdn.net/qq_39541319/article/details/88831053
- synchronized实现可见性,是因为同步块 对一个变量执行unlock之前,必须先把此变量同步回主存中(执行strore、write操作)
- final 关键字的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去(没有引用逃逸),那在其他线程中能立马看到final字段的值。
- final操作原语 :https://blog.csdn.net/qq_39541319/article/details/89849706
- 引用逃逸 :https://blog.csdn.net/qq_39541319/article/details/89849725
3. 有序性
- 有序性是指程序指向的顺序按照代码的先后顺序执行。
- 可以使用 volatile 和 synchronized 实现有序性
- volatile 实现有序性是 volatile 通过插入内存屏障的方式禁止指令重排序
- synchronized 是通过锁实现线程的互斥访问,使得两个同步块只能串行地进入(注意:synchronized 修饰的同步块临界区中还是会进行指令重排序,由于同步块里面是互斥访问的,所以临界区中的指令重排序不会影响)
三、重排序
在执行程序的时候,为了提高并行度,编译器和处理器会对指令进行重排序。
并行度: 指令或数据并行执行的最大数目。在指令流水中,同时执行多条指令称为指令并行。
1. 重排序的种类及重排序条件
(加粗部分为重排序的条件)
- 编译器重排序(编译器优化重排序):编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
- 处理器重排序(指令级并行的重排序,内存系统的重排序):如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
2. 带来的问题
- 重排序在多线程环境下可能导致数据不安全
- 解决
- 可使用 volatile 来禁止指令重排序(插入相应的内存屏障)https://blog.csdn.net/qq_39541319/article/details/89814486
- synchronized 修饰的代码块中 还是会进行指令重排序,但是由于线程的互斥访问,这种重排序不会有影响
3. 应用:DCL 单例模式:
https://blog.csdn.net/qq_39541319/article/details/89851821
四、顺序一致性
- 一个线程中所有的操作必须按照程序的顺序来执行;
- 不管程序是否同步,所有线程都只能看到一个单一的执行顺序。
- 每个操作都必须原子执行且立即对所有线程可见
- JMM不保证未同步的程序的执行结果与该程序顺序一致性模型中的执行结果相同。
五、as-if-serial
- 编译器和处理器不管对代码怎么进行重排序没应保证代码在单线程下执行结果是不变的。
- 我们理解的一段程序代码的执行在单个线程中看起来是有序的,顺序执行的。那是因为单线程中无法观测到重排序带来的问题。
- 事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但是无法保证程序在多线程执行结果的正确性。
六、happens-before
- JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
- 如果一个操作 happens-before 另外一个操作,那么第一个操作的直系那个结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
- 两个操作制之间存在 happens-before 关系,并不意味着一定要按照 happens-before原则的顺序来执行,如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
1. 理解
- happens-before 是JMM最核心的理论,保证内存可见性。
- JMM在设计时,一方面为程序员提供足够强的内存可见性保证,另一方面,对编译器和处理器的限制要尽可能放松。
- 通过happens-before 关系,不允许影响程序执行结果发生改变的重排序。
- 只要不改变程序的直系那个结果(指的是单线程程序 和 正确同步的多线程程序),编译器和处理器怎么优化都可以。
- as-if-serial 语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
- as-if-serial 语义给编写单线程程序的程序员创造了一个幻境:单线程程序是程序的顺序来执行的。
- happens-before 关系给编写正确同步的多线程程序的程序员创建一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。