本篇只是基于我这几天的并发编程学习笔记,只是个人理解,可能有误,看到请指正
引入jar包
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
使用jol查看对象的内存布局:
ClassLayout.parseInstance(testObjectSize).toPrintable()
Java对象保存在内存中时,由以下三部分组成:
1,对象头
2,实例数据
3,对齐填充字节
java的对象头由以下三部分组成:
1,Mark Word
2,klass word 指向类class对象的指针
3,数组长度(只有数组对象才有)
4,对齐填充(8字节对齐,差了就补)
64位系统下的头信息:
1.无锁:未用25+hashcode31+未用1+4(对象年龄)+偏向锁标识1+锁状态2
2.偏向锁:偏向内核线程id 54+epoch(偏向记录)2+未用1+4(对象年龄)+偏向锁标识1+锁状态2
3.轻量级锁:栈中锁记录的指针62+锁状态2
4.重量级锁:线程Monitor的指针62+锁状态2
最后三位 101 偏向锁,001 无锁,00轻量锁,10重量锁,11 GC标识
java使用的是小端存储,即高位低存
21685669
cn.mlg.test.jol.Test2 object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 a5 e5 4a (00000001 10100101 11100101 01001010) (1256563969)
4 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
8 4 (object header) 28 30 41 21 (00101000 00110000 01000001 00100001) (557920296)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
hashcode :21685669(十进制)
Mark Word信息,前8个字节00000000 00000000 00000000 00000001 01001010 11100101 10100101 00000001
对象大小计算:
Mark Word 8字节
klass word 开启指针压缩为4字节 没开启8字节
数组长度 4字节没开启8字节
对齐填充
非静态实例数据
引用类型 4字节没开启8字节
基本数据根据其长度计算
-XX:+UseCompressedOops 开启指针压缩(默认)
-XX:-UseCompressedOops (关闭指针压缩)
指针压缩堆内存范围是4G-32G,
java -v 查看指令
synchronized在编译时会转换成monitorenleter指令,关于monitorenleter的jvm规范:objectref必须是引用类型。每个对象都有一个与其关联的监视器。执行monitorenter的线程获得与objectref关联的监视器的所有权。如果另一个线程已经拥有与objectref关联的监视器,则当前线程将等待直到对象被解锁,然后再次尝试获取所有权。如果当前线程已经拥有与objectref关联的监视器,则它将在监视器中增加一个计数器,以指示该线程进入监视器的次数。如果与objectref关联的监视器不属于任何线程,则当前线程将成为监视器的所有者,并将此监视器的条目计数设置为1
https://docs.oracle.com/javase/specs/jvms/se6/html/Instructions2.doc9.html#monitorenter
jdk在1.6之前synchronized一直是重量级锁、悲观锁,互斥锁,在1.6之后优化了一下分成偏向锁,轻量级锁、重量级锁
重量级锁:内置锁在Java中被抽象为监视器锁(monitor)。在JDK 1.6之前,监视器锁可以认为直接对应底层操作系统中的互斥量(mutex),它每次会调用系统函数(待验证)引起的内核态与用户态切换、线程阻塞造成的线程切换、使用成本比较高。在多个线程竞争的时候synchronized关键字会膨胀为重量级锁
public static void main(String[] args) throws InterruptedException {
DemoA demoA = new DemoA();
Thread thread1 = new Thread(){
public void run() {
synchronized (demoA){
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
}
}
};
thread1.start();
Thread thread2 = new Thread(){
public void run() {
synchronized (demoA){
System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
}
}
};
thread2.start();
}
轻量级锁:如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,比如交替运行线程,只会调用一次os函数
DemoA demoA = new DemoA();
Thread thread1 = new Thread(){
public void run() {
synchronized (demoA){
System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
}
}
};
thread1.start();
thread1.join();
new Thread().start();
Thread thread2 = new Thread(){
public void run() {
synchronized (demoA){
System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
}
}
};
thread2.start();
}
中间new Thread().start();是为了不让线程id相同,这个线程id不是java线程id,如果不加,运行可能导致线程id线程相同,都是偏向锁
偏向锁:如果不仅仅没有实际竞争,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS,不用调用os函数
有的文档将自旋也是定义一个锁,其实它不是一个状态,只是获取一个锁的过程
偏向锁和轻量级锁 都是乐观锁
public static void main(String[] args) throws InterruptedException {
//
Thread.sleep(6000l);
DemoA demoA = new DemoA();
Thread thread1 = new Thread(){
public void run() {
synchronized (demoA){
System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
}
}
};
thread1.start();
}
sleep原因:JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁。这个延时的时间大概为4s左右,具体时间因机器而异。当然我们也可以设置JVM参数 -XX:BiasedLockingStartupDelay=0 来取消延时加载偏向锁
使用参数-XX:-UseBiasedLocking禁止偏向锁优化(默认打开)
需要注意的是如果对象已经计算了hashcode就不能偏向了,如果调用wait方法则立刻变成重量锁
public static void main(String[] args) throws InterruptedException {
Thread.sleep(6000l);
DemoA demoA = new DemoA();
System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
Thread thread1 = new Thread(){
public void run() {
synchronized (demoA){
System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
}
}
};
thread1.start();
thread1.join();
System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
new Thread(){
@Override
public void run() {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
Thread thread2 = new Thread(){
public void run() {
synchronized (demoA){
System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
}
}
};
thread2.start();
// thread2.join();
// thread1.join();
System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
/* new Thread(){
@Override
public void run() {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();*/
Thread thread3 = new Thread(){
public void run() {
synchronized (demoA){
System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
}
}
};
thread3.start();
System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
//thread3.join();
}
1.jvm初始完创建对象,虽然是一个偏向锁状态,但是其线程id是0 也就是偏向任何线程,是一个可偏向状态
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
2.thread1 加了synchronized ,此时只有一个线程独享,无其他线程竞争,而且线程是可偏向状态,此时就会偏向thread1 ,将线程id修改为thread1 id,大致过程是先判断线程是否可偏向可以就偏向,不可以就判断线程id是否是相同
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 80 2d 25 (00000101 10000000 00101101 00100101) (623738885)
偏向线程执行完成后,不会撤销,只会膨胀
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 80 2d 25 (00000101 10000000 00101101 00100101) (623738885)
3.thread2 等thread1执行完成后同步,此时发现已经偏向了别的线程,先撤销偏向锁,改为无锁状态,在膨胀为轻量级锁,指针指向指向原线程的锁记录,用CAS将指针指向当前线程锁记录成功就获得轻量级锁,失败会自旋(循环一定次数),减少线程切换
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 10 ee cb 25 (00010000 11101110 11001011 00100101) (634121744)
轻量锁执行完成后,会置为无锁状态,上述代码因为下面测试重量级锁,不一样
cn.mlg.test.concurrent.DemoA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4.轻量级锁自旋失败就会升级为重量级锁,会将Mark Word指向monitor,monitor是一个C++对象,对C++和jvm源码不懂,只知道里面存了一个等待锁的集合和当前持有锁线程
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 2a 37 2a 21 (00101010 00110111 00101010 00100001) (556414762)
重量级锁执行完成后还是重量级锁,不会撤销和降级
public static void main(String[] args) throws InterruptedException {
Thread.sleep(6000l);
List<DemoA> list= new ArrayList<>();
Thread thread = new Thread(){
@Override
public void run() {
for (int i = 0; i < 50; i++) {
DemoA demoA = new DemoA();
synchronized (demoA){
list.add(demoA);
/* System.out.println(Thread.currentThread().getId());
System.out.println(ClassLayout.parseInstance(demoA).toPrintable());*/
if (i==10){
System.out.println(ClassLayout.parseInstance(demoA).toPrintable()) ;
}
}
}
try {
Thread.sleep(50000l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread.start();
try {
//保证thread循环完成
Thread.sleep(3000l);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread1 = new Thread(){
public void run() {
for (int i = 0; i <40; i++) {
DemoA demoA = list.get(i);
synchronized (demoA){
if (i==18 ||i==19 ){
System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
}
}
}
try {
Thread.sleep(50000l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread1.start();
try {
//保证thread1循环完成
Thread.sleep(3000l);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread2 = new Thread(){
public void run() {
for (int i = 20; i <50; i++) {
DemoA demoA = list.get(i);
synchronized (demoA){
if (i==20 ||i==39 ){
System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
}
}
}
try {
Thread.sleep(50000l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread2.start();
}
批量重偏向好测试,
以class为单位,维护了一个偏向锁撤销计数器,每次class对象撤销就+1,当这个值达到阀值(默认20)时,jvm认为该class偏向有问题,就发送批量重偏向。主要是根据epoch的,class中维护了个epoch,对象中也有epoch(倒数第9 和第十位),对象在创建的时候取得就是class中的epoch,每次发送批量重偏向class中的epoch+1,就会遍历jvm所有栈中 在处于偏向锁加锁状态的对象(还在同步块中的),将其epoch改为class中的epoch,不在同块中的就不修改,下次获得锁时候,就去判断epoch和class中是否相等就不相等说明已经失效,就通过cas将线程id改为当前线程id
Thread.sleep(6000l);
List<DemoA> list= new ArrayList<>();
Thread thread = new Thread(){
@Override
public void run() {
for (int i = 0; i < 50; i++) {
DemoA demoA = new DemoA();
synchronized (demoA){
list.add(demoA);
if (i==35){
try {
System.out.println("35到了");
Thread.sleep(50000l);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("35走了");
}
}
}
try {
Thread.sleep(50000l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread.start();
try {
//保证thread循环完成
Thread.sleep(2000l);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread1 = new Thread(){
public void run() {
for (int i = 0; i <35; i++) {
DemoA demoA = list.get(i);
synchronized (demoA){
if (i==20 ){
System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
System.out.println(ClassLayout.parseInstance(list.get(34)).toPrintable());
}
}
}
try {
Thread.sleep(50000l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread1.start();
thread.join();
thread1.join();
批量撤销有点难重现(批量重偏向同一代码下面就是),达到了批量撤销的阈值(默认40),就会发生批量撤销,此时膨胀为轻量级级锁
-XX:+PrintFlagsInitial 可以打印启动信息
BiasedLockingBulkRebiasThreshold = 20 批量偏向的阈值
BiasedLockingBulkRevokeThreshold = 40 批量撤销的阈值
BiasedLockingStartupDelay = 4000 偏向锁延时
目前还有很多问题没清楚,学习并发需要熟悉JVM,系统和硬件有一定了解,还有很多其实没搞清楚,等我学完在更新