Java 并发学习之 JMM

Java 并发学习之 JMM

顺序一致性模型与 JMM

顺序一致性模型是一种理想的内存模型,在这个模型下,指令是严格按照代码的编写顺序执行,同时所有线程只能看到同一个内存区且对内存区的操作都是互斥的,内存对所有线程都是可见的。

JMM 中,由于每个线程有自己的工作内存,很多情况下,只是对工作内存中的变量副本进行修改而未真正同步到主内存中,因此每个线程对内存的更改对其他线程都是不可见的,同时出于对性能的优化,指令的顺序经过编译器和处理器的重排,其执行的顺序跟代码的编写顺序很有可能不一样。所以导致了 JMM 不可能像顺序一致性模型那样具有极强的内存可见性保证,在 JMM 中如果要一个操作执行的结果对另一个操作可见,那么这两个操作必须满足 happens-before 原则。

什么是指令重排?

指令重排是编译器和处理器用于优化程序性能的手段,举个例子,指令 B 的执行依赖指令 A 的结果,此时有一些对内存的访问操作就可以插入指令 B 和指令 A 之间,以提高 IO 的效率。指令重排有一个基本原则,就是在单线程中遵守数据的依赖关系(写写、写读、读写指令间都是有依赖关系,不允许重排),从而保证单线程中执行结果的一致性。

本文将从happens-before 原则出发,分析 JMM 中用于提供内存可见性的设计。

happens-before

volatile

volatile 变量具有可见性原子性。可见性是指对一个 volatile 变量的读总是能够看到对这个 volatile 变量的最后写入;原子性是指对 volatile 变量的读操作和写操作具有原子性,复合操作(volatile++)不具有。

为了保证 volatile 变量的两个特性,JMM 在对 volatile 变量的读操作和写操作进行以下的设计:

  1. 当写一个 volatile 变量时,JMM 会将线程工作内存的变量刷新到主内存。
  2. 当读一个 volatile 变量时,JMM 会将工作内存的变量置为无效,直接从主内存中获取。

由于指令的重排序导致多个线程下,变量的读写依赖无法被满足,JMM 对 volatile 变量的指令重排做了限制:

  1. volatile 写之前的操作不能被重排到 volatile 写之后。
  2. volatile 读之后的操作不能被重排到 volatile 读之前。
  3. 当第一个操作是 volatile 写,第二个操作是 volatile 读时,不能重排。

这些限制都是通过 JMM 内存屏障来实现的。内存屏障的细节本文不做详细介绍。

针对锁的获取到释放过程遵循以下三个 happens-before 关系:

  1. 程序次序规则:先获取锁,再执行临界区代码
  2. 监视器规则:先释放锁,再获取锁
  3. happens-before 传递性:先获取锁的临界区代码先执行

为了保证内存的可见性,JMM 在锁的释放和获取操作进行以下的设计:

  1. 当线程释放锁时,会把该线程对应的工作内存的变量刷新到主内存。
  2. 当线程获取锁时,会把线程对应的工作内存的变量置为无效,从而使得临界区代码必须从主内存中读取变量。

JMM 锁的释放和获取操作的可见性实际上是通过对 volatile 变量的操作来实现的。(参考 AQS 的实现)

final 域

JMM 对 final 域的重排序也做了限制:

  1. 在构造函数内对 final 域的写入,与随后将被构造对象的引用赋值给一个引用对象,这两个操作不能重排。
  2. 初次读包含 final 域对象的引用,与随后初次读这个 final 域,这两个操作不能重排。

这些限制也是通过 JMM 内存屏障实现的。

线程

在线程 A 中执行操作 ThreadB.start(),A 线程中的 ThreadB.start() 操作 happens-before 线程 B 中的任意操作。在线程 A 中执行操作 ThreadB.join(),B 线程中的任意操作 happens-before 线程 A 从 Thread.join() 操作返回。

原文地址:https://www.cnblogs.com/bdsir/p/8719550.html

时间: 2024-08-27 08:11:39

Java 并发学习之 JMM的相关文章

Java并发学习之五——线程的睡眠和恢复

本文是学习网络上的文章时的总结,感谢大家无私的分享. 1.Thread类的sleep方法,可以使线程睡眠.此方法接收一个整数作为参数,表示线程暂停运行的毫秒数.在调用sleep方法后,当时间结束时,JVM会安排他们CPU时间,线程会继续按指令执行. 另一种可能是使用一个有TimeUnit列举元素的sleep方法,使用线程类的sleep方法让当前线程睡眠,但是它接收的参数单位后会转成毫秒的. 2.当你调用sleep()方法,Thread离开CPU并在一段时间内停止运行.在这段时间内,他是不消耗CP

Java并发学习笔记(九)-原子类AtomicInteger

AtomicInteger能够保证对一个整型的操作是原子性.像i++这个操作不是原子操作,存在竞态条件,所以需要加锁,但是加锁的性能不高,如果仅仅为了对一个整数加1.我们来看下他的实现. private volatile int value; AtomicInteger本身持有一个整型变量,所有的操作都是基于这个变量的.变量由violate修饰,这个变量是保证可见性的,具体可见另一篇博客 Java并发学习笔记(六)-互斥性和内存可见性. 来看一下对value加1的操作 public final

Java并发学习之二——获取和设置线程信息

本文是学习网络上的文章时的总结,感谢大家无私的分享. Thread类的对象中保存了一些属性信息能够帮助我们辨别每一个线程,知道它的一些信息 ID:每个线程的独特标示: Name:线程的名称: Priority:线程对象的优先级.优先级别在1-10之间,1是最低级,10是最高级. Status:线程状态.在java中,线程只有6种状态:new,runnable,blocked,waiting,time waiting 或terminated. 现在写一个程序,将线程的信息保存到文件中方便查看 pa

Java并发学习之七——守护线程

本文是学习网络上的文章时的总结,感谢大家无私的分享. 1.Java有两种Thread:"守护线程Daemon"与"用户线程User".用户线程:Java虚拟机在它所有非守护线程已经离开后自动离开:守护线程:则是用来服务用户线程的,如果没有其他用户线程在运行,那么就没有可服务对象,也就没有理由继续下去. 2.setDaemon(boolean on)方法可以方便的设置线程的Daemon模式,true为Daemon模式,此方法必须在线程启动之前调用,当线程正在运行时调用

Java并发学习之四——操作线程的中断机制

本文是学习网络上的文章时的总结,感谢大家无私的分享. 1.如果线程实现的是由复杂算法分成的一些方法,或者他的方法有递归调用,那么我们可以用更好的机制来控制线程中断.为了这个Java提供了InterruptedException异常.当你检测到程序的中断并在run()方法内捕获,你可以抛这个异常. 2.InterruptedException异常是由一些与并发API相关的Java方法,如sleep()抛出的. 下面以程序解释 package chapter; import java.io.File

【Todo】Java并发学习 & 示例练习及代码

接上一篇:http://www.cnblogs.com/charlesblc/p/6097111.html <Java并发学习 & Executor学习 & 异常逃逸 & 同步互斥Best Practice & wait/notify, conditon#await/signal> 原文参考:http://www.ciaoshen.com/2016/10/28/tij4-21/ <Thinking in Java 读书笔记:第二十一章 - 并发> 第

Java并发学习之八——在线程中处理不受控制的异常

本文是学习网络上的文章时的总结,感谢大家无私的分享. 1.Java里有2种异常: 检查异常:这些异常必须强制捕获她们或在一个方法里的throws子句中. 未检查异常:这些异常不用强制捕获它们. 2.在一个线程对象的run()方法里抛出一个检查异常,我们必须捕获并处理她们.因为run()方法不接受throws子句.当一个非检查异常抛出,默认的的行为是在控制台写下stack trace并退出程序. package chapter; public class Main8 { /** * <p> *

Java并发学习之六——等待线程的终结

本文是学习网络上的文章时的总结,感谢大家无私的分享. 1.在某些情况下,我们需要等待线程的终结.例如,我们可能会遇到程序在执行前需要初始化资源.在执行剩下的代码之前,我们需要等待线程完成初始化任务.为了达到此目的,我们使用Thread类的join()方法.当前线程调用某个线程的这个方法时,它会暂停当前线程,直到被调用线程执行完成. 2.Java提供2种形式的join()方法: Join(longmilliseconds) Join(long milliseconds,long nanos) 第一

Java并发学习之三——线程的中断

本文是学习网络上的文章时的总结,感谢大家无私的分享. 1.一个多个线程在执行的Java程序,只有当其全部的线程执行结束时(更具体的说,是所有非守护线程结束或者某个线程调用System.exit()方法的时候),它才会结束运行.有时,你需要为了终止程序而结束一个线程,或者当程序的用户想要取消某个Thread对象正在做的任务. 2.Java提供中断机制来通知线程表明我们想要结束它.中断机制的特性是线程需要检查是否被中断,而且还可以决定是否相应结束的请求.所以,线程可以忽略中断请求并且继续运行. 3.