java锁的膨胀升级过程实例详细解

synchronized 关键字的作用:

确保线程间能互斥地访问同步块,即同一时间只有一个线程能进入同步块

  • 解决了可见性的问题
  • 解决了指令重排的问题
  • 解决了原子性问题

多个线程有可能同时去访问同一个变量,我们称之为临界资源。

隐式锁(内置锁) - synchronized的使用方法:

1.synchronized 加在方法上面,锁是加在当前类的对象上面,this。

2.synchronized 加在静态方法上面,锁加在当前方法所在类的上面Test.class.

3.synchronized 加在方法中的同步块,自己定义的object上面。

synchronized (object) {
    count++;
} 

显示锁的使用方法:

显示锁 - ReentrantLock lock = new ReentrantLock();

lock.lock();
count++;
finally{ 
    lock.unock();
}

Synchronize底层原理:

java锁的膨胀升级过程实例详细解_第1张图片

unsafe方法的如下代码可以代替synchronized并且可跨越方法使用:

汇编中对应:

java锁的膨胀升级过程实例详细解_第2张图片

以上对应java8大操作中的lock和unlock操作。

为什么有了Synchronized 还要有Reentranlock ?

在synchronized小于java1.6版本的时候效率非常低,如下,他依赖于操作系统的互斥量Mutex,需要线程从用户态切换到内核态,线程状态切换开销非常大.

后来duogli开发了一套AQS,其中一个ReentrantLock效率比Synchronize高,虽然是用纯java开发的但是效率却比java原生的Synchronize更高。

为了挽回面子,Oracle收购Java之后,有对Synchronize进行了优化,加入了偏向锁,轻量级锁等概念,称为锁的膨胀升级过程。

java锁的膨胀升级过程实例详细解_第3张图片

在汇编中Synchronized所包含的代码块会被翻译成下面2个命令来说实现锁的:

锁的升膨胀升级过程:

java锁的膨胀升级过程实例详细解_第4张图片

对于锁的升级信息记录在object的Mark word里面的。

我们知道一个对象包含对象头,实例数据区,对其填充位。

Mark word就在对象头中,下图是对象内存结构:

  1. 对象头
  2. 对象实际数据
  3. 对齐填充位:对象的大小必须是8字节的整数倍。

Mark work的32位是如何记录锁的状态的?

实例观测锁的升级过程:

1. 使用jol-col工具来打印mark word.

        
            org.openjdk.jol
            jol-core
            0.10
        

JVM, 会默认延迟启动偏向锁,默认延迟4秒。因为JVM内部的启动自己的线程会有部分激烈的竞争,为了提高效率避免从偏向锁,轻量级锁,重量级锁这样一步一步的升级效率低, 所以延迟启动偏向锁,让其自己从轻量级锁开始。

2. 所以此处关闭默认延迟:

-XX:BiasedLockingStartupDelay=0

模拟锁升级过程:

下面代码中有4个步骤,实例展示锁是如何升级的:

        Object o = new Object();
        //Step 1
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        //Step 2
        synchronized(o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
        //Step 3
        new Thread(()-> {
            synchronized(o) {
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }).start();
        //Step 4
        new Thread(()-> {
            synchronized(o) {
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }).start();

执行结果:

分析结果:

Step1 执行结果:没有任何线程在object加锁,object是匿名偏向锁。

java锁的膨胀升级过程实例详细解_第5张图片

如上图为mark work的区域,操作系统分为大端模式和小端模式,windows和linux用的是小端模式,所以前面是地位后面是高位。我们将其调换与对照表一样的格式为

小端模式 -> 00000101 00000000 00000000 00000000

转换为大端模式->00000000 00000000 00000000 00000101

最后结果为101,对照表格,101位偏向锁,为什么以上来还没有加任何锁的时候就是偏向锁呢 ?

-- 这里是匿名偏向,虽然还没有任何线程在该object上加锁,但是java默认给它加了一个匿名偏向,就相当于一个锁前的准备状态。

Step2 执行结果:有一个线程在object加锁,这时候object上记录了加锁的线程号。因为没有竞争所以这里加的是偏向锁。

java锁的膨胀升级过程实例详细解_第6张图片

小端模式 -> 00000101 11101000 01110100 00000010

转换为大端模式->00000010 01110100 11101000 00000101

这里的末尾3位虽然还是101,但是可以看出,线程ID 的位置不在是0了,这里记录了锁住object的线程Id。

Step 3执行结果:存在少少量的锁竞争,所以是轻量级锁

小端模式 -> 00100000 11110100 01100000 00011010

转换为大端模式->00011010 01100000 11110100 00100000

最后2位是00, 对照表格为偏向锁。因为当step3或step4执行的时候,发现有竞争,自己自旋等待加锁,发现在自旋结束前加锁成了,所以加上了偏向锁。

step 4 执行结果:偏向锁升级成了重量级锁

小端模式 -> 11011010 10001000 00001001 00000011

转换为大端模式->00000011 00001001 10001000 11011010

最后三位是010,对照表格是重量级锁,因为刚才的step3和step4同时执行存在竞争,竞争失败的那个线程进行了锁升级,于是变成了重量级锁。
 

Hashcode一开始没有打印出来,因为这里默认是懒打印的,所以这里没有体现。

对象的hashcode在偏向锁状态的情况下在上图中没有标记出来,在哪里记录的呢 ?

-- 在偏向锁的状态下调用hashcode会触发锁升级,升级成轻量级锁,因为按照表格里面没有记录hashcode的地方。

 

Monitor 管存对象是什么?

可以把它理解成一个控制同步的工具,他就是一个对象用C语言定义的一个对象。在object升级成重量级锁的时候,其高位记录的就是Monitor对象的地址。在c语言中,ObjectMonitor对象中包含很多属性,介绍下面几个重要的属性,

_WaitSet -- 处于wait状态的线程会被加到这个队列中,等待Notify。

_EntryList -- 处于等待锁被block的线程会被加到这个队列中,等待锁结束。

_Owner -- 指向持有ObjectMonitor对象的线程,即,在重量级锁中进入到同步块的那个线程。

每个java对象被创建时都会为其创建一个monitor对象,java对象的对象头Mark word中有指向该monitor对象的指针,Synchronized就是使用这种方式来获取锁的,这就是为什么任何一个java object都可以作锁的原因。同时notify、notifyall,wait也需要使用到nomitor对象,如上面介绍的_WaitSet。所以为什么这些命令必须要在Snchronized块中。

 

关闭偏向锁:-XX:-UseBiasedLocking, JDK 1.6 之后并默认开启偏向锁

 

你可能感兴趣的:(java,高并发,java,多线程,jvm,锁的膨胀升级过程,Monitor管存对象)