轻量级锁升级重量级锁,这块的内容总算是捋明白了,只要存在阻塞状态,那么肯定就是重量级锁了。必定和monitor对象中的waitSet以及entryList所相关。
偏向锁,我上次写的代码中,看到了无锁,轻量级锁,重量级锁。但是却没有看到偏向锁。偏向锁其实是最常见的锁。下面说一下。
偏向锁状态
偏向锁其实默认就开启着,对象创建的时候,其实也应该是偏向锁状态。但是,就我上次所写的代码,默认情况下是无锁normal。问题何在?
因为偏向锁的加载是一个延迟加载的过程,不会在程序启动时,立刻加载。如果想加载,需要添加-XX:BiasedLockingStartupDelay=0禁用延迟。试试呗。
package com.bo.threadstudy.four;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
/**
* 偏向锁测试
*/
@Slf4j
public class BiasedLockTest {
public static void main(String[] args) {
Object o = new Object();
String s = ClassLayout.parseInstance(o).toPrintable();
log.debug(s);
}
}
看看禁用了偏向锁延迟后的结果。
现在状态是101了,也就是偏离锁了。
那么,再复习一下对象头是偏离锁的mark word是什么样子。
thread是指该锁资源对于具体偏向线程的线程ID。可以这么理解,偏向锁在使用过程中,假如一直都是当前线程在访问锁资源,锁资源没有被其它线程所获取,那么就直接校验一下,当前线程的线程ID是否与偏向锁中的thread一致,如果一致,则直接进入,获取到这个锁即可,锁的话,比运用了CAS的轻量级锁更加轻量。在轻量级锁场景下线程每次访问锁资源,要先在线程底部创建锁记录,然后使用CAS将锁资源的markword信息与锁记录中的地址信息以及状态信息进行交换,这个操作很明显比偏离锁重。
epoch,以及unused这个后期查查,在这块知识点中不是重要内容。
age,代表了在JVM中survior复制区中的年龄。
biased_lock代表锁偏离状态,0代表无锁,1代表偏离锁。
后面的两位也是锁状态了。
锁偏离状态流程转化
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import java.io.IOException;
/**
* 偏向锁测试
*/
@Slf4j
public class BiasedLockTest {
private static Dog lock =new Dog();
// private static Object lock =new Object();
public static void main(String[] args) {
new Thread(() -> {
log.debug("进入同步代码块前锁状态");
getStatus();
synchronized(lock){
log.debug("进入同步代码块中锁状态");
getStatus();
}
log.debug("从同步代码块出来状态");
getStatus();
}).start();
}
private static void getStatus() {
String s = ClassLayout.parseInstance(lock).toPrintable();
log.debug(s);
}
}
@Slf4j
class TeacherTest{
public static void main(String[] args) throws IOException {
Dog d = new Dog();
ClassLayout classLayout = ClassLayout.parseInstance(d);
new Thread(() -> {
log.debug("synchronized 前");
System.out.println(classLayout.toPrintable());
synchronized (d) {
log.debug("synchronized 中");
System.out.println(classLayout.toPrintable());
}
log.debug("synchronized 后");
System.out.println(classLayout.toPrintable());
}, "t1").start();
}
}
class Dog{
}
在写代码的时候,发现了一个问题,如果我用我创建的dog类对象来作为锁,此时三种状态下均是偏离锁。
但如果这里用Object对象来作为锁,发现了其状态为偏离锁->轻量级锁->无锁的情况。
这个是什么原因呢?
个人理解,应该是Object状态下,有偏离锁撤销的操作,比如调用了HashCode的方法之类的。不过只是猜测,这个先作为一个疑问留下来吧。
偏离锁的撤销操作
1.hashcode方法撤销偏离
hashcode方法撤销偏离锁,其实是Object类里未被重写的hashcode方法调用才可以撤销偏离锁,而被重写的hashcode方法是没有这个功能的。看了一下,object中的hashcode是一个native本地方法,应该是在底层中完成了对对象头的内容改造。因为刚才看过,hashcode的值是存放至markword中的,markword共有64bit,但在偏离锁场景下,thread字段就占据了54条,hashcode存放不下,只能改造对象头以存放hashcode,此时偏离锁撤销为无锁。
轻量级锁和重量级锁是不会撤销的,因为轻量级锁的hashcode记录在经过CAS交换后的线程内部的锁记录中,可以取到。而重量级锁,锁资源的hashcode存放至操作系统中的monitor对象中,也是可以及时获取到的。
package com.bo.threadstudy.four;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
/**
* 撤销偏离锁的操作
*/
@Slf4j
public class RevokeBiasedLockTest {
// private static final String lock = new String();
private static final Dog1 lock = new Dog1();
public static void main(String[] args) {
log.debug("创建时");
getStatus();
log.debug("hashcode后");
lock.hashCode();
getStatus();
}
private static void getStatus() {
String s = ClassLayout.parseInstance(lock).toPrintable();
log.debug(s);
}
}
class Dog1{
}
结果不贴了,确实锁撤销了。
2.其他线程使用偏向锁对象,升级为轻量级锁
准确的说是,在没有发生锁竞争的情况下,其他线程使用该偏向锁,会将其升级为轻量级锁。
这个过程可以理解为偏向锁->撤销锁->轻量级锁的过程。
使用的时候注意操作系统底层的线程复用,这个也是偏向锁快的一个原因。
package com.bo.threadstudy.four;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
/**
* 撤销偏离锁的操作
*/
@Slf4j
public class RevokeBiasedLockTest {
// private static final String lock = new String();
private static final Dog1 lock = new Dog1();
public static void main(String[] args) {
//hashcode达到锁撤销
// log.debug("创建时");
// getStatus();
// log.debug("hashcode后");
// //只有原生的native的hashcode方法才能实现锁撤销
// lock.hashCode();
// getStatus();
//其他线程在没有竞争的情况下,获取锁资源升级成轻量级锁,最后解锁达到锁撤销
Thread t1 = new Thread(() -> {
//阻塞当前线程,因为操作系统底层可能出现线程复用的情况,即假设我需要t2线程执行完成后,在执行t1,如果使用join方法
//那么很可能t2执行完成后,t1调用的操作系统底层线程还是原先的t2下调用的线程,线程ID一致,偏离锁校验成功,最终还是偏离锁
//操作系统底层的线程复用,也是偏离锁提升性能的一种方式。
synchronized (Dog1.class){
try {
Dog1.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//保证t2执行完成后,t2底层调用的操作系统线程与t1调用的操作系统线程不是同一个
synchronized (lock){
log.debug("t1线程中Synchronized中");
//轻量级锁
getStatus();
}
//无锁
log.debug("t1线程中Synchronized后");
getStatus();
}
},"t1");
Thread t2 = new Thread(() -> {
synchronized (lock){
log.debug("t2线程中Synchronized中");
//偏离锁
getStatus();
}
log.debug("t2线程中Synchronized后");
//偏离锁
getStatus();
synchronized ( Dog1.class){
Dog1.class.notify();
}
},"t2");
t1.start();
t2.start();
}
private static void getStatus() {
String s = ClassLayout.parseInstance(lock).toPrintable();
log.debug(s);
}
}
class Dog1{
}
3.wait/notify方式锁撤销
这种其实是发生在调用了wait,但没有发生竞争的情况下。偏离锁在调用wait方法前,是当前线程的偏离锁,在调用wait方法后,释放了这个锁,成为无锁状态,然后被另一个线程获取到升级成重量级锁。
可以理解为在未发生竞争的条件下,调用wait前轻量级锁->wait释放了锁(无锁)->另一个线程获取到锁(重量级锁)。
如果发生了竞争,此时这个锁就是锁膨胀。没发生竞争调用wait,这种方式就是锁撤销。
package com.bo.threadstudy.four;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 撤销偏离锁的操作
*/
@Slf4j
public class RevokeBiasedLockTest {
// private static final String lock = new String();
private static final Dog1 lock = new Dog1();
public static void main(String[] args) throws InterruptedException {
//1.hashcode达到锁撤销
// log.debug("创建时");
// getStatus();
// log.debug("hashcode后");
// //只有原生的native的hashcode方法才能实现锁撤销
// lock.hashCode();
// getStatus();
//2.其他线程在没有竞争的情况下,获取锁资源升级成轻量级锁,最后解锁达到锁撤销
// Thread t1 = new Thread(() -> {
// //阻塞当前线程,因为操作系统底层可能出现线程复用的情况,即假设我需要t2线程执行完成后,在执行t1,如果使用join方法
// //那么很可能t2执行完成后,t1调用的操作系统底层线程还是原先的t2下调用的线程,线程ID一致,偏离锁校验成功,最终还是偏离锁
// //操作系统底层的线程复用,也是偏离锁提升性能的一种方式。
// synchronized (Dog1.class){
// try {
// Dog1.class.wait();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// //保证t2执行完成后,t2底层调用的操作系统线程与t1调用的操作系统线程不是同一个
// synchronized (lock){
// log.debug("t1线程中Synchronized中");
// //轻量级锁
// getStatus();
// }
// //无锁
// log.debug("t1线程中Synchronized后");
// getStatus();
//
// }
//
// },"t1");
// Thread t2 = new Thread(() -> {
// synchronized (lock){
// log.debug("t2线程中Synchronized中");
// //偏离锁
// getStatus();
// }
//
// log.debug("t2线程中Synchronized后");
// //偏离锁
// getStatus();
//
// synchronized ( Dog1.class){
// Dog1.class.notify();
// }
//
//
// },"t2");
// t1.start();
// t2.start();
//wait/notify实现锁撤销
AtomicBoolean flag = new AtomicBoolean(true);
//主线程
log.debug("主线程");
getStatus();
Thread t1 = new Thread(() -> {
synchronized (lock) {
log.debug("t1线程中Synchronized中");
//重量级锁
getStatus();
lock.notify();
flag.set(false);
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (lock) {
try {
while(flag.get())
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t2线程中Synchronized中");
//重量级锁
getStatus();
}
log.debug("t2线程中Synchronized后");
//重量级锁
getStatus();
}, "t2");
t1.start();
t2.start();
t2.join();
log.debug("结束后");
getStatus();
}
private static void getStatus() {
String s = ClassLayout.parseInstance(lock).toPrintable();
log.debug(s);
}
}
class Dog1 {
}
批量重偏向
初次创建一个对象,线程是偏离锁状态,此时的偏离锁是没有归属线程的。只有在进入synchronized时,此时才会指明偏向的线程。
但是当存在其它线程来调用这个资源的时候,但不存在竞争情况,此时这个资源会升级成轻量级锁。但是轻量级锁相对偏离锁性能较差。所以,撤销偏向锁阈值超过20次之后,jvm会觉得底层偏向错误,然后对使用了当前线程的锁资源进行重新偏向。但是我无法联想在真正的业务场景下,什么情况下才会超过偏向锁阈值20次?就针对一个锁来说,不理解。
package com.bo.threadstudy.four;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import java.util.ArrayList;
/**
* 批量重偏向测试方法
*/
@Slf4j
public class BatchBiasedLockedTest {
// private static final String lock = new String();
public static void main(String[] args) {
ArrayList<String> lockList = new ArrayList<String>();
new Thread(() -> {
//测试批量重偏向,关键问题在于撤销偏离锁20次,除了for循环创建20次锁,还有别的办法撤销20次偏离锁吗,这个没有锁降级,应该不行,只能for循环了
for (int i = 0; i < 20; i++) {
String lock = new String();
log.debug("t1线程synchronized前"+i);
getStatus(lock);
lockList.add(lock);
//最初进入肯定是偏离锁
synchronized (lock){
log.debug("t1线程synchronized中"+i);
getStatus(lock);
}
//从synchronized出来后,线程ID记录在markword中,肯定还是当前线程的偏离锁了
log.debug("t1线程synchronized后"+i);
getStatus(lock);
}
synchronized (lockList){
lockList.notify();
}
}, "t1").start();
new Thread(() -> {
synchronized (lockList){
try {
lockList.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < lockList.size(); i++) {
//在这里将偏向锁升级成轻量级锁,也就是偏向锁解锁,看看20次是否会重偏向
String lock = lockList.get(i);
synchronized (lock){
log.debug("t2线程synchronized前"+i);
getStatus(lock);
}
}
}, "t2").start();
}
private static void getStatus(Object lock) {
String s = ClassLayout.parseInstance(lock).toPrintable();
log.debug(s);
}
}
批量撤销
当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象
都会变为不可偏向的,新建的对象也是不可偏向的。
还是无法联想业务场景,难道是创建一个类专门做锁类吗,然后这个锁类所创建的对象来作为synchronized中的锁进行操作?满40次直接轻量锁?不理解。罢了,也不是重点。
代码都不想写了,过。
自旋优化
轻量级锁,升级重量级锁的这个过程中,底层会存在一个自旋锁,来进行自旋优化。因为轻量级锁升级重量级锁,底层会存在用户态向内核态之间的切换,性能不高。所以,设置自旋优化,期待在自旋的这段时间内,执行任务的线程可以将任务执行完成,最后由等待的线程直接获取到锁资源,而不用去monitor对象中,唤醒entryList中的元素,然后走耗费性能较高的操作。
这次偏离锁学的全是疑问,后续实践中慢点来吧。