为了唤醒大家的记忆,介绍synchronized原理之前,先给一个大家都很熟悉的使用场景: 单例模式(double check)。
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton() {
}
/**
* 获取实例
*
* @return
*/
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
1. 锁机制的两种特性
- 互斥性:即在任何时刻都只有一个线程持有某个对象锁,通过这种特性来实现多线程中对共享数据的协调访问机制(协议),即在同一时刻只有一个线程访问锁内的代码,互斥性我们也往往称为操作的原子性。
- 可见性:必须保证在锁被释放之前对共享数据所做的修改,对于随后获得该锁的线程是可见的,即在获得锁时应获得最新共享数据,否则就会出现数据不一致进而引发很多严重的问题。
2. synchronized的三种应用方式
2.1 同步实例方法
源码
public synchronized void synMethod() {
System.out.println("======同步实例方法======");
}
字节码
当前方式是对实例方法加锁,即会直接锁当前 实 例 对 象 \color{red}实例对象 实例对象。可以通过过字节码看到,此时会在方法flags中添加 ACC_SYNCHRONIZED 标识。
2.2 同步静态方法
源码
public static synchronized void synStaticMethod() {
System.out.println("======同步静态方法======");
}
字节码
当前方式是对静态方法加锁,因此锁住的是 类 对 象 \color{red}类对象 类对象。和同步实例方法一样,也会在方法flags中添加 ACC_SYNCHRONIZED 关键标识。
2.3 同步代码块
源码
public void synCodeBlock() {
synchronized (this) {
System.out.println("======同步(实例对象)代码块======");
}
}
public static void synCodeBlock2() {
synchronized (SynchronizeDemo.class) {
System.out.println("======同步(类对象)代码块======");
}
字节码
synchronized(instance|class) {},代码块是通过对实例对象或者类对象加锁实现的,锁的范围即给定的锁对象。可以从字节码看到在同步块的入口和出口分别增加 monitorenter ,monitorexit指令来加锁及释放锁。
在 Java 中,每个对象都会有一个Monitor 对象即监视器。某一线程获取某个对象锁的时候,先判断monitor 的计数器是否为0,如果是0表示没有线程占有,则该线程占有这个对象锁,并将这个对象的monitor+1;如果不为0,表示这个锁已经被其他线程占有,则该线程判断是否是当前线程占有,若是则monitor+1,否则线程等待。当线程释放释放锁的时候,monitor-1; 同一线程可以对同一对象进行多次加锁,即重入性。每一次加锁必然对应一次释放锁,指导monitor的值为0,表示锁彻底释放,则可以进入新一轮所竞争。
3. JVM的锁优化
3.1 基本概念
在Hotspot虚拟机中,对象在内存中的布局分为三块区域:对象头(Mark Word、Class Metadata Address)、实例数据和对齐填充;Java对象头是实现synchronized的锁对象的基础。一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键。
对象头的结构如下:
Mark World 结构:
Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
3.2 锁优化
jdk1.6以后对synchronized的锁进行了优化,引入了偏向锁、轻量级锁,锁的级别从低到高逐步升级: 无锁->偏向锁->轻量级锁->重量级锁。
- 无锁状态:没有加锁
- 偏向锁:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成01(表示当前是偏向锁)。如果没有设置,则使用CAS竞争锁。如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。竞争不激烈的时候适用。
- 轻量级锁:是用于线程有交替执行的情况且互斥性不是很强,CAS失败,转态00
- 重量级锁:重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。强互斥的时候适用,状态为10,等待时间长。
- 自旋锁:竞争失败的时候,不是马上转化级别,而是执行几次空循环,通常是5 10 。还有一种适应性自旋,是赋予了自旋一种学习能力,它并不固定自旋10次一下。他可以根据它前面线程的自旋情况,从而调整它的自旋,甚至是不经过自旋而直接挂起。
- 锁消除:JIT在编译的时候吧不必要的锁去掉,比如循环中调用StringBuffer的append方法。
- 如果jvm检测到有一串零碎的操作都对同一个对象加锁,将会把锁粗化到整个操作外部,如循环体。