synchronized的实现原理

    在多线程并发编程中synchronized一直是元老级角色,很多人都会称呼它为重量级锁。

    利用synchronized实现同步的基础:Java中每一个对象都可以作为锁。集体表现为以下3中形式:

    1、对于普通同步方法,锁是当前实例对象

    2、对于静态同步方法,锁是当前类的Class对象

    3、对于同步代码块,锁是synchronized括号里配置的对象

    JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的。

public static void main(String[] args) {
    String s = "";
    synchronized (s){
        System.out.println(222);
    }
}
public synchronized void f(){
    System.out.println(111);
}

对上述代码进行编译后,使用javap -verbose 类名反编译这个类可以得到下列结果。



可以看到,同步代码块和同步方法使用不同的方式实现的。

    monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须要对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获取对象的锁。

synchronized被很多人成为重量锁。Java为了减少获取锁和释放锁带来的性能消耗而引入了偏向锁和轻量级锁,以及锁的存储结构和升级过程。

    1、对象头

    synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用三个字宽(Word)存储对象头,如果对象时非数组类型的,则用两字宽存储对象头。在32位虚拟机中,1字宽等于4字节   

Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。

在运行期间,Mark Word里存储的数据会随着锁标记位的变化而变化。Mark Word可能变化为存储一下4中数据。


    2、锁的升级

    锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。

    a、偏向锁:

            大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当锁对象第一次被线程获取的时候,虚拟机会将对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后在每次进入这个锁相关的同步块时,不用再进行任何同步操作,只是简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。

            当有另一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向后恢复到未锁定“01”或轻量级锁定“00”的状态,后续的同步操作就会按照轻量级锁那样执行。


    b、轻量级锁

        线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

        轻量级锁解锁时,会使用原子的CAS操作将锁记录中的对象头替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀到重量级锁。

    

    上图所示,两个线程同时竞争锁,导致锁膨胀。

     3、锁的优缺点对比


猜你喜欢

转载自blog.csdn.net/yanghan1222/article/details/80274488