Synchronized 实现原理与应用
在多线程并发编程中synchronized一直都是比较重要的角色,很多人都把他称之为重量级锁。但是随着Java SE 1.6对synchronized进行了各种优化以后,有些情况下它就不是那么重了。
以下为大家介绍Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。
Java中的每个对象都可以作为锁。具体表现为以下几种形式:
- 对于普通同步方法,锁是当前对象。
- 对于静态同步方法,锁是当前类的Class对象。
- 对于同步方法块,锁是Synchonized括号里面配置的对象
当一个线程视图去访问同步代码块时,它首先必须获得锁,退出或者抛出异常时必须释放锁。
锁到底存在哪里,锁里面存储了什么对象呢
从JVM规范中可以看到Synchronized在JVM里的实现原理,JVM基于进入和退出Monitor对象来进行实现方法同步和代码块同步,但是两者的实现细节不是一样的。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种实现方式实现的,细节在JVM规范里并没有详细说明。但是方法的同步同样可以使用这两个指令来实现。
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令的时候,将会尝试获取对象所对应的monitor的所有权,即尝试获取对象的锁。
一个简单的Java代码:
多线程操作一个变量进行累加操作
/**
* Created by gongyan on 2018/4/16.
*/
public class SynchronizedClass {
int i = 0;
public static void main(String[] args) {
new SynchronizedClass().addNumber();
}
public void addNumber() {
synchronized (SynchronizedClass.class) {
i++;
}
}
}
我们来看一下它解析完是什么样子的呢?
使用javap -c xxx 去进行反编译
public void addNumber();
Code:
0: ldc_w #3 // class com/wsqandgy/SynchronizedClass
3: dup
4: astore_1
5: monitorenter
6: aload_0
7: dup
8: getfield #2 // Field i:I
11: iconst_1
12: iadd
13: putfield #2 // Field i:I
16: aload_1
17: monitorexit
18: goto 26
21: astore_2
22: aload_1
23: monitorexit
24: aload_2
25: athrow
26: return
我们可以看到在进行操作的时候会进行设置 5: monitorenter
进入监听器 在操作完以后 17: monitorexit
异常的时候会进行一次 23: monitorexit
Java 头对象
Synchronized用的锁是存在Java对象头里面的。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用两个字宽存储对象头。在32位虚拟机中,1字宽等于4字节,32bit。
对象头的长度Java对象头里面的Mark Word里面默认存储对象的HashCode、分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构如下所示:
image.png在运行期间,Mark Word里面存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据:
image.png64位虚拟机下,Mark Word是64bit大小的,存储结构如下:
image.png
锁的升级与对比
Java SE 1.6为了减少获得锁和释放锁带来的性能损耗,引入了偏向锁
和轻量级锁
,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争状态逐渐升级。锁可以升级但是不能降级,意味着偏向锁升级为轻量级锁以后不能降级为偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
偏向锁
HotSpot的作者经过研究发现,大多时候,锁不仅不存在多线程的竞争,并且总是由同一个线程多次获得,为了让线程获得锁的代价更低所以引入了偏向锁。当一个线程访问同步块并获取锁的时候,会在对象头和栈针中的锁记录里面存储偏向锁的线程ID,以后该线程在进入和退出同步块的时候不需要进行CAS操作来进行加锁和解锁,只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获取了锁。如果测试失败了的话,则需要再测试一下Mark Word中偏向锁的标识是不是被设置成了1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试去竞争偏向锁的时候,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。他会首先暂停拥有偏向锁的线程,然后去检查偏向锁的线程是否存活,如果线程不在活动状态,则将对象头设置为无锁状态;如果线程仍然存活,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark word要么重新偏向其他的线程,要么恢复到无锁或者标记对象不适合偏向锁,最后唤醒暂停的线程。
下图展示了偏向锁初始化和撤销的流程:
偏向锁的获得和撤销流程偏向锁的关闭
偏向锁在Java 6和Java 7 是默认启用的,但是需要在启动后几秒钟之后才会被激活,如果有必要可以使用JVM参数来关闭延迟 :-XX:BiasedLockingStartupDelay=0。如果确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁。
Biased 偏向的,结果偏移的,有偏见的。意思为偏向于一个线程使用的锁,偏向锁
轻量级锁
- 轻量级锁加锁
线程在执行同步块之前,JVM会现在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方成为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则表示其他线程竞争所,当前线程便尝试使用自旋来获取锁。
- 轻量级锁解锁
轻量解锁时,会使用院子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
两个线程同事争夺锁,导致锁膨胀的流程图。
两个线程争夺锁流程因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁的状态了。当锁处于这个状态下,其他线程视图获取锁时候,都会被阻塞住,当持有锁的线程释放锁之后就会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之战。
网友评论