Synchronized含义
关键字 synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性
1
2
3
4
5
6
7
8
9
10
11public class Synchronized {
public static void main(String[] args) {
synchronized (Synchronized.class) {
}
method();
}
public static synchronized void method() {
}
}编译运行,然后使用命令:javap -v SynchronizedDemo.class
大致可以看出,对于上述代码中的同步块 的实现是通过monitorenter和monitorexit 指令,而同步方法是依靠方法修饰符上的ACC_SYNCHRONIZED 来完成的
上述的两种方式,无论采用的是哪一种方式,其本质是对一个对象的监视器(monitor) 进行获取,而这个获取过程是排他的,也就是说同一时刻只有一个线程获取到由synchronized所保护对象的监视器
synchronized 允许使用任何的一个对象作为同步的内容,因此任意一个对象都应该拥有自己的监视器(monitor),当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态
下图描述了对象、对象的监视器、同步队列和执行线程之间的关系:
从上图中我们可以看到,任意线程对 Object(Object 由 synchronized 保护)的访问,首先要获得 Object 的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问 Object 的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取
Java 虚拟机对 synchronized 的优化
synchronized 相对于 volatile 是重量了很多,因此在以前很让人诟病,但是从 JDK 1.6 版本以后为了减少获得锁和释放锁带来的性能消耗而引入了偏向锁和轻量级锁,以及锁的存储结构和升级过程
从 JDK 对synchronized的优化,可以看出 Java 虚拟机对锁优化所做出的努力,下边我们就分别学习一下什么是偏向锁、轻量级锁、重量级锁、自旋锁
在理解这四种锁之前,我们先看一下synchronized锁的存放位置,synchronized 用的锁是存在 Java 对象头里的 ,如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节,即 32bit,如下图:
- Java 对象头里的Mark Word里默认存储对象的 HashCode、分代年龄和锁标记位。32位 JVM 的Mark Word的默认存储结构如下图所示:
在 Java SE 1.6 中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率
下边分别研究一下这几个状态
偏向锁
偏向锁是一种针对加锁操作的优化手段,他的核心思想是:如果一个线程获得了锁,那么锁就进入了偏向模式。当这个线程再次请求锁时,无需再做任何同步操作,这样就节省了大量有关锁申请的操作,从而提高了程序的性能
偏向锁获取锁流程
- 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁,只需简单地测试一下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁
- 如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下 Mark Word 中偏向锁的标识是否设置成1(表示当前是偏向锁)
- 如果没有设置,则使用 CAS 竞争锁
- 具体流程图如下:
偏向锁升级为轻量锁
- 对于只有一个线程访问的同步资源场景,锁的竞争不是很激烈,这时候使用偏向锁是一种很好的选择,因为连续多次极有可能是同一个线程请求相同的锁
- 但是在锁竞争比较激烈的场景,最有可能的情况是每次不同的线程来请求相同的锁,这样的话偏向锁就会失效,倒不如不开启这种模式,幸运的是 Java 虚拟机提供了参数可以让我们有选择的设置是否开启偏向锁
- 如果偏向锁失败,虚拟机并不会立即挂起线程,而是使用轻量级锁进行操作
轻量级锁
- 如果偏向锁失败,虚拟机并不会立即挂起线程,而是使用轻量级锁进行操作。轻量级锁他只是简单的将对象头部作为指针,指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。如果线程获得轻量级锁成功,则可以顺利进入临界区。如果轻量级锁加锁失败,则表示其他线程抢先夺到锁,那么当前线程的轻量级锁就会膨胀为重量级锁
自旋锁
- 轻量级锁就会膨胀为重量级锁后,虚拟机为了避免线程真实的在操作系统层面挂起,虚拟机还会在做最后的努力–自旋锁
- 由于当前线程暂时无法获得锁,但是什么时候可以获得锁时一个未知数。也许在几个 CPU 时钟周期之后,就可以获得锁。如果是这样的话,直接把线程挂起肯定是一种得不偿失的选择,因此系统会在进行一次努力:他会假设在不就的将来,限额和从那个可以得到这把锁,因此虚拟机会让当前线程做几个空循环(这也就是自旋锁的意义),若经过几个空循环可以获取到锁则进入临界区,如果还是获取不到则系统会真正的挂起线程
- 那么为什么锁的升级无法逆向那?
- 这是因为,自旋锁无法预知到底会空循环几个时钟周期,并且会很消耗 CPU,为了避免这种无用的自旋操作,一旦锁升级为重量锁,就不会再恢复到轻量级锁,这也是为什么一旦升级无法降级的原因所在
三种锁的优缺点的对比
Java 虚拟机对锁优化所做的努力
- 从 Java 虚拟机在优化 synchronized 的时候引入了:偏向锁、轻量级锁、重量级锁以及自旋锁,都可以看出 Java 虚拟机通过各种方式,尽量减少获取所和释放锁所带来的性能消耗
- 但这还不全是 Java 虚拟机锁做的努力,另外还有:锁消除 、 CAS 等等,更重要的还有一个无锁的概念