历史问题
一开始的synchronized执行需要暂停线程,一旦暂停线程就需要操作系统的介入,由用户态转换为内核态,需要消耗大量的CPU资源
有时候切换的时间可能会比代码执行的时间还要长
所以我们引入轻量级锁
Monitor
每个对象都存在着一个 Monitor对象与之关联。执行 monitorenter 指令就是线程试图去获取 Monitor 的所有权,抢到了就是成功获取锁了;执行 monitorexit 指令则是释放了Monitor的所有权。
每个对象都有一个Monitor,monitor的本质是依赖于操作系统的Mutex Lock的实现,这个就是重量级锁
1. 如果一个java对象被某个线程锁住,则对象的MarkWord字段中会存在一个指向monitor地址的指针
2. Monitor的owner字段会存放拥有对象锁的线程id
偏向锁:单线程竞争
相当于经常占有资源的线程,就把这个资源直接不设置锁,让这个资源被这个线程独占,不需要引入操作系统,降低线程切换提高系统性能
偏向锁偏向于第一个访问锁的线程,将第一个线程id存储在markword里面,以后线程每次使用资源就会检查一下Markword看看里面存的是不是自己,如果是自己就不需要强锁了,直到遇到线程竞争才会释放锁
锁竞争会尝试使用CAS的方法修改markword的id
竞争成功,还是偏向锁,线程id变为新线程id
竞争失败,这个时候可能会升级为轻量级锁
如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也即偏向锁在资源没有竞争情况下消除了同步语句,懒得练CAS操作都不做了,直接提高程序性能
性能最高
轻量级锁
本质是CAS
JVM会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,官方成为Displaced Mark Word。若一个线程获得锁时发现是轻量级锁,会把锁的MakWord复制到自己的Displaced Mark Wod里面。然后线程尝试用CAS将锁的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。
重量级锁
当轻量级锁自旋的次数超过一定次数,就进行锁升级为重量级锁
基于monitor实现了
在编译时加入monitor enter和monitor exit
当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor。
锁升级为轻量锁或者重量锁之后mark word保存的是两个锁的指针,已经没有位置保存hash码了,那么hash码在哪里?
hash码在称为偏向锁的时候就没有了,但是升级为重量锁的时候又有了只不过这个时候的hash码存储在objectmonitor里面
一个对象调用hashcode之后就没办法称为偏向锁了,如果还要上锁的话就直接变为轻量级锁
小结
JIT编译器与锁
锁消除
当对象称为锁,然后再进行hashcode方法,就会让这个锁变成无锁
再jit层面,相当于sychronized不存在
锁粗化
public class LockBigDemo
{
static Object objectLock = new Object();
public static void main(String[] args)
{
new Thread(() -> {
synchronized (objectLock){
System.out.println("111111");
}
synchronized (objectLock){
System.out.println("222222");
}
synchronized (objectLock){
System.out.println("333333");
}
synchronized (objectLock){
System.out.println("444444");
}
synchronized (objectLock){
System.out.println("111111");
System.out.println("222222");
System.out.println("333333");
System.out.println("444444");
}
},"t1").start();
}
}
如果有多段sychronized锁住的代码块,再jit层面可能会进行锁合并,把它变成一个sychronized区块