介绍对象头之前先说一下Java对象内部的组成结构:
1,成员变量(Data1...DataN)
2, 对象头
Java对象头的组成(根据对象头分析对象状态借此优化代码)
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.8</version>
</dependency>
Java对象的对象头由 mark word 和 meaData,数组长度 三部分组成,
markword: 记录着线程同步锁状态、线程id、标识、hashcode、GC状态等等。
meaData: 元数据指针,指向元空间
数组长度:(类包含了数据对象,才有数组长度)
注意:
1,对象是存在堆里面的
2,类的Class是加载到元空间的(类.Class)
3,指针指向的也是元空间
package com.oujiong.locklearning;
import org.openjdk.jol.info.ClassLayout;
/**
* 打印对象头信息分析代码,进行优化
* @USER: hx
* @DATE: 2021/10/25
**/
public class TypeLock {
String filed = "输出的第一行内容和锁状态内容(thread id和epoch)对应" +
"\n第三行中表示的是被指针压缩为32位的klass pointer" +
"\n第四行则是我们创建的A对象属性信息 1字节的boolean值" +
"\n第五行则代表了对象的对齐字段 为了凑齐64位的对象,对齐字段占用了3个字节,24bit\n";
//todo
// biased_lokc lock 状态
// 0 01 无锁
// 1 01 偏向锁
// 0 00 轻量级锁
// 0 10 重量级锁
// 0 11 GC标记
//todo lock: 锁状态标记位,该标记的值不同,整个mark word表示的含义不同。
//todo biased_lock:偏向锁标记,为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
//todo age:Java GC标记位对象年龄。
//todo identity_hashcode:对象标识Hash码,采用延迟加载技术。当对象使用HashCode()计算后,并会将结果写到该对象头中。当对象被锁定时,该值会移动到线程Monitor中。
//todo thread:持有偏向锁的线程ID和其他信息。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。
//todo epoch:偏向时间戳。
//todo ptr_to_lock_record:指向栈中锁记录的指针。
//todo ptr_to_heavyweight_monitor:指向线程Monitor的指针。
public static void main(String[] args) throws InterruptedException {
//todo 无锁状态
// System.err.println("-------------------------无锁状态----------------------------");
// A a = new A();
// System.out.println(ClassLayout.parseInstance(a).toPrintable());
//todo 偏向锁状态-可以理解成 “特殊状态的偏向锁”,偏向锁就绪状态
//todo JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。
//todo 在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁(但是可以理解成正处于偏向状态,准备好进行偏向了)
//todo 为了减少初始化时间,JVM默认延时加载偏向锁,也可以设置JVM参数 -XX:BiasedLockingStartupDelay=0 来取消延时加载偏向锁。
// System.err.println("-------------------------偏向锁就绪状态----------------------------");
// Thread.sleep(5000);
// A a = new A();
// System.out.println(ClassLayout.parseInstance(a).toPrintable());
// System.err.println("-------------------------偏向锁----------------------------");
//todo 偏向锁
//todo 此时对象a,对象头内容(输出的第一行内容)有了明显的变化,当前偏向锁偏向主线程
// Thread.sleep(5000);
// A a = new A();
// synchronized (a){
// System.out.println(ClassLayout.parseInstance(a).toPrintable());
// }
//todo 轻量级锁,一个线程放了,但主线程还会和放了的线程存在竞争关系
// System.err.println("-------------------------轻量级锁----------------------------");
// Thread.sleep(5000);
// A a = new A();
//
// Thread thread1= new Thread(){
// @Override
// public void run() {
// synchronized (a){
// System.out.println("thread1 locking -- 偏向锁 thread1");
// System.out.println(ClassLayout.parseInstance(a).toPrintable()); //偏向锁
// }
// }
// };
// //todo 表示的线程转换为RUNNABLE状态
// thread1.start();
// //todo 等待thread1执行完,在执行往下运行
// thread1.join();
// //todo 使用场景:主要是为了暂停当前线程,把cpu片段让出给其他线程,减缓当前线程的执行。
// //todo 1. sleep是帮助其他线程获得运行机会的最好方法,但是如果当前线程获取到的有锁,sleep不会让出锁。
// //todo 2. 线程睡眠到期自动苏醒,并返回到可运行状态(就绪),不是运行状态。
// //todo 3. sleep()是静态方法,只能控制当前正在运行的线程
// Thread.sleep(10000);
//
// synchronized (a){
// System.out.println("main locking -- 轻量级锁");
// //todo thread1虽然已经退出同步代码块,但主线程和thread1仍然为锁的交替竞争关系.故此时主线程输出结果为轻量级锁。
// System.out.println(ClassLayout.parseInstance(a).toPrintable());//轻量锁
// }
//todo 重量级锁,让两个线程相互竞争a对象,不顺序释放,升级成重量级锁 -- 没有合理的利用锁
System.err.println("-------------------------重量级锁----------------------------");
Thread.sleep(5000);
TypeLock a = new TypeLock();
Thread thread1 = new Thread(){
@Override
public void run() {
synchronized (a){
System.out.println("thread1 locking");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
try {
//让线程晚点儿死亡,造成锁的竞争
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread thread2 = new Thread(){
@Override
public void run() {
synchronized (a){
System.out.println("thread2 locking");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread1.start();
thread2.start();
}
}
偏向锁批量重偏向与批量撤销
批量重偏向:当一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,会导偏向锁重偏向的操作
批量撤销:在多线程竞争剧烈的情况下,使用偏向锁将会降低效率,于是乎产生了批量撤销机制
JVM的默认参数值#
通过JVM的默认参数值,找一找批量重偏向和批量撤销的阈值
设置JVM参数-XX:+PrintFlagsFinal,在项目启动时即可输出JVM的默认参数值
当然我们可以通过
-XX:BiasedLockingBulkRebiasThreshold 和 -XX:BiasedLockingBulkRevokeThreshold 来手动设置阈值
intx BiasedLockingBulkRebiasThreshold = 20 默认偏向锁批量重偏向阈值
intx BiasedLockingBulkRevokeThreshold = 40 默认偏向锁批量撤销阈值
批量偏向和批量撤销案例
package com.oujiong.locklearning;
import org.openjdk.jol.info.ClassLayout;
import java.util.ArrayList;
import java.util.List;
/**
* 批量偏向锁
* @USER: hx
* @DATE: 2021/10/25
**/
public class BatchBiasLock {
String filed = "输出的第一行内容和锁状态内容(thread id和epoch)对应" +
"\n第三行中表示的是被指针压缩为32位的klass pointer" +
"\n第四行则是我们创建的A对象属性信息 1字节的boolean值" +
"\n第五行则代表了对象的对齐字段 为了凑齐64位的对象,对齐字段占用了3个字节,24bit\n";
// public static void main(String[] args) throws Exception {
//
// System.err.println("---------------------批量重偏向--------------------------");
// //todo 延时产生可偏向对象--偏向锁状态-可以理解成 “特殊状态的偏向锁”,偏向锁就绪状态
// Thread.sleep(5000);
//
// //todo 创造100个偏向线程t1的偏向锁
// List<BatchBiasLock> listA = new ArrayList<>();
// Thread t1 = new Thread(() -> {
// for (int i = 0; i <100 ; i++) {
// BatchBiasLock a = new BatchBiasLock();
// synchronized (a){
// listA.add(a);
// }
// }
// try {
// //todo 为了防止JVM线程复用,在创建完对象后,保持线程t1状态为存活 sleep 100000000 不会释放该锁,一直持有者
// Thread.sleep(100000000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// });
// //todo 启动,但t1始终没有释放锁
// t1.start();
//
// //睡眠3s钟保证线程t1创建对象完成
// Thread.sleep(3000);
// System.out.println("打印t1线程,list中第20个对象的对象头:");
// System.out.println((ClassLayout.parseInstance(listA.get(19)).toPrintable()));
//
// //todo 创建线程t2竞争线程t1中已经退出同步块的锁
// Thread t2 = new Thread(() -> {
// //这里面只循环了30次!!!
// for (int i = 0; i < 30; i++) {
// BatchBiasLock a =listA.get(i);
// synchronized (a){
// //分别打印第19次和第20次偏向锁重偏向结果
// if(i==18||i==19){
// System.out.println("第"+ ( i + 1) + "次偏向结果");
// System.out.println((ClassLayout.parseInstance(a).toPrintable()));
// }
// }
// }
// try {
// Thread.sleep(10000000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// });
// //todo 启动,但t1始终没有释放锁
// t2.start();
//
// //todo 所以两个线程还是存在着竞争关系
// Thread.sleep(3000);
// System.out.println("打印list中第11个对象的对象头:");
// System.out.println((ClassLayout.parseInstance(listA.get(10)).toPrintable()));
// System.out.println("打印list中第26个对象的对象头:");
// System.out.println((ClassLayout.parseInstance(listA.get(25)).toPrintable()));
// System.out.println("打印list中第41个对象的对象头:");
// System.out.println((ClassLayout.parseInstance(listA.get(40)).toPrintable()));
//
// System.err.println("---------------------批量重偏向--------------------------");
//todo 简单总结#
// 概念: 根据设置BiasedLockingBulkRebiasThreshold(默认偏向锁批量重偏向阈值)达到重偏向阈值
// 会自动升级成轻量级锁(如果是在其他线程竞争内)
// }
public static void main(String[] args) throws Exception {
System.err.println("---------------------批量撤销#--------------------------");
Thread.sleep(5000);
List<BatchBiasLock> listA = new ArrayList<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i <100 ; i++) {
BatchBiasLock a = new BatchBiasLock();
synchronized (a){
listA.add(a);
}
}
try {
Thread.sleep(100000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(3000);
Thread t2 = new Thread(() -> {
//todo 这里循环了40次,达到了批量撤销的阈值,重新偏向的就是批量撤销
for (int i = 0; i < 40; i++) {
BatchBiasLock a =listA.get(i);
//todo t2对listA 从索引下边0到39的偏向锁进行了重偏向
synchronized (a){
}
}
try {
Thread.sleep(10000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t2.start();
//———————————分割线,前面代码不再赘述——————————————————————————————————————————
//todo
// 前20个对象,并没有触发了批量重偏向机制,线程t2执行释放同步锁后,转变为无锁形态
// 第20~40个对象,触发了批量重偏向机制,对象为偏向锁状态,偏向线程t2,线程t2的ID信息为xx
// 而41个对象之后,也没有触发了批量重偏向机制,对象仍偏向线程t1,线程t1的ID信息为yy
Thread.sleep(3000);
System.out.println("打印list中第11个对象的对象头:");
System.out.println((ClassLayout.parseInstance(listA.get(10)).toPrintable()));
System.out.println("打印list中第26个对象的对象头:");
System.out.println((ClassLayout.parseInstance(listA.get(25)).toPrintable()));
System.out.println("打印list中第90个对象的对象头:");
System.out.println((ClassLayout.parseInstance(listA.get(89)).toPrintable()));
Thread t3 = new Thread(() -> {
for (int i = 20; i < 40; i++) {
BatchBiasLock a =listA.get(i);
synchronized (a){
//todo 分别打印第21次和第23次偏向锁重偏向结果
//todo 达到设置批量撤销的阈值,进行批量撤销打印消息头
if(i==20||i==22){
System.out.println("thread3 第"+ i + "次");
System.out.println((ClassLayout.parseInstance(a).toPrintable()));
}
}
}
});
t3.start();
//todo 简单总结#
// 根据设置BiasedLockingBulkRevokeThreshold (偏向锁批量撤销阈值),超过了设置的阈值,自动
// 经过锁膨胀,转换成轻量级锁,创建出的新对象剥夺了使用偏向锁的权利
// 1、批量重偏向和批量撤销是针对类的优化,和对象无关(因此重新输出新实例A的对象状态是 无锁状态的)
// 2、偏向锁重偏向一次之后不可再次重偏向 (所以t3的对象经过锁膨胀都从 "偏向锁" 升级成 "轻量级锁")
// 3、当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利
Thread.sleep(10000);
System.out.println("重新输出新实例A");
System.out.println((ClassLayout.parseInstance(new BatchBiasLock()).toPrintable()));
System.err.println("---------------------批量撤销#--------------------------");
}
}
简单总结
1、批量重偏向和批量撤销是针对类的优化,和对象无关
2、偏向锁重偏向一次之后不可再次重偏向
3、当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利