Synchronized实现原理深入解析

  • 作者: 凯哥Java(公众号:凯哥Java)
  • 并发
  • 时间:2020-10-31 21:26
  • 5140人已阅读
简介 Synchronized关键字算是Java的元老级锁了,一开始它撑起了Java的同步任务,其用法简单粗暴容易上手。但是有些与它相关的知识点还是需要我们开发者去深入掌握的。比如,我们都知道通过Synchronized锁来实现互斥功能,可以用在方法或者代码块上,那么不同用法都是怎么实现的,以及都经历了了哪些优化等等问题都需要我们扎实的理解。1.基本用法2.实现原理2.1同步代码块的实现2.2同步方法的

🔔🔔好消息!好消息!🔔🔔

 如果您需要注册ChatGPT,想要升级ChatGPT4。凯哥可以代注册ChatGPT账号代升级ChatGPT4

有需要的朋友👉:微信号 kaigejava2022

Synchronized关键字算是Java的元老级锁了,一开始它撑起了Java的同步任务,其用法简单粗暴容易上手。但是有些与它相关的知识点还是需要我们开发者去深入掌握的。比如,我们都知道通过Synchronized锁来实现互斥功能,可以用在方法或者代码块上,那么不同用法都是怎么实现的,以及都经历了了哪些优化等等问题都需要我们扎实的理解。

1.基本用法

通常我们可以把Synchronized用在一个方法或者代码块里,方法又有普通方法或者静态方法。

对于普通同步方法,锁是当前实例对象,也就是this

public class TestSyn{  private int i=0;  public synchronized void incr(){
    i++;
  }
}

对于静态同步方法,锁是Class对象

public class TestSyn{  private static int i=0;  public static synchronized void incr(){
    i++;
  }
}

对于同步代码块,锁是同步代码块里的对象

public class TestSyn{  private  int i=0;  Object o = new Object();  public  void incr(){    synchronized(o){
        i++;
    }
  }
}

2.实现原理

在JVM规范中介绍了synchronized的实现原理,JVM基于进入和退出Monitor对
象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,通过一个方法标志(flag) ACC_SYNCHRONIZED来实现的。

2.1 同步代码块的实现

monitorenter 和 monitorexit

https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.monitorenter (参考来源)

下面看下JVM规范里对moniterenter 和 monitorexit的介绍

Each object has a monitor associated with it. The thread that executes monitorenter gains ownership of the monitor associated with objectref. If another thread already owns the monitor associated with objectref, the current thread waits until the object is unlocked,

每个对象都有一个监视器(Moniter)与它相关联,执行moniterenter指令的线程将获得与objectref关联的监视器的所有权,如果另一个线程已经拥有与objectref关联的监视器,则当前线程将等待直到对象被解锁为止。

A monitorenter instruction may be used with one or more monitorexit instructions to implement a synchronized statement in the Java programming language. The monitorenter and monitorexit instructions are not used in the implementation of synchronized methods

重点来了,上面这段介绍了两点:

  • 通过monitorenter和monitorexit指令来实现Java语言的同步代码块(后面有代码示例)

  • monitorenter和monitorexit指令没有被用在同步方法上!!!

4b8e59e6977987f9edf339bf9412c65c.png

2.2 同步方法的实现

先看下JVM规范里怎么说的
https://docs.oracle.com/javase/specs/jvms/se6/html/Compiling.doc.html#6530 (参考来源)

A synchronized method is not normally implemented using monitorenter and monitorexit. Rather, it is simply distinguished in the runtime constant pool by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the current thread acquires a monitor, invokes the method itself, and releases the monitor whether the method invocation completes normally or abruptly.

上面这段话主要讲了几点:

  • 同步方法的实现不是基于monitorenter和monitorexit指令来实现的

  • 在运行时常量池里通过ACC_SYNCHRONIZED来区分是否是同步方法,方法执行时会检查该标志

  • 当一个方法有这个标志的时候,进入的线程首先需要获得监视器才能执行该方法

  • 方法结束或者抛异常时会释放监视器

public class TestSyn {    private int i=0;    // 同步方法
    public synchronized void incer(){
        i++;
    }    // 同步代码块
    public  void decr(){
        synchronized (this) {
            i--;
        }
    }
}

可以通过反编译字节码来查看底层是怎么实现的

// 得到字节码javac TestSyn.java
// 反编译字节码javap -v TestSyn.class

同步代码块的反编译结果如下:

e4d7c54a614380159f2945b37ce63c19.png

同步方法的反编译结果如下:

92ee70d7c83b91f9c1ba3e95bc5e55b1.png

3.锁升级

3.1 Java对象头介绍

对象的内存布局

在我们常见的HotSpot虚拟机中对象由三部分组成,分别是对象头,实例数据,以及对齐填充位。其中对象头是跟锁信息相关的部分,在对象头里会存储该对象运行时数据,包括哈希吗,GC分代年龄,锁状态(无锁,偏向锁,轻量级锁,重量级锁),是否偏向锁,偏向线程ID等信息。
存储上述这些的区域叫做Mark Word(标记词),除了这部分对象头还有一部分区域用来存储类型指针,可以通过该类型指针来定位对象的元数据信息。下面重点看下,对象头的内存布局,因为这部分是跟我们这次相关的。

对象在内存中的表示如下图:

7f17ea17804cfffa12862515efcc7fd6.png

对象头的结构表示如下图:

b4d806ec7a887ae3c0abcdbfe446b55f.png

mark word的表示如下图:

abb416f3b8d31b971c6d87ce3250e57e.png

3.2 什么是锁升级

下面举个抢茅坑的例子来解释一下锁升级过程。

当只有一个线程访问时叫做偏向锁

假设我们每个厕所都有一把钥匙,要想使用厕所首先必须得获得锁。某天上午员工甲急急忙忙的打完卡上厕所了,并在厕所门上贴了 “工号007使用中”的标签,说明目前被工号007(相当于线程id)的员工占用呢,他再次向进入的时候只要上面的标签还显示工号007,他自己可以随便进入,不需要再次上锁了,有点偏向工号007员工的意思,所以这叫偏向锁。

发生竞争的时候升级成轻量级锁 (自旋等待)

员工甲正在使用厕所的时候,又来了两个人想用厕所,但发现厕所被人使用着呢,无法获得锁。所以只能在外面等着甲出来,他们等的过程叫做“自旋”,这个叫做轻量级锁。那么又有一个问题,当甲出来之后正等着的那两个人谁活得锁呢?有两种方式,按到达的顺序来排队或者不排队,这两种都可以实现,前者叫做公平锁,后者叫做非公平锁。

自旋等待没结果的时候升级成重量级锁

但那两个人自旋一段时间之后发现甲还没出来(JDK1.6规定为10次),一直这么等也不是个法子啊,所以打算向上升级,找厕所管理员(操作系统)反馈,升级成了重量级锁了

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。

9cebf37c16bc438f94793f2fb6a2ccde.png

锁升级过程中mark word的变化如下:

d0878c61aa6bc79e53b97aca9692ea62.png

偏向锁
偏向锁也是JDK 1.6中引入的一项锁优化, 引入它是为了优化在没有锁竞争场景下的锁消除。比如一段同步代码一直是由单个线程调用,在这种场景下就没必要使用同步锁了,这里指的同步锁不是指synchronized,而是说没不要到操作系统层面的互斥量了。
偏向锁的偏向是指该同步代码会一直偏向第一个调用它的线程,直到有别的线程过来竞争这把锁,在第一次调用同步代码并获得锁时会在对象头和栈帧锁记录行(Lock Record)里存储偏向线程Id,该线程在此进入的时候就不需要重新申请锁了。只需检测对象头的Mark Word里是否存储着指向该线程的ID即可。

直到又有线程来竞争这把锁的时候偏向锁会撤销偏向。

b3bb45a81f73e6d4e28ed4bd3e25e996.png

3aed366201a496d9abe307074a1d1f81.png

轻量级锁

轻量级锁是JDK 1.6之中加入的新型锁机制, 它名字中的“轻量级”是相对于使用操作系统
互斥量来实现的传统锁而言的, 因此传统的锁机制就称为“重量级”锁。 它并不是用来代替重量级锁的, 它的本意是在统的重量级锁使用操作系统互斥量产生的性能消耗。

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁.一直原地自旋,如果自旋数达到10次了则升级为重量级锁。

重量级锁
竞争的线程自旋一段时间未能获取锁之后会升级为重量级锁,这个时候锁的获取与释放都会由操作系统来分配了,如果持有锁的线程释放锁之后操作系统会唤醒所有阻塞的哪些线程,并进入新一轮的争抢模式,需要注意的是这些阻塞的线程没有获得锁的优先级,也就是说synchronized锁是非公平的。除此之外synchronized对中断操作也是无感的,不会因为被中断而放弃阻塞等待,它要么得到锁要么一直阻塞。


https://www.cnblogs.com/wyc1994666/p/11748212.html




TopTop