锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。
synchronize用的锁是存储在Java对象头里的。
长度 | 内容 | 说明 |
32/64bit | Mark Word | 存储对象的Hashcode、锁信息等 |
32/64bit | Class Metadata Address | 存储到对象类型数据的指针 |
32/64bit | Array Length | 数组的长度(如果当前对象是数组) |
Mark Word的状态变化
锁状态 | 是否是偏向锁 | 锁标记位 |
无锁 | 01 | |
偏向锁 | 1 | 01 |
轻量级锁 | 00 | |
重量级锁 | 10 |
锁的状态变化
无锁升级为偏向锁:第一个线程来获取锁,升级为偏向锁。这个线程成为锁的偏向线程。
偏向锁升级为轻量级锁:偏向线程释放了锁,其他线程(非偏向线程)来获取锁,升级为轻量级锁。
偏向锁升级为重量级锁:偏向线程没有释放锁,其他线程来获取锁,存在竞争,升级为重量级锁。
轻量级锁升级为重量级锁:线程A是偏向线程,线程A释放了锁,线程B来获取锁,升级为轻量级锁,线程B未释放锁,线程C来获取锁,升级为重量级锁。
锁的一些特点
1.轻量级锁不存在锁的竞争,只要存在锁的竞争,一定是重量级锁。
2.不管偏向锁还是轻量级锁,只要存在锁的竞争,就升级为重量级锁。
3.锁升级之后不能逆向降级。
一、偏向锁
对象头的Mark Word里存储偏向线程ID。
当前线程的栈帧的锁记录里存储偏向线程的ID。
1.偏向锁的撤销
有线程来竞争偏向锁的时候才会释放锁。
对象头的Mark Word要么偏向于其他线程、要么恢复到无锁或者标记对象不适合作为偏向锁。
删除当前线程的栈帧里存储的锁记录。
二、轻量级锁
1.轻量级锁加锁
线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中。
然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。
2.轻量级锁解锁
对应轻量级锁的加锁,将栈帧中存储的锁记录替换回对象头。
三、重量级锁
重量级锁的实现使用内核中的互斥量mutex实现。
当锁是重量级锁状态,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。
四、不同锁的优缺点
锁 | 优点 | 缺点 | 适用场景 |
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 自旋消耗CPU资源 | 追求响应速度 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应慢 | 追求吞吐量 |