synchronized也叫同步监视器,同步监视器的作用:阻止多个线程对同一个共享资源进行并发访问,通常推荐使用可能被并发访问的共享资源充当同步监视器。
synchronized同步监视器是借用jvm调用操作系统的互斥量(mutex)实现的。在JDK1.6之前,synchronized同步都是调用操作系统函数实现的,JDK1.6之后对synchronized进行了优化,于是就出现了偏向锁、轻量级锁、重量级锁的概念。锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking来禁用偏向锁。
加锁和解锁过程中,会多次涉及CAS,我们先需要知道CAS是什么?CAS即Compare-and-Swap,即比较并替换,也有叫做Compare-and-Set的,比较并设置。比较:读取到了一个值A,在将其更新为B之前,检查原值是否仍为A(未被其他线程改动);设置:如果是,将A更新为B,结束;如果不是,则什么都不做。
safepoint这个词我们在GC中经常会提到,简单来说就是其代表了一个状态,在该状态下所有线程都是暂停的。在GC整体流程中,第一步是雷打不动的根节点枚举。在目前所有的收集器中,根节点枚举都必须要暂停所有用户线程,原因是如果该过程中根节点在不断的变化,将无法保证后续可达性分析的准确性。
在synchronized的偏向锁机制中同样存在需要与Safe Point(安全点)相配合的操作,即偏向锁的撤销。与GC Safe Point(GC安全点)最大的区别在于,Biased Safe Point(偏向锁安全点)是一个“局部”安全点,即其并不需要暂停所有的用户线程,而只需暂停偏向线程即可。
synchronized中锁升级的过程如下:锁升级:偏向锁 → 轻量级锁 → 重量级锁。
一个锁只能按照偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不允许降级。线程T1第一次执行完同步代码块后,当线程T2尝试获取锁的时候,发现是偏向锁,会判断线程T1是否仍然存活。如果线程T1仍然存活,会在线程到达全局安全点将线程T1暂停,此时偏向锁升级为轻量级锁,之后线程T1继续执行,线程T2自旋;如果判断结果是线程T1不存在了,则线程T2持有此偏向锁,锁不升级。
在JVM中,对象在内存中的存储布局包含三个部分:Java对象头、实例数据、对齐填充。
对于普通对象而言,其对象头中有两类信息:mark word和类型指针。另外对于数组而言还会有一份记录数组长度的数据。类型指针是指向该对象所属类对象的指针,mark word用于存储对象的HashCode、GC分代年龄、锁状态等信息。
存放类实例的属性数据信息,包括父类的属性信息。
由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。对齐填充会将对象的字节码大小填充为最接近的8的整数倍。
可以看到锁信息也是存在于对象的mark word中的。当对象状态为偏向锁(biasable)时,mark word存储的是偏向的线程ID;当状态为轻量级锁(lightweight locked)时,mark word存储的是指向线程栈中Lock Record的指针;当状态为重量级锁(inflated)时,为指向堆中的monitor对象的指针。
我们可以借助JOL来分析Java的对象存储布局。后续synchronized锁膨胀时我们也是通过JOL工具包查看对象MarkWord中的锁状态变化。
org.openjdk.jol
jol-core
0.8
测试代码 我们看看JavaObject的对象存储布局 |
|
---|---|
三、锁的分类synchronized锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。但是锁只有三种偏向锁、轻量级锁和重量级锁。 (1)偏向锁在锁不存在多线程竞争情况下,为了减小线程获取锁的代价而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程再进入同步块时只需简单判断下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
|
|
(2)轻量级锁轻量级锁(Lightweight Lock)是JDK 1.6 时加人的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就被称为“重量级”锁。不过,需要强调一点,轻量级锁并不是用来代替重量级锁的,它设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。 在无竞争的情况下,轻量级锁使用CAS操作来实现锁的获取和释放,避免了线程的阻塞和唤醒,从而提高了并发性能 (i)轻量级锁的获取过程线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。 轻量级锁也会有锁重入的情况,如果线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针失败时,会先判断对象头中的Mark Word是否已经指向当前线程锁记录的指针,如果是,说明是锁重入,这时线程会新创建一个栈帧,对象头中的Mark Word设置为null,表示重入计数器。object reference(也叫owner指针)指向对象的对象头中的Mark Word。
加锁成功 加锁失败 (ii)轻量级锁的释放过程 |
|
轻量级锁什么时候会解锁失败呢?在发生锁竞争时并且占用锁的线程未释放,这时(自旋默认了10次还是未获取到锁)竞争锁的线程就会将Mark Word修改为重量级锁,并且将自己阻塞在该锁的monitor对象(操作系统层面对象)上。之后占用锁的线程将栈帧中的Mark Word进行CAS替换回对象头的Mark Word的时候,发现有其它线程竞争该锁(已经由竞争锁的线程更改了锁状态),然后它释放锁并且唤醒在等待的线程,后续的线程操作就全部都是重量级锁了。 轻量级锁重入的释放过程:判断线程栈帧中Displaced Mark Word对象是否为null,是则说明是重入锁的释放,只需要将object reference(也叫owner指针)指向null即可。
(3)重量级锁重量级锁也就是普通的悲观锁了,也就是竞争锁失败会阻塞等待唤醒再次竞争那种。重量级锁是借助操作系统来完成的。java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。所以重量级锁的性能会比较差。
|
|
四、锁的膨胀过程(1)各种锁存在的情形
|
|
(2)jvm的延迟偏向机制JVM默认开启了偏向锁延迟,因为在JVM虚拟机启动的时候,初始化虚拟机方法中会调用synchronized()方法,而且jvm认为这些初始化方法中的synchronized()方法会被多个线程调用,存在资源。如果线程之间存在竞争的话,就需要频繁的去暂停拥有偏向锁的线程然后检查状态, 决定是否重新偏向还是升级为轻量级别锁,性能就会大打折扣了,如果事先能够知道可能会存在竞争那么可以选择关掉偏向锁如果一开始就开始偏向锁的话, 所以jvm开启了延迟偏向,确保了虚拟机初始化时不会因为频繁的锁重偏向和锁升级造成性能损耗。 (关闭延迟偏向:-XX:BiasedLockingStartupDelay=0) 开启延迟偏向时锁状态 |
现在通过-XX:BiasedLockingStartupDelay=0指令把jvm的偏向锁延迟关了,再看看锁的状态
偏向锁存在的情形是只有一个线程持有锁。第一种情况是对象第一次加锁,是无锁状态(初始状态),只需CAS设置偏向锁指向自己。第二种情况是对象是偏向锁,但是偏向的那个线程不存在了,直接CAS设置偏向锁指向自己线程。
public static void main(String[] args) throws Exception{
JavaObject object = new JavaObject();
out.println("before lock +++++++++++");
out.println(ClassLayout.parseInstance(object).toPrintable());
Thread t1 = new Thread(() -> {
synchronized (object) {
out.println("bias " + Thread.currentThread().getName() + " locking +++++++++++");
out.println(ClassLayout.parseInstance(object).toPrintable());
}
}, "t1");
t1.start();
t1.join();
Thread t2 = new Thread(() -> {
synchronized (object){
out.println(" bias ? lightWeight "+ Thread.currentThread().getName()+ " locking +++++++++++");
out.println(ClassLayout.parseInstance(object).toPrintable());
}
},"t2");
Thread.sleep(1000);
while (true){
//t1死亡
if (!t1.isAlive()){
t2.start();
break;
}
}
t2.join();
out.println("after Lock +++++++++++");
out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
线程t1在对象无锁状态下对对象object加锁,线程t2在线程t1死亡后对对象object加锁,这两种情况都是偏向锁。
偏向锁解锁同步块后,不会释放锁。
偏向锁升级为轻量级锁是存在多个线程交替执行,但是不存在资源竞争。
public static void main(String[] args) throws Exception{
JavaObject object = new JavaObject();
out.println("before lock +++++++++++");
out.println(ClassLayout.parseInstance(object).toPrintable());
Thread t1 = new Thread(() -> {
synchronized (object) {
out.println(Thread.currentThread().getName() + " locking +++++++++++" + ClassLayout.parseInstance(object).toPrintable());
}
try {
//t1退出同步块后继续执行
Thread.sleep(2000);
out.println(Thread.currentThread().getName() + " running +++++++++++");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "t1");
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(() -> {
synchronized (object){
out.println(Thread.currentThread().getName() + " locking +++++++++++" + ClassLayout.parseInstance(object).toPrintable());
}
},"t2");
t2.start();
t2.join();
out.println("after Lock +++++++++++");
out.println(ClassLayout.parseInstance(object).toPrintable());
}
可以通过命令java -XX:+PrintFlagsFinal 来看JVM中的默认配置。
BiasedLockingBulkRebiasThreshold:偏向锁批量重偏向的默认阀值为20次。BiasedLockingBulkRevokeThreshold:偏向锁批量撤销的默认阀值为40次。BiasedLockingDecayTime:距上次批量重偏向25秒内,撤销计数达到40,就会发生批量撤销。每隔(>=)25秒,会重置在[20, 40)内的计数,这意味着可以发生多次批量重偏向。
当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁。这个过程是要消耗一定的成本的,所以如果说运行时的场景本身存在多线程竞争的,那偏向锁的存在不仅不能提高性能,而且会导致性能下降。因此,JVM中增加了一种批量重偏向/撤销的机制。
注意:
1)批量重偏向和批量撤销是针对类(class)的优化,和对象无关。
2)偏向锁重偏向一次之后不可再次重偏向。
3)当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利
以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id 改成当前线程Id。
当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。
1)首先引入一个概念epoch,其本质是一个时间戳,代表了偏向锁的有效性,epoch存储在可偏向对象的MarkWord中。除了对象中的epoch,对象所属的类class信息中,也会保存一个epoch值。
2)每当遇到一个全局安全点时,比如要对class A进行批量再偏向,则首先对 class A中保存的epoch进行增加操作,得到一个新的epoch_new。
3)然后扫描所有持有 class A 实例的线程栈,根据线程栈的信息判断出该线程是否锁定了该对象,仅将epoch_new的值赋给被锁定的对象中,也就是现在偏向锁还在被使用的对象才会被赋值epoch_new。
4)退出安全点后,当有线程需要尝试获取偏向锁时,直接检查 class A 中存储的 epoch 值是否与目标对象中存储的 epoch 值相等, 如果不相等,则说明该对象的偏向锁已经无效了(因为(3)步骤里面已经说了只有偏向锁还在被使用的对象才会有epoch_new,这里不相等的原因是class A里面的epoch值是epoch_new,而当前对象的epoch里面的值还是epoch),此时竞争线程可以尝试对此对象重新进行偏向操作。
批量重偏向(bulk rebias)
/**
* 批量重偏向
* 当一个线程t1运行结束后,所有的对象都偏向t1。
* 线程t2只对前30个对象进行了同步,0-18的对象会由偏向锁(101)升级为轻量级锁(00),19-29的对象由于撤销次数达到20,触发批量重偏向,偏向线程t2。
* t2结束后,0-18的对象由轻量级锁释放后变成了无锁,19-29的对象偏向t2,30-49的对象还是偏向t1。
*
* 总结:批量重偏向会以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,
* 当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向于当前线程。
*/
public static void batchReBias() throws Exception{
for (int i = 0; i < 40; i++) {
D d = new D();
objects.add(d);
}
t1 = new Thread(() -> {
for (int i = 0; i < objects.size(); i++) {
synchronized (objects.get(i)) {// 50个对象全部偏向t1 101
}
}
//唤醒t2
LockSupport.unpark(t2);
});
t2 = new Thread(() -> {
//阻塞当前线程
LockSupport.park();
//循环了30次
for (int i = 0; i < 30; i++) {
Object a = objects.get(i);
synchronized (a) {
//分别打印第19次和第20次偏向锁重偏向结果
if (i == 18 || i == 19) {
System.out.println("第" + (i + 1) + "次偏向结果");
// 第19次轻量级锁00,第20次偏向锁101,偏向t2
System.out.println((ClassLayout.parseInstance(a).toPrintable()));
}
}
}
});
t1.start();
t2.start();
t2.join();
System.out.println("打印list中第11个对象的对象头:");
// 01 无锁,轻量级锁退出同步块后变成无锁状态,且不可偏向
System.out.println((ClassLayout.parseInstance(objects.get(10)).toPrintable()));
System.out.println("打印list中第26个对象的对象头:");
// 101 偏向t2,偏向锁退出同步块后不会释放锁
System.out.println((ClassLayout.parseInstance(objects.get(25)).toPrintable()));
System.out.println("打印list中第35个对象的对象头:");
// 101 偏向t1
System.out.println((ClassLayout.parseInstance(objects.get(34)).toPrintable()));
}
批量撤销偏向(bulk revoke)
/**
* 批量撤销偏向
* t1执行完后,0-39的对象偏向t1。
* t2执行完后,0-18的对象为轻量级锁,19-39偏向t2。
* t3执行时由于之前执行过批量重偏向了,所以这里后20个对象(20-39)会升级为轻量级锁。达到批量撤销的阈值
* t4休眠前对象45为匿名偏向状态,t4休眠后,由于触发了批量撤销,所以锁状态变为轻量级锁,所以批量撤销会把正在执行同步的对象
* 的锁状态由偏向锁变为轻量级锁,而不在执行同步的对象的锁状态不会改变(如对象46)。
*
* 总结:
* 批量重偏向和批量撤销是针对类的优化,和对象无关。偏向锁重偏向一次之后不可再次重偏向。当某个类
* 已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利。
*/
public static void batchRevokeBias() throws Exception{
for (int i = 0; i < 60; i++) {
D d = new D();
objects.add(d);
}
t1 = new Thread(() -> {
//必须大于或等于40
for (int i = 0; i < 40; i++) {
D d = objects.get(i);
synchronized (d) {
}
}
LockSupport.unpark(t2);
}, "t1");
t2 = new Thread(() -> {
LockSupport.park();
//必须大于或等于40
for (int i = 0; i < 40; i++) {
D d = objects.get(i);
synchronized (d) {
//0-19 00 偏向锁撤销20次
//19-39 101 偏向t2
}
}
}, "t2");
t3 = new Thread(() -> {
LockSupport.park();
System.out.println("t3");
//批量撤销的阈值40
for (int i = 20; i < 40; i++) {
D d = objects.get(i);
synchronized (d) {
//后面20个对象由偏向锁变成轻量级锁,偏向锁撤销20次,与t2线程撤销的20次相加一共40次,触发批量撤销的阈值
if(i == 20 || i == 39){
out.println("t3 " + i + " : "+ ClassLayout.parseInstance(objects.get(i)).toPrintable()); // 00
}
}
}
}, "t3");
t4 = new Thread(() -> {
synchronized (objects.get(44)) {
out.println("t4 begin (45) " + ClassLayout.parseInstance(objects.get(44)).toPrintable()); // 101
LockSupport.unpark(t3);
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//t3触发批量撤销,还在同步块的class都升级为轻量级锁
out.println("t4 end (45) " + ClassLayout.parseInstance(objects.get(44)).toPrintable()); // 00
//不在同步块的d对象,匿名偏向
out.println("t4 end (46) " + ClassLayout.parseInstance(objects.get(45)).toPrintable()); // 101
}
}, "t4");
t4.start();
t1.start();
t2.start();
t3.start();
t3.join();
t4.join();
//撤销偏向
System.out.println(ClassLayout.parseInstance(new D()).toPrintable()); // 01
}
重量级锁存在的情形是多个线程存在资源竞争。
public static void main(String[] args) throws Exception{
JavaObject object = new JavaObject();
out.println("before lock +++++++++++");
out.println(ClassLayout.parseInstance(object).toPrintable());
Thread t1 = new Thread(() -> {
synchronized (object) {
out.println(Thread.currentThread().getName() + " locking +++++++++++" + ClassLayout.parseInstance(object).toPrintable());
}
try {
//t1退出同步块后继续执行
Thread.sleep(2000);
out.println(Thread.currentThread().getName() + " running +++++++++++");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "t1");
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(() -> {
synchronized (object){
out.println(Thread.currentThread().getName()+ " locking +++++++++++" + ClassLayout.parseInstance(object).toPrintable());
try {
//确保t2,t3会存在资源竞争
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"t2");
t2.start();
Thread t3 = new Thread(() -> {
synchronized (object){
out.println(Thread.currentThread().getName()+ " locking +++++++++++" + ClassLayout.parseInstance(object).toPrintable());
}
},"t3");
t3.start();
//确保t2,t3,都执行完同步块,且已经销毁
while (true){
if(!t2.isAlive() && !t3.isAlive()){
out.println("after Lock +++++++++++");
out.println(ClassLayout.parseInstance(object).toPrintable());
break;
}
}
}
public class A {
int i=0;
boolean b = false;
long c = 0l;
// boolean flag =false;
public synchronized void parse(){
i++;
BiasDetails.countDownLatch.countDown();
}
public synchronized void biasParse(){
i++;
}
public void noLockParse(){
i++;
}
}
/**
* 无锁下的性能
* 执行三次的耗时:836ms 859ms 779ms
* @param a
*/
public static void noLock(A a){
long start = System.currentTimeMillis();
//调用无锁方法来计算1000000000L的++,对比无锁和偏向锁的性能
for(int i=0;i<1000000000L;i++){
a.noLockParse();
}
long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end - start));
}
/**
* 偏向锁下的性能
* 执行三次的耗时:18969ms 18699ms 18640ms
* @param a
*/
public static void bias(A a){
long start = System.currentTimeMillis();
//调用同步方法1000000000L 来计算1000000000L的++,对比偏向锁和轻量级锁的性能
for(int i=0;i<1000000000L;i++){
a.biasParse();
}
long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end - start));
}
/**
* CountDownLatch是一个同步工具类,用来协调多个线程之间的同步,用来作为线程间的通信而不是互斥作用。
* CountDownLatch中多个线程是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。
*/
static CountDownLatch countDownLatch = new CountDownLatch(1000000000);
……
/**
* 轻量级锁下的性能
* 执行三次的耗时:28313ms 28763ms 29495ms
* @param a
*/
public static void lightWeightLock(A a){
try {
long start = System.currentTimeMillis();
for (int i = 0; i < 2; i++) {
new Thread(() -> {
while (countDownLatch.getCount() > 0 )
a.parse();
}).start();
}
//线程调用countdownLatch.await(),通过其他线程调用countdownLatch.countDown()减少计数器,直到减少到0,被await()挂起的线程恢复执行。
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end - start));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
通过上述简单的代码实验,发现锁之间的性能差别还是很大的,说明synchronized的优化是必要的。
锁升级过程:偏向锁 → 轻量级锁 → 重量级锁
一个锁只能按照偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不允许降级。