- 上一篇 我们聊了java 基于JMM 的原则,解决了并发编程中的三个点:
- 原子性
- 有序性
- 内存可见性。
- java 实际解决依赖的内置原语有两个 synchronized 和volatile. 非内置的CAS,lock也是解决上述三个点的解决措施(之后会在其他文章赘述)。
本篇文章,我们简单梳理下java synchornized和volatile的一些日常使用时候的注意点,以及简单的原理概括。
synchronized
用法
- synchronized 常用于方法或者是代码块。
- synchronized 常用下面三个场景
- 修饰实例方法:作用于当前实例加锁,进入同步代码需要获取到当前实例的锁
- 修饰静态方法:作用于当前对象加锁,进入同步代码前要获取到当前对象的锁
- 修饰代码块,指定加锁对象,对指定对象加锁,进入同步代码库前获取到指定对象的锁。
synchronized 底层原理浅析
1 | “JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现” 《方腾飞,魏鹏,程晓明 著. “Java并发编程的艺术 (Java核心技术系列)》 |
- monitorenter 编译后插入同步代码块的开始位置
- monitorexit 编译后插入同步代码块的结束位置。
- 每个对象都有一个Monitor与之关联,当一个monitor被使用后,当线程在运行到monitorenter指令时候,会尝试获取锁。
Monitor
- java 内置 的Monitor(监视器),可以确保一个代码块多线程同时执行指定代码块同时只有一个线程在执行。
具体流程如下
因为synchronized 是依赖于java对象头来做monitor的获取和释放,所以下面聊下JAVA对象头
JAVA 对象头
- 本质上是根据对象头的不同内容来区分开无锁,偏向锁,轻量级锁,重量级锁
- 见下图
- 其中32位机器与64为机器头部放置的内容有点区别
锁升级
- 升级后的锁为无锁状态->偏向锁->轻量级锁。
偏向锁的获取与释放
- 因为大部分线程对统一代码块都是一个线程进行获取,所以引入了偏向锁的概念。
- 偏向锁的意思是,如果一个线程获取到了偏向锁,如果接下来的一段时间没有其他线程来竞争锁,那么持有偏向锁的线程再次进入或退出同一个代码块,不需要再次进行抢占所和释放锁的操作。
- 需要注意的是,如果存在多个线程在竞争同一个同步代码块的时候吗,会触发偏向锁的撤销,升级为轻量级锁。这里可以理解存在偏向锁状态,就是同一个线程在重复的执行同步代码块。
- 偏向锁可以通过 -XX:+UseBiasedLocking开启或者关闭
- 具体的获取与释放见下图
轻量级锁的获取与释放
- 前面聊到,多个线程竞争的时候,会导致偏向锁先升级为轻量锁。
- 轻量级锁,我理解是使用CAS尝试获取锁,如果CAS获取失败,那么就需要升级为重量级锁(线程wait.等待其他节点唤醒)。
- 具体的获取与释放见下图
- 具体的获取与释放见下图
重量级锁
- 当锁升级为重量级锁之后,内部的原理就是讲该线程临时挂起,等待获取锁的线程释放锁之后,再被唤醒。
注意
- 上面的从上面的分析来看,synchronized 在竞争获取锁的时候,是没有先后顺序的,这就有可能导致某个线程一直在挂起的状态。所以就有了java并发包中的lock接口(后续会聊到)。
volatile
保证读写的可见性
- 这块的原理其实就是依赖于JMM的主内容与工作内存。
- 简单来讲就是线程在读写一个volatile修饰的变量时,都会主动刷新到主内存中,或者是主动从主内从读取。
禁止指令重排
- 原理是主要通过内存屏障来实现其内存可见性以及禁止指令重排。
- 关键性的就是要理解CPU指令内存屏障(Memory Barrier): 如果在指令间插入一条内存屏障指令,则表示告诉编译器和CPU,在内存屏障前后的指令都不允许和这条内存屏障重新排序。
- 内存屏障还有一点是,会强行输出各种CPU缓存数据,因此任何CPU上的线程都可以读取到数据的最新版本。
参考大佬(膜拜)
- 《java并发编程的艺术》方腾飞 魏鹏 程晓明 著
- 全面理解Java内存模型(JMM)及volatile关键字
- 深入理解Volatile
- (五)Synchronized原理分析