高并发时,同步调用应该考虑到锁的性能消耗。能用无锁的数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能使用对象锁,就不用类锁。
锁的升级:
无锁 => 偏向锁 => 轻量级锁 => 重量级锁
偏向锁:当一段同步代码一直被同一个线程多次访问,那么该线程在后续的访问时会自动获取锁 。
偏向锁:
- 当同步代码首次被一个线程访问,那么就会在Mark Word记录该线程的ID,从无锁状态(001)变成偏向锁(101),当同步代码执行结束,该线程并不会释放锁。
- 当下一次同步代码被访问时,那么就会检测该线程ID与锁的Mark Word 中的线程ID是否是相同。
- 相同:则直接进入同步代码,因为之前没有释放锁
- 不同:表示发生了竞争,会尝试使用CAS来替换Mark Word里面的线程ID。竞争成功则会替换Mark Word 里面的线程ID,竞争失败可能会变成轻量级锁。
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放锁的。
偏向锁默认在程序启动4秒后才会开启,可以用下面参数更改时间
开启偏向锁(jdk6之后默认开启):-XX:+UseBiasedLocking
将偏向锁延迟时间由4000ms改为0:-XX:BiasedLockingStartupDelay=0
偏向锁的撤销:
撤销需要等待全局安全点,同时检查持有偏向锁的线程是否还在执行。
- 第一个线程正在执行synchronized方法(处于同步代码块),他还没有执行完,其他线程来争抢,该偏向锁会被取消并出现锁升级。此时轻量级锁由原持有偏向锁线程持有,继续执行其同步代码,而在竞争的会进入自旋等待获得该轻量锁。
- 第一个线程执行完synchronized方法(退出同部分代码块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向。
由于维护成本高,偏向锁在jdk15及之后被移除了。
轻量锁
轻量级锁是为了在线程近乎交替执行同步块时提高性能
在没有多线程的前提下,通过CAS减少重量级锁使用操作系统互斥产生的性能损耗。
轻量锁的加锁:JVM会为每个线程创建用于存储锁的记录空间,官方成为 Displaced Mark Word。若线程获得锁时发现是轻量锁,则会把锁的 Mark Word 复制到Displaced Mark Word 里面。
轻量锁的释放:当前线程会使用CAS操作将Displaced Mark Word里面的内容复制回Mark Word里面。
自旋抢占轻量锁,当自旋到达一定次数依然没有成功获取锁将升级为重量锁。
- jdk6之前:
- 默认10次自旋未成功升级锁 使用VM参数修改 -XX:PreBlockSpin=10
- 或者自旋线程超过了cpu核心数一半
- jdk6之后:自适应自旋锁。JVM底层优化,若线程上次自旋成功获取锁,那么下一次就会增加自旋的次数,以保证更大的可能性获取锁;若线程很少自旋成功获取锁,那么下次就会减少自旋的次数,避免cpu空转。
轻量锁与偏向锁的区别:
- 争夺轻量锁失败,自旋锁会尝试枪锁。
- 轻量锁每次执行完同步代码块都会释放锁,而偏向锁只有在发生竞争时才会释放锁。
重量锁
ObjectMonitor类 操作系统管程monitor
锁升级后hashcode去哪儿了?
偏向锁:Mark Word 存储的是偏向的线程ID
轻量锁:Mark Word 存储的是指向线程栈中Lock Record 的指针
重量锁:Mark Word 存储的是指向堆中的monitor对象的指针
无锁状态:Mark Word 可以存储 hash code值。当对象的hashCode()方法第一次被调用时,JVM会生成对应的hash code值并存储到Mark Word中。
对于偏向锁:在线程获取偏向锁时,会用Thread ID和epoch值覆盖掉hash code所在的位置。如果一个对象的hashCode() 方法已经被调用过一次之后,这个对象不能被设置偏向锁。因为可以的话,那么Mark Word中的hash code必然会被偏向线程id给覆盖。
对于轻量级锁:JVM会在当前线程的栈帧中创建Lock Record(锁记录空间),用于存储锁对象的Mark Word拷贝。所以轻量级锁与hash code 共存。释放锁后会将信息回写到对象头。
对于重量级锁:代表重量级锁的ObjectMonitor类里有字段记录非枷锁状态下Mark Word,锁释放后也会将信息写回到对象头。
- 在获取偏向锁之前调用 hashCode() ,会升级成轻量级锁。(也就是 空白Mark Word 先存储hashcode 后在存储线程ID)
- 在获取偏向锁之后调用 hashCode(),会升级成重量级锁。(也就是 空白Mark Word先存储线程ID后又想存储hashCode)
小总结
先自旋,不行再阻塞。
如果同步代码块执行时间过长,那么轻量级锁自旋带来的性能消耗就比使用重量级锁更加严重。
JIT编译器对锁的消除
锁消除
下面代码发生了锁消除,因为每个线程都自己new了一个对象作为自己的锁,这样是没有意义的(应是多个线程抢同一把锁),jit编译器会自己忽略它(逃逸分析)。
public class SynchronizedDemo1 {
static Object staticObject = new Object();
public void m1(){
Object o = new Object();
synchronized (o){
System.out.println(Thread.currentThread().getName()+"\t"+o.hashCode()+"\t"+staticObject.hashCode());
}
}
public static void main(String[] args) {
SynchronizedDemo1 synchronizedDemo1 = new SynchronizedDemo1();
for (int i = 0; i < 10; i++) {
new Thread(()->{
synchronizedDemo1.m1();
},String.valueOf(i)).start();
}
}
}
锁粗化
方法中前后相邻都是同一个锁对象,JIT会把几个synchronized合并成一个大块。
一次申请和释放锁,提高了性能。
new Thread(() -> {
synchronized (o) {
System.out.println(111);
}
synchronized (o) {
System.out.println(222);
}
synchronized (o) {
System.out.println(333);
}
synchronized (o) {
System.out.println(444);
}
}).start();
new Thread(() -> {
synchronized (o) {
System.out.println(111);
System.out.println(222);
System.out.println(333);
System.out.println(444);
}
}).start();