synchronized的实现原理及jvm对其优化

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Hzt_fighting_up/article/details/78619065

synchronized的使用方式
1)在普通方法前面加锁,锁是当前实例对象,即调用该方法的对象
2)在静态方法前面加锁,锁是当前类的Class对象
3)同步代码块中,锁是synchronized括号里配置的对象

jvm对其实现的原理
jvm基于进入和退出monitor对象来实现方法同步和代码块同步。monitorenter指令在编译以后插入到同步代码块的开始位置,monitorexit指令插入到方法结束处和异常处,同步方法实现是使用的另外一种方法实现,但其实也是可以通过这两条指令来实现的。monitorenter和monitorexit两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象,如果java程序中的synchronized明确指定了对象参数,则就是这个对象的reference,如果没有明确指定,则根据synchronized修饰的是实例方法还是类方法,去取对应的对应的对象实例或class对象来作为锁对象。
jvm保证每个monitorenter有对应的monitorexit与之配对。
每个对象都有一个monitor与之关联
当线程执行到monitorenter指令时,将获取对象对应的monitor的所有权,即尝试获取对象的锁。

jvm对其的优化
为什么需要优化
synchronized是一个重量级锁。java的线程是映射到操作系统的原生线程上的,如果要阻塞或唤醒一个线程,都需要操作系统来完成,从用户状态转化到核心状态,状态的转换需要消耗处理器的时间,可能导致转化时间大于代码执行时间,所以需要对其进行一个优化。优化方法:
1)自旋锁:让请求锁的线程的那个线程线程不放弃处理器的执行时间,稍微等待一下,看看持有锁的线程是否很快释放锁。让线程执行一个忙循环(自旋),就是自旋锁技术。自旋等待避免了线程切换(上下文切换)的开销,但要占用处理器时间。如果锁被占用的时间很短,自旋效果很好,否则要等待很久,占用大量处理器资源。所以自旋时间必须有一定的限制,自旋超过了限定的次数仍然没有获得锁(所以自选时间的多久是用自旋的次数去衡量的),超过限制则使用传统方式挂起线程。默认自旋次数(自旋时间)为10次
升级版的自旋锁:自适应的自旋锁
自适应自旋锁和自旋锁的区别:自旋的时间不在固定,而是由前一次在同一个锁上自旋时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚刚成功获得锁,并且持有锁的线程正在运行中,那么虚拟机认为这次自旋锁也有可能再次成功,它将允许自旋等待持续相对更久的时间,相反,如果很少成功获得锁,那可能在获得这个锁时,将忽略自旋过程,转而调用传统方法,将线程挂起。

在介绍下面两个优化时,先介绍一下java对象头
synchronized用的锁是存放在java对象头里的。
java对象头中主要包括Mark Word ,Class Metadata Address ,Array length
Mark Word是实现轻量级锁和偏向锁的关键
Mark Word里主要存储锁状态,对象的HashCode,对象分代年龄,是否是偏向锁,锁标志位(标志锁的类型,是轻量级锁、重量级锁、偏向锁还是没有加锁)。在运行期间存储的数据会随着锁标志位的变化而变化。
2)轻量级锁:
线程在执行同步块之前,jvm首先会在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录(Displaced Mark Word)中,然后线程尝试使用CAS将对象中的Mark Word替换为指向锁记录的指针。如果成功,则表示当前线程获得锁,否则表示有其他线程已经获得了该锁,当前线程采用自旋方式来获得锁。还是有点类似偏向锁记录线程ID。
轻量级锁解锁:使用原子的CAS操作将Displaced Mark Word替换回到对象头。如果替换失败,则说明有其他线程尝试获过取该锁,那需要在释放锁的同时,唤醒被阻塞的线程。如果锁存在竞争,则将升级为重量级锁,避免自旋消耗CPU

3)偏向锁:当一个线程访问同步代码块并获得锁时,会在对象头和栈帧中的锁记录中存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单测试一下对象头中的Mark Word里是存储着指向当前线程的偏向锁

以下面的代码进行分析:

public class TestSynchronize {
    private Object object = new Object();
    private static int i;
    public int increment() {
        synchronized (object) {
            return i++;
        }
    }
}

当一个线程访问该段代码的同步块时,会在object这个对象头上记录下线程ID(线程ID指向获得锁的线程),标志该线程已经拿到了该对象的锁
偏向锁总是偏向第一次获得锁的线程。并且偏向锁使用了一种等到竞争出现才会释放锁的机制,只有当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放偏向锁。此时持有偏向锁的线程如果正在执行,将被暂停,解锁,将线程ID置为null,如果没有子还行,则直接将对象头置为无锁状态
为什么会有这种设计:因为总是同一线程获得锁,如果总是将锁偏向获得锁的线程(感觉就是获得以后不释放锁),可以减少获得锁释放锁带来的cpu切换消耗。
总结:当一个线程尝试获得锁时,首先判断持有该锁的对象的对象头中的线程ID是否指向自己,如果是,则表示之前已经获得锁了,可以直接执行同步块。如果不是指向自己的话,判断是否是偏向锁(对象头中有记录),如果是,则使用CAS将对象头的偏向锁指向自己,此时会暂停之前获得锁的线程,解锁线程ID或若不工作直接释放(就是让线程ID不指向之前的线程),如果不是偏向锁,则直接使用CAS获得锁。偏向锁是通过减少锁的释放和获得来进行优化操作的。

锁的优缺点比较:

这里写图片描述

4)锁消除
锁消除是指虚拟机即时编译时,对一些代码上要求同步,但是检测到不可能存在共享数据竞争的锁进行消除。
5)锁粗化
将几个能够合在一起的同步代码块合并为一个同步代码块
锁粗化的案列:

 public class StringBufferTest {
     StringBuffer stringBuffer = new StringBuffer();

     public void append(){
         stringBuffer.append("a");
         stringBuffer.append("b");
         stringBuffer.append("c");
    }
}

StringBuffer.append()方法中都有一个同步块,jvm会将其优化为一个同步代码块。
锁粗化是通过减少锁的数量从而减少锁的释放和获得来达到优化目标的

猜你喜欢

转载自blog.csdn.net/Hzt_fighting_up/article/details/78619065