面试题:谈谈你对synchronized的理解
可以从synchronized的特性,实现原理,是否可重入,是否是公平锁,synchronized的优化,锁升级的过程,synchronized的使用方式来说。
简介
synchronized关键字是Java里面最基本的同步手段,它经过编译之后,会在同步块的前后分别生成monitorenter和monitorexit字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解锁的对象。
实现原理
在学习Java内存模型的时候,我们介绍过两个指令:lock和unlock
lock,锁定,作用于主内存的变量,它把主内存中的变量标识为一条线程独占状态。
unlock,解锁,作用于主内存的变量,它把锁定的变量释放出来,释放出来的变量才可以被其他线程锁定。
但是这两个指令并没有提供给用户直接使用,而是提供了两个更高层次的指令monitorenter和monitorexit来隐式地使用lock和unlock指令。
而 synchronized 就是使用 monitorenter 和 monitorexit 这两个指令来实现的。
根据JVM规范的要求,在执行monitorenter指令的时候,首先要去尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,就把锁的计数器加1,相应地,在执行monitorexit的时候会把计数器减1,当计数器减小为0时,锁就释放了。
Monitor原理
Monitor被翻译为监视器或管程
每个Java对象都可以关联一个Monitor对象,如果synchronized给对象上锁(重量级锁)之后,该对象头的Mark word 中就被设置指向Monitor对象的指针
Monitor结构如下:
1.刚开始Monitor中的Owner为null
2.当Thread2执行synchronized(obj)就会将Monitor的所有者Owner设置为Thread2,Monitor中只能有一个Owner
3.在Thread2上锁的过程中,如果Thread3,Thread4,Thread5也来执行synchronized(obj),就会进入EntryList 相当于阻塞队列 进入阻塞状态
4.Thread2执行完同步代码块中的内容后,然后唤醒EntryList中等待的线程来竞争锁,竞争锁时是非公平的(不按照阻塞队列的顺序获取锁)
5.图中thread0,Thread1是之前获取过锁,但是条件不满足进入WAITING状态的线程。
让我们来看看在字节码层面
public class SynchronizedTest {
public static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock){
counter++;
}
}
}
反编译后
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // Field lock:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter
6: getstatic #3 // Field counter:I
9: iconst_1
10: iadd
11: putstatic #3 // Field counter:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
我们可以看到,字节码层面synchronized是通过moniterenter和moniterexit实现的。
可重入锁还是非可重入锁
通过代码验证
public class SynchronizedTest {
public static synchronized void m1(){
m2();
System.out.println("第三次进入");
}
public static synchronized void m2(){
m3();
System.out.println("第二次进入");
}
public static synchronized void m3(){
System.out.println("synchronized是可重入锁");
}
public static void main(String[] args) {
m1();
}
}
通过输出结果可知synchronized是可重入锁。
synchronized的优化以及锁升级的过程
在jdk1.6之前,synchronizd主要的机制是使用操作系统级别的monitor对象锁,以下是ObjectMonitor.cpp 中定义的锁对象 waitSet为等待队列 EntryList为阻塞队列,可参考上面Monitor原理,性能比较低下,在JDK1.6之后,synchronized做出了大量优化,我们在下面给大家做出介绍。
1.偏向锁
JDK1.6中引入了偏向锁来做进一步的优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。在线上环境下,大多数时间只有一个线程在跑同步代码块,偏向锁就是进行优化了,没有底层的阻塞和线程上下文切换。
JVM默认延时4s自动开启偏向锁,可通过-XX:BiasedLockingStartupDelay=0取消延时
static final Object obj = new Object();
public static void m1(){
synchronized (obj){
m2();
}
}
public static void m2(){
synchronized (obj){
m3();
}
}
public static void m3(){
synchronized (obj){
}
}
这里也显示出了synchronized的可重入特性
一个对象创建时:
如果开启了偏向锁(默认开启),那么对象创建后,markword的值为0x05即最后三位为101
处于偏向锁的对象解锁后,线程id仍存储于对象头中。
2.轻量级锁
是指当锁是偏向锁时,被另外一个线程访问,偏向锁会升级为轻量级锁,这个线程会通过CAS自旋的方式尝试获取锁,不会阻塞,提高性能。
创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的MarkWord 对象头中有hashcode、Epoch 、ThreadID 、 分代年龄、 偏向状态0或者1 、元数据指针、锁记录等 01表示无锁
对象头信息
让锁记录中Object reference指向锁对象,并尝试使用CAS替换Object的Mark Word,将Mark Word的值存入锁记录中 00表示轻量级锁
如果CAS成功,对象头中就存储了锁记录地址和状态00,表示由该线程给对象加锁。
如果CAS失败了,有两种情况
1.如果是其他线程已经持有了该对象的轻量级锁,这时表明有竞争,进入锁膨胀过程
2.如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为锁重入的计数
当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入记数减1
当退出synchronized代码块时锁记录不为null,这时使用CAS将markWord的值恢复给对象头,如果成功,则表示解锁成功,如果失败,表示轻量级锁进行了锁膨胀或者已经升级为重量级锁,进入重量级锁解锁的流程。
3.重量级锁
如果尝试加轻量级锁的过程中,CAS无法操作成功,这是一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这是需要进行锁膨胀,将轻量级锁变为重量级锁。
当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁,
这时Thread-1加轻量级锁失败,进入锁膨胀过程
即为Object对象申请Monitor锁,让Object指向重量级锁地址
然后自己进入Monitor的EntryList BLOCKED
4.自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持有锁的线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
总结
(1)synchronized在编译时会在同步块前后生成monitorenter和monitorexit字节码指令;
(2)monitorenter和monitorexit字节码指令需要一个引用类型的参数,基本类型不可以哦;
(3)monitorenter和monitorexit字节码指令更底层是使用Java内存模型的lock和unlock指令;
(4)synchronized是可重入锁;
(5)synchronized是非公平锁;
(6)synchronized可以同时保证原子性、可见性、有序性;
(7)synchronized有三种状态:偏向锁、轻量级锁、重量级锁;