synchronized 的使用与原理
基本语法
synchronized 有三种方式来加锁,不同的修饰类型,代表锁的控制粒度
- 修饰实例方法,获取当前对象锁;
- 修饰静态方法,获取当前类的Class对象锁;
- 修饰代码块,获取指定对象锁;
// 修饰实例方法
private synchronized int nextThreadNum() {
return 0;
}
// 修饰静态方法
private static synchronized int nextThreadNum() {
return 0;
}
// 修饰代码块
private Object lockObj = new Object();
private int nextThreadNum() {
synchronized(lockObj) {
return 0;
}
}
synchronized语义
- synchronized可以保证方法或代码块在运行时,同一时刻只有一个线程可以进入到临界区,同时它还可以保证共享变量的内存可见性;
- 每个对象都拥有自己的监视锁Monitor对象,java通过Monitor对象实现synchronized语义,保证了在同一时刻只有一个线程能访问共享资源。
Monitor对象
- Monitor详细介绍
- Monitor其实是一种同步工具,也可以说是一种同步机制
- 提供线程进入临界区的许可;
- 也提供了singal机制,允许正持有“许可”的线程暂时放弃“许可”,等待某个条件;并且条件成立后,拥有“许可”的线程可“通知”等待条件的线程重新获取“许可”
锁
synchronized修饰的对象会成为锁对象,那么锁信息和锁状态是如何存储的,synchronized如何工作的呢?
锁存储
任何一个对象都有可能被synchronized修饰,那么对象数据中必定需要预留空间存储其锁信息;(Mark Word)
- 对象在内存中的存储布局
- 对象头(Header)
- Mark Word
- 类元信息
- 实例数据(Instance Data)
- 对齐填充(Padding)
- 对象头(Header)
- Mark word 记录了对象和锁有关的信息
- 32bit 的Mark word 不同锁状态的存储内容
锁状态 | 锁信息 30bit | 锁标志 2bit |
---|---|---|
无锁 | HashCode(25bit),分代年龄(4bit), 偏向锁(1bit) | 01 |
偏向锁 | 线程ID(23bit),Epoch(2bit),分代年龄(4bit), 偏向锁(1bit) | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 |
重量级锁 | 指向重量级锁的指针 | 10 |
GC标记 | 空 | 11 |
- 64bit 的Mark word 不同锁状态的存储内容
锁状态 | 锁信息 62bit | 锁标志 2bit |
---|---|---|
无锁 | unused(25bit),HashCode(31bit),unused(1bit),分代年龄(4bit), 偏向锁(1bit) | 01 |
偏向锁 | 线程ID(54bit),Epoch(2bit),unused(1bit),分代年龄(4bit), 偏向锁(1bit) | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 |
重量级锁 | 指向重量级锁的指针 | 10 |
GC标记 | 空 | 11 |
为什么任何对象都可以实现锁
- 1 Java 中的每个对象都派生自 Object 类;
- 2 线程在获取锁的时候,实际上就是获得一个监视器对象(monitor),而所有对象都有一个monitor对象与之对应;
synchronized 锁的升级
- JDK1.5 都是重量级锁,1.6开始引入了偏向锁和轻量级锁
偏向锁的基本原理
- 通过Mark word存储当前拥有偏向锁的线程,使得该线程进入、退出同步代码块时,无需额外的加锁和释放锁。
偏向锁的获取与撤销
- 1 判断锁标志(01 则进入偏向锁获取)
- 2 判断偏向锁标志(0:无锁,1:可能Thread为空、也可能是其他Thread、也有可能是当前线程)
- 3 检查ThreadId是否当前线程
- 4 CAS 获取偏向锁
- 5 检查原持有偏向锁线程执行状态(需要等待原持有偏向锁线程进入全局安全点)
- 注:当只有单线程访问没有出现同步块竞争时,偏向锁是不需要解锁的
JVM设置偏向锁参数
- jdk1.6之后默认开启
- 参数开启方式:-XX:+UseBiasedLocking,启动默认五秒之后生效
- 额外配置可立即生效,配置延迟时间为0:-XX:BiasedLockingStartupDelay=0
- 关闭偏向锁配置:-XX:-UseBiasedLocking
轻量级锁的基本原理
- 当处于偏向锁状态的同步对象产生第二个线程竞争时,偏向锁将升级为轻量级锁;
- 持有偏向锁的线程将分配栈空间存储锁记录,同步对象的Mark word也CAS为栈中锁记录地址;
- 进入竞争的第二个线程也分配锁记录的栈空间,将进入自旋状态CAS替换同步对象的Mark word(将Mark word替换成自己的栈中锁记录地址)
- 栈空间锁记录存储同步对象的Mark word,锁记录owner指向同步对象
- 默认情况下自旋的次数是 10 次,可进行自行设置次数 -XX:PreBlockSpin=n
轻量级锁的加锁和解锁
JVM设置轻量级锁参数
- JDK1.6:-XX:+UseSpinning 开启自旋锁;-XX:PreBlockSpin参数来设置自旋锁等待的次数
- JDK1.7以后:自旋锁的参数被取消,自旋锁总是会执行,自旋锁次数也由虚拟机自动调整
重量级锁的基本原理
- 当轻量级锁膨胀到重量级锁之后,线程只能被阻塞等待被唤醒;
- 升级为重量级锁时,线程访问同步块是获取monitor对象和释放monitor对象的过程
- 前面说到过,所有对象都拥有自己的monitor对象(监视器),JVM提供的同步机制
- Monitor的线程阻塞等待机制是通过操作系统的mutex互斥锁实现的
总结
synchronized 是可重入锁
- 因为获取锁就是获取monitor对象,monitor对象内部维护线程重入次数,即重复获取同一个monitor对象无需重复繁重的加锁过程
synchronized 是非公平锁
- 无论锁处于各种状态,线程尝试获取锁时,并不会先判断进入阻塞队列,而是直接尝试获取锁,不满足先到先得,所以 synchronized 是非公平锁
关键词列表
- 使用方式
- synchronized 语义
- markword
- 锁升级
- monitor
- 1.6对锁优化
引入自旋锁,自适应自旋锁,偏向锁,锁消除,锁粗化,锁膨胀