一、轻量级锁
在多线程条件下,虽然一个对象会有多个线程访问,但是他们访问的时间是错开的(没有竞争关系),那么可以使用轻量级锁来优化。
1.使用轻量级锁的目的
降低无实际竞争关系的情况下,直接使用重量级锁带来的性能消耗。
2.轻量级锁的使用
轻量级锁对使用者是透明的,语法仍然是synchronized.
假设有两个方法同步块,对同一个对象加锁。
static final Object object = new Object();
public static void method1(){
synchronized(obj){
//同步块A
method2();
}
}
public static void method2(){
synchronized(obj){
//同步块B
}
}
-
创建锁记录对象(Lock Record),每个线程的栈帧都会包含锁记录结构,内部可以存储锁定对象的Mark Word 以及对象的指针。
-
让锁记录中的object reference指向锁对象,并尝试用cas替换object中的Mark Word,将Mark Word的值与Lock Record中的地址互换。
- 如果cas成功,那么对象头的Mark Word变成了lock record,锁标记从01(无锁状态)变成了00(轻量级锁),此时该线程加锁成功。
二、锁膨胀
问:上述执行轻量级锁的过程中,如果cas失败了,怎么办?
- 如果cas失败,说明存在竞争关系,需要升级为重量级锁,也称为锁膨胀。
锁膨胀的过程 - 当Thread-0在执行同步代码块的过程中,Thread-1也来使用轻量级锁的方式去获取锁,发现对象头中的Mark Word锁标记已经从01变成了00,Thread-1加轻量级锁失败,进入锁膨胀流程。
- 为object申请Monitor锁,让Object指向Monitor地址,锁标记从00变成10。
- 然后自己进入Monitor的阻塞队列entryList中,变成BLOCKED状态。
-
当Thread-0执行完同步代码块后,释放轻量级锁的时候发现Mark Word中的地址已经发生改变,解锁失败,将进入Monitor锁的解锁流程。
三、自旋
如果持有锁的线程很快就能将锁释放,那么其余线程就不需要进入EntryList阻塞队列中等待(内核态与用户态之间的切换进入阻塞状态)。它们只需要短时间的等待(自旋),等待持有锁的线程释放锁后,即可不用进入阻塞队列直接获取锁。
- 自旋也会消耗cpu的资源,如果自旋执行时间太长,会有大量的线程处于自旋状态而占用cpu资源,进而会影响整体的性能。那么如何去选择自旋的执行时间呢?
JVM默认的限定次数是10次,超过线程也会进入EntryList阻塞队列中等待。
四、锁消除
static int x = 0;
public void methodA(){
x++;
};
public void methodB(){
Object obj = new Object();
synchronized(obj){
x++;
}
}
上述代码中methodA()和methodB()方法不同点是methodB()方法对局部变量对象加锁去执行++操作,理论上synchronized会影响性能,降低代码的执行效率。但是通过Benchmark对两个方法进行测试,发现两个方法的执行时间几乎没有差别。
为什么:
JVM中重要的核心模块之一 JIT即时编译器,它可以对字节码文件进一步优化,它会发现上述代码中的局部变量根本不会逃离方法的作用范围,就意味着这个对象不可能被共享,那么methodB()中的synchronized关键字也就显得没有意义了,JIT即使编译器就会将无意义的代码优化掉,也就是锁消除。
- 关闭锁消除优化
-XX:-EliminateLocks
五、偏向锁
顾名思义,他会偏向第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在别的线程来使用的情况,就会给线程加一个偏向锁。锁标记为1,没有偏向锁则为0。
如果在执行过程中,遇到了其他线程抢占锁,则会升级为轻量级锁。
JVM默认启用偏向锁,在竞争激烈的场合,偏向锁会增加系统负担。
关闭偏向锁
-XX:-UseBiasedLocking
JVM启用偏向锁时,默认会有4s的延迟。原因在于,系统刚启动时,一般数据的竞争是比较激烈的,此时启用偏向锁会降低性能。
修改偏向锁延迟时间
-XX:BiasedLockingStartupDelay=0