Java Memory Model——JMM。
一 概念
- 定义了多线程之间共享变量的可见性。
- 对共享变量进行同步。
- 并发编程的两个问题:通信和同步。
二 线程间通信方式——共享内存模型
- 一个线程的共享变量 对另一个线程可见。
- 线程之间的共享变量存储在
主内存Main Memory
- 每个线程都有一个私有的
本地内存 Local Memory
,本地内存存储该线程以读/写共享变量的副本。 - 本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了写缓冲,寄存器,以及其他硬件和编译器优化。
1 两个线程之间的通信
- 线程A把本地内存A中修改过的共享变量同步到主存中。
- B线程到主内存中读取A线程修改过的共享变量。
2 JMM的实现
- Java内存模型把内存分为2部分:线程栈区ThreadStack,堆区Heap。
- 线程栈也称为调用栈,包含了
当前线程
执行的方法调用相关信息,当前方法
的所有局部变量信息,存储基本数据类型
的本地变量,局部变量的对象的引用。 - 堆包含Java应用创建的所有对象信息,包括基本数据类型的
封装类
;不管对象是属于成员变量,还是方法中的局部变量,都会被存储在堆中。static变量以及类本身都会存储在堆中。
3 JMM基础原理
- 指令重排序
- 数据依赖性
- 内存屏障
三 硬件内存
- CPU寄存器(在CPU内部),CPU多级缓存,RAM主存。
- Java内存模型和硬件内存交叉访问。堆和栈都可以访问硬件所有的存储空间。
- 当对象和变量存储到计算机的各个区域时,必然会有2个问题:
- 共享对象对各个线程的可见性
- 共享对象的竞争现象
四 关于并发编程
- 核心理论
- 线程间的协作(wait/notify/sleep/yield/join)
- 竞争对象
Synchronized
及其实现原理,底层优化(轻量级锁,偏向锁) - 共享对象的可见性
volatile
的使用及其原理
1 并发核心理论
- 共享性
- 互斥性:资源互斥,只允许一个访问者对其进行访问,具有唯一性和排他性。将锁分为共享锁和排它锁,也称读锁和写锁。
- 原子性:数据的操作是一个独立不可分割的整体(如 i++).
- 可见性
- 有序性:
- 编译器优化的重排序。
- 指令级并行的重排序。
- 内存系统的重排序。
2 线程间协作
2.1 线程状态
- 新建 New: new Thread();
- 就绪 Runnable: 调用start后,等待CPU资源。
- 运行 Running : 就绪状态获取到CPU执行权后进入运行态。就是进入run方法。
- 阻塞 Blocked:线程没有执行完,就让出CPU,自身进入阻塞状态。
- 死亡 Dead:线程执行完成或执行过程中产生异常。
2.2 Object类 wait/notify/notifyAll方法的使用
- wait()是将当前线程挂起,进入阻塞状态,直到notify或notifyAll方法来唤醒线程,如设定时间后在这个时间范围内没有唤醒,则会自动唤醒。
- notify/notifyAll() 唤醒对应的对象的monitor。
- wait()后,线程会释放对monitor对象的所有权。但wait()必须依赖于同步。synchronized。
- 一个通过wait()阻塞的线程,必须满足:线程需要被唤醒(notify),线程唤醒后需要竞争到锁(monitor)。
2.3 Thread类 sleep/yield/join方法
- sleep()让当前线程暂停指定的时间(ms)。只是让出CPU,并不释放锁。
- yield()暂停当前线程,以便其他线程有机会执行,不过不能指定暂停时间,也不保证马上就暂停。Running状态变为Runnable就绪状态。一般用来测试调试。
- join()父线程等待子线程执行完成后再执行——
将异步执行的线程合并为同步的线程
。- 通过wait()来将线程的阻塞,如果join的线程还在执行,则将当前线程阻塞起 来,直到join的线程执行完成,当前线程才能执行。
- join只调用了wait(),却没有对应的notify(),原因是Thread的start()中做了相应的处理,所以当join的线程执行完成以后,会自动唤醒主线程继续往下执行。
3 竞争对象——互斥 synchronizd
synchronized是Java中解决并发问题最常见的一种方法,也是最简单的。
3.1 Synchronized 作用
- 保证共享变量的修改能够及时可见——多线程共享一个对象,同时去修改它,保持可见性。
- 确保线程互斥的访问,以及同步代码——即同一时间只能一个线程进入代码竞争区。
- 有效解决重排序问题。
- 三种用法:修饰普通Method,static Method,代码块。
public synchroinzed void method() {}
public static synchroinzed void staticMethod() {}
public void codeBlockMethod() {
synchroinzed (this) {
thread.doSomethind();
}
}
3.2 Synchroinzed 原理
- 对于方法块来说,使用的是monitorenter, monitorexit
- 关于monitorenter:
- 如果monitor entry count = 0,则该线程进入monitor,然后reset 进入数=1,该线程为monitor的所有者。
- 如果线程已经占用该monitor,只是重新进入,reset monitor的进入数=1;
- 如果其他线程占用了monitor,则该线程进入阻塞,知道monitor进入数为0时,重新尝试获取monitor的所有权 ownership。
- 关于monitorexit:
- 执行monitorexit的线程必须是Objectref所对应的monitor的所有者。
- 指令执行时,monitor进入数-1,线程退出monitor,不再是这个monitor的owner。其他阻塞线程可尝试获取monitor的ownership。
-
wait/notify等方法也依赖monitor对象,这就是为何只有在
同步的块或方法中
才可调用wait/notif,否则会抛出IllegalMonitorStateException
. - 对于方法的同步,常量池中有一个ACC_SYNCHRONIZED标识符。JVM根据此符号实现方法调用。
- 方法调用时,调用指令会检查这个标识符是否被设置。如果设置了,执行线程将先获取monitor,获取成功过后执行方法体,执行完后再释放。
- 在执行期间,任何线程都无法再获得同一个monitor对象。
- 本质上都是要用monitor实现同步,但方法的同步是一种隐式的实现,无需通过字节码完成。
3.2 Synchronized 执行结果
- 1个对象的2个普通方法的同步:竞争对象上的锁(monitor),只能互斥获取到锁。所以就会顺序执行两方法。
- 同一个类的2个对象的2个static 方法的同步:获取同一个类上的锁monitor,也只能顺序执行。
- 2个代码块的同步:竞争1个对象this的monitor,(sychronized(this)),顺序执行。
3.3 Synchronized 底层优化——偏向锁,轻量级
3.3.1 重量级锁
- Synchronized 是通过对象内部的一个叫做monitor锁来实现的。
- 但监视器锁本身依赖于底层OS的
Mutex Lock
来实现的,OS实现线程间的切换需要从用户态->核心态。状态之间的切换导致Synchronized低效的原因。 - Mutex Lock称为重量级锁。Java1.6后为了减少获得锁和释放锁带来的性能消耗,引入了
轻量级锁,偏向锁
。
3.3.2 轻量级锁
- 锁的四种状态:无锁状态,偏向锁,轻量级锁和重量级锁。锁的升级是单向的,只能从低向高升级。
3.3.3 几种 锁的比较
4 共享对象的可见性 volatile
4.1 volatile 作用
- 防止重排序。在并发环境下的单例模式中,一般采用双重检查锁DDL的方式实现。
- 实例化一个对象的步骤:1 分配内存空间 2 初始化对象 3 将内存空间的地址赋值给对应的引用。
- 使用volatile修饰singleton,就是为了防止在多线程环境下这个过程的重排序,将一个未初始化的对象引用暴露出来。
public static volatile Singleton singletion; public static Singleton getInstance() { if (singleton == null) { synchroinzed (this) { singleton = new Singleton(); } } }
- 实现可见性:一个线程修改了共享变量值,保证另一个线程可见。保证变量直接从主存读取,对变量的更新也会直接写到主存中。
- 保证原子性:对volatile变量的单次读写操作可以保证原子性,如long和double类型变量,但不能保证i++这种操作的原子性,因为本质上i++是读写两次操作。若要保证这类操作的原子性可通过
AtomicInteger 或 Synchronized
。
4.2 volatile 原理
- 有序性的实现。
- a happen-before b规则: a所做的任何操作对b可见。单线程内按代码顺序执行。
- monitor解锁操作 happen-before 加锁操作 (Sychronized规则)。
- 对volatile变量的写操作 happen-before 读操作 (volatile规则)。
- 线程start()方法 happen-before 该线程所有 后续操作 (线程启动规则)。
- 线程所有的操作 happen-before 其他线程在该线程调用 join返回成功后的操作;
- 如果 a happen-before b, b happen-before c, 则 a happen-before c (传递性)。
- 可见性的实现。
- 线程的工作内存和主内存进行数据交互。
- volatile变量修改后强制更新到主内存中。
- volatile变量修改后其他线程工作内存的对应值失效,使用时需要从主内存中读取。
- 内存屏障 —— 内存栅栏
- 为了实现volatile可见性和happen-before的语义,JVM使用
内存屏障
来完成。 - 是一组处理器指令,用于实现对内存操作的顺序限制。
- 为了实现volatile可见性和happen-before的语义,JVM使用
参考文献 Java并发编程