重量级锁synchronized关键字
一、synchronized的作用
1.可见性:synchronized关键字在线程解锁前必须把变量同步回主内存,在线程加锁前必须从主内存中重新获取最新的值。
2.原子性:synchronized关键字保证每一个时刻只能一个线程持有该同步块,来保证其原子性。
3.有序性:synchronized通过一个时刻只允许一个线程执行同步块,即使指令进行重排序仍旧不影响结果。它不会禁止指令重排序,为保证指令重排序最好的方法是禁止处理器优化和指令重排序。
4.可重入行:synchronized和ReentrantLock都是可重入锁,当一个线程试图操作一个由其它线程持有的对象锁的临界资源时,会处于阻塞状态,但是同一个线程可再次持有,这种情况属于重入锁,即可重复申请锁。
二、锁原理
jvm中synchronized是通过监视器锁Monitor来实现代码的同步,同一时刻只允许一个线程调用synchronized关键字加锁的代码。同步代码块是使用monitorenter和monitorexit指令来实现,同步方法通过标记ACC_SYNCHRONIZED来隐式实现。同步代码块调用时会判断代码块是否有线程在进行使用,如果有且不是使用线程,则加入对象头的等待队列,否则将正在执行的代码数量加1,退出后则减1。当线程计数为0时则允许其它线程调用。同步方法调用时是通过同步判断标记来进行调用。
如下代码是一个未使用synchronized修饰的代码
public class SynchronizedTest {
public void myPrint(){
for(int i=0; i<100;i++){
System.out.print(i);
}
}
}
其反编译结果如下
如下代码是增加synchronized修饰的代码。
public class SynchronizedTest {
public void myPrint(){
Object lock = new Object();
for(int i=0; i<100;i++){
synchronized(lock){
System.out.print(i);
}
}
}
}
经过反编译javap -v xxx.class后如下
通过对比我们能发现反编译结果中添加了monitorenter和monitorexit,代表进入Monitor监视器,和退出Monitor监视器。
public class SynchronizedTest {
public synchronized void myPrint(){
for(int i=0; i<100;i++){
System.out.print(i);
}
}
}
方法锁反编译后后效果
三、java对象头
HotSpot虚拟机将对象分头为两部分,一部分用于存储运行时数据,如哈希码(HashCode)、GC分代年龄(Generational GC Age)等,官方将其称为标记位“Mark Word",它是实现轻量级锁和偏向锁的关键。另一部分用于存储指向方法区对象的指针,如果是数组对象的化,还会有一个额外的部分用于存储数组长度。
四、锁优化
1、自旋锁和自适应锁
当虚拟机中有一个线程A去正在使用共享数据,这时另外一个线程B过来需要使用该共享数据。如果线程B发现共享资源已经被使用而直接放弃尝试获取锁且释放cpu资源,但线程A在该线程放弃持有锁时很快就释放了共享数据的锁,这时线程B又要去获取cpu资源然后获取共享数据,这个过程很浪费资源。对于这种情况我们只需要让线程B进行空循环(自旋),就可以不进行cpu资源的释放和重新占用,这项技术就是所谓的自旋锁。但是自旋次数太多又会浪费处理器资源就需要控制自旋次数,自旋次数默认位10,这时可以使用-XX:PreBlockSpin来更改自旋次数。JDK1.6引入自适应锁,这时自旋时间不再固定,而是通过前一次的自旋时间和锁的持有状态来决定,即自适应锁。
2、锁消除
虚拟机运行时对一些代码要求同步,但是被检测到不可能存在共享数据时对锁进行消除。判断依据逃逸分析。
3、锁粗化
在进行代码编写时,我们会遵循同步块范围控制的尽量小的原则,只在共享数据作用域进行同步。大部分情况下,这样的原则是对的,但是如果一些列操作都对同一对象反复加锁,甚至在循环体中进行加锁操作,这样即使没有线程竞争频繁的加锁和解锁也会损耗大量的性能,这时会将加锁动作进行粗化,例如上面第一段代码将锁放在循环体外执行。
4、轻量级锁
在代码进入同步块时,如果此时同步块没有被锁定(锁的标记位为01状态),虚拟机会在当前线程的栈帧中建立一个锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。然后尝试使用CAS操作尝试将对象的Mark Word进行更新,如果更新成功,那么这个线程就持有了这个对象的锁,并且对象Mark Word的锁标志位将转变位00.如果更新失败虚拟机首先会检查对象是否指向当前线程,如果是则直接进行更新,否则说名锁已被其它线程抢占则说明两个线程持有对象,将锁膨胀位重量级锁。
5、偏向锁
如果轻量级锁在无竞争的情况下使用CAS操作去使用的互斥量,那偏向锁就是无竞争的情况下将整个同步消除,连CAS操作也不做了。当锁对象第一次被线程获取的时候,虚拟机将这个对象头的标志位设为01,即偏向模式,同时使用CAS操作把取到的这个锁的ID记录在对象的Mark Word之中,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机不再进行任何同步操作。-XX:UseBiasedLocking可以禁止偏向锁来提升性能。