Synchonrized介绍

目录

前提知识

Synchronized介绍

Synchronized使用场景

synchronized底层原理

Synchronized的可重入的实现机制

Monitor监视器锁

什么是monitor

对象布局

Synchronized的8锁现象


前提知识

多线程编程中,有可能会出现多个线程同时访问同一个共享变量,可变资源的情况,这个资源我们称之为临界资源;这种资源对象可能是:对象,变量,文件等.

  • 共享:资源可以由多个线程同时访问
  • 可变:资源可以在其生命周期内被修改

引出的问题:

  • 由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问!

如何解决线程并发安全问题

  • 实际上,所有的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源.即在同一时刻,只能有一个线程访问临界资源,也称之为同步互斥访问.
  • Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock

Synchronized介绍

  • 内置锁,隐式锁(加锁和解锁看不见)
  • Synchronized方法不会被继承,需要在子类中重新指定

执行的流程:可点击链接放大后查看清晰图片

注意:等待轻量级锁的进程不会被阻塞,会一直自旋的等待也是一个自旋锁,不会被挂起,而是自旋

若干个自旋之后还没有获取到锁,就会被挂起,获得锁就执行代码

详细的流程图:

Synchronized使用场景

我们正常去使用Synchronized一般都是用在下面这几种场景:

  • 修饰实例方法:对当前实例对象this加锁
public class Synchronized {
  public synchronized void husband(){

  }
}
  • 修饰静态方法:对当前类的Class对象加锁
public class Synchronized {
  public static synchronized void husband(){
      
  }
}
  • 修饰代码块,指定一个加锁的对象,给对象加锁
public class Synchronized {
  public void husband(){
      Object object = new Object()
      synchronized(object ){
       }
  }
}

synchronized底层原理

  • synchronized内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问
  •  synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,他是一个重量级锁性能较低.当然,JVM内置锁在1.6之后发生了重大的优化,如锁粗化,锁消除,轻量级锁,偏向锁,适应性自旋等技术来减少锁操作的开销,内置锁的并发性基本能于Lock持平
  • Synchronized关键字被编译成字节码后会被翻译成monitorenter和monitorexit两条指令分别在同步代码块逻辑代码的起始位置与结束位置

  •  每个同步对象都有一个自己的Monitor(监视器锁),加锁过程如下

  •  Synchronized修饰的代码块反编译之后会多出来一个monitorexit:异常的时候要保证彻底的释放锁和退出

  • 修饰方法时反编译后方法的修饰符会多一个SYS_SYNCHRONIZED(表示当前方法需要加锁),JVM在执行这个方法时,如果碰到了这个标识,在进入这个方法之前,会给当前方法加上锁 

Synchronized的可重入的实现机制

        当执行monitorenter时,如果目标锁对象的计数器为零,那么说明他没有被其他线程所持有,java虚拟机会将该所对象的持有线程设置为当前线程,并且将其计数器加1,在目标对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁,当执行monitorexit时,java虚拟机则需要将锁对象的计数器减1,计数器为0代表锁已被释放.线程不再是monitor的拥有者,,其他被monitor阻塞的线程可以尝试获取这个monitor的所有权

        通过上面两段描述,Synchronized的语义底层是通过一个monitor的对象来完成,wait和notify等方法也依赖于monitor对象(这也是为什么只有在同步的块或者方法里才能调用wait和notify,否则会抛出java.lang.IIIegalMonitorStateException)

        方法的同步:相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标识符,JVM就是根据标识符来实现方法的同步的:

        方法被调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功后才能执行方法体,方法执行结束后才能释放monitor.在方法执行期间,其他任何线程都无法再获得同一个monitor对象

        两个指令的执行时JVM通过调用操作系统的互斥源语mutex来实现的,被阻塞的线程会被挂起,等待重新调度,会导致"用户态和内核态"两个态来回切换,对性能有较大影响

Monitor监视器锁

        任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,他将处于锁定状态.Synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然实现细节不一样,但是都可以通过MonitorEnter和MonitorExit指令来实现.

  • MonitorEnter:每个对象都是一个监视器锁(Monitor),当Monitor占用时就会处于锁定状态,线程执行MonitorEnter指令时尝试获取Monitor的所有权,过程如下:
    • a.如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,改进程为Monitor的所有者
    • b.如果线程已经占有该Monitor,只是重新进入,则进入Monitor的进入数加1
    • c.如果其他线程已经占用了monitor,则线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权
  • Monitorexit:执行Monitor的线程必须是Monitorenter对应的所有者.指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个Monitor的所有者,其他被这个Monitor阻塞的线程可以尝试获取获取这个monitor的所有权.Monitorexit出现了两次,第一步为同步正常退出时释放锁,第二次为发生异步退出时释放锁

通过上面的描述,我们应该能很清楚的看出synchronized的实现原理,synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出lllegalMonitorStateException异常的原因.

        从编译结果上看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上也可以通过这两条指令来实现),不过相对于普通方法,其常量池中对了ACC_SYNCHRONZIED标识符,JVM就是根据该标识符来实现方法的同步: 方法调用时,调用指令将会检查ACC_SYNCHRONZIED访问标志是否被设置,如果设置了,执行线程将会获取monitor.获取成功后才能执行方法体,方法释放完后再释放monitor,在方法执行期间,其他任何线程都无法在获得同一个monitor对象

        两种同步方式本质上没有任何区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成.两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起,等待重新调度,会导致"用户态和内核态"两个态之间来回切换,对性能有较大影响.

什么是monitor

         可以把Monitor理解为一个同步工具,也可以描述为一种同步机制,它通常被描述成一个对象,与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有可能成为Monitor的潜质,因为在Java的设计中,每一个Java对象自打new出来就带了一把看不见的锁,他交做内部锁或者Monitor锁,也就是通常所说的Synchronzied对象锁,MarkWord锁标识为10,其中指针指向的是Monitor对象的起始地址.在Java虚拟机中(HotSpot中)Monitor是由ObjectMonitor实现的,其主要数据结果如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):

         objectMonitor中有两个队列,_WaitSet _EntrySet用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象), _owner指向持有ObjectMnitor对象的线程,当多个线程同时访问一段同步代码时:

  1. 首先会进入_Entrylist集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
  2. 若线程调用wait()方法,将释放当前持有的monitor,owner变量恢复为null.count自减1,同时该线程进入WaitSet集合中等待被唤醒;
  3. 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入monitor(锁)

同时,Monitor对象存在于每个Java对象的对象头Mark Word(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用.监视器Monitor有两种同步方式:互斥与协作.多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问.

我们知道synchronzied加锁加在对象上,对象是如何记录锁的状态呢??答案是锁状态是被记录在每个对象的对象头(Mark Word)中

对象布局

        关于对象布局在JVM的章节中也会介绍到

        HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header),实例数据(Instance Data),对齐填充(Padding0)

  • 对象头: 包括两部分信息,第一部分是"Mark Word",用于存储对象自身的运行时数据,比如Hash码,对象所属的年代,对象锁,锁状态信息,偏向锁(线程)ID,偏向时间等,它是实现轻量级和偏向锁的关键.这部分数据的长度在32位和64位的虚拟机中分别为32和64个Bits,官方称之为"Mark Word".对象需要存储的运行时数据很多,其实已经超过了32,64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,他会根据对象的状态复用自己的存储空间,例如在32位的HotSpot虚拟机中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志位,1Bit固定为0,在其他状态(轻量级锁定,重量级锁定,GC标记,可偏向)下对象的存储内容下表所示.
    • Java对象头一般占有两个机器码(在32为虚拟机中,1个机器码等于4个字节,也就是32bit,在64为虚拟机中,1个机器码也就是8个字节也就是64bit),
    • 但是如果对象是数组类型的,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,单数无法从数据的元数据来确认数组的长度,所以用一块来记录数据长度
  • 实例数据:存放类的属性数据信息;包括父类的属性信息
  • 对齐填充:由于虚拟机要求对象的大小必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐

结构图:

对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,他会根据对象的状态复用自己的存储空间也就是说: Mark Word(4字节)会随着锁的变化而更新内部信息:

  • 轻量级锁:2位为锁标志位,其余30位都是指向栈中锁记录的指针
  • 重量级锁:2位为锁标志位,其余30位都是指向互斥量(重量级)的指针
  • 由图可知:对象的hashCode在轻量级锁,重量级锁和偏向锁三种状态下hashcode记录在哪
    • 偏向锁不存储对象的hashCode,如果此时为偏向锁,调用对象的hashCode,实际上会调用系统延时计算hashCode的一个方法,会导致偏向锁升级成轻量级锁.
    • 轻量级锁将对象的hashCode记录在线程栈的Replace lock Record中,同时会将原先的偏向锁降级(并不是真的降级)为无锁状态(能够保存对象的hashCode)并保存该无锁状态的MarkWord保存到线程栈的Replace lock Record中,轻量级的30位指针会指向这个栈中保存到MarkWord.同时线程栈的Replace lock Record中保存的MarkWord里面的指针也会指向轻量级锁的MarkWord.
    • 重量级锁的hashcode记录在monitor中

public static void main(String[] args) {
    try {
        TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    Object o = new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
    synchronized (o){
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
    //输出hashcode
    System.out.println(o.hashCode());
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
    synchronized (o){
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

//打印结果(1):未加锁(匿名偏向,可偏向状态):已经预备可以偏向,线程可以来拿
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

//打印结果(2):偏向锁
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 40 11 05 (00000101 01000000 00010001 00000101) (85016581)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

//打印的hashcode: 517380410
//打印结果(3):偏向锁状态调用hashcode方法后,锁状态为无锁状态(信息会被保存在轻量级锁的栈中)
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 3a 99 d6 (00000001 00111010 10011001 11010110) (-694601215)
      4     4        (object header)                           1e 00 00 00 (00011110 00000000 00000000 00000000) (30)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

//打印结果(4):表现为轻量级锁
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           f0 f5 ca 04 (11110000 11110101 11001010 00000100) (80410096)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
//打印对象内存区域的一个依赖
<dependency>
	<groupId>org.openjdk.jol</groupId>
	<artifactId>jol-core</artifactId>
	<version>0.10</version>
</dependency>
    
//调用方法
ClassLayout.parseInstance(对象名).toPrintable();



public static void main(String[] args) {
    Object o = new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
//打印的结果

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

//Mark Word的信息
//操作系统分为:大端模式和小端模式,下面为小端模式应该改为大端模式
00000001 00000000 00000000 00000000  ==>  00000000 00000000 00000000 00000001 
锁标志位:001(无锁状态)
hashcode为懒加载模式,一开始的时候是没有的


Object o = new Object();
synchronized (o){
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

//打印信息
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           b8 f6 11 05 (10111000 11110110 00010001 00000101) (85063352)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

10111000 11110110 00010001 00000101  ==>      00000101  00010001 11110110  10111000
锁标志位:00(轻量级锁状态) ????为什么会是轻量级状态,根据前面的锁膨胀,不是应该是偏向锁吗

解释说明: JVM会延迟启动偏向锁:JVM本身自己会依赖大量的hashmap以及class类,这些也存在大量的同步代码块,内部也会启动多个线程,内部存在竞争,为了避免出现偏向锁->轻量级锁->重量级锁的升级的过程,延迟使用轻量级锁(如果想要看到准确的信息,需要延迟5秒)

public static void main(String[] args) {
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        synchronized (o){
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
//两次的打印结果
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 40 2a 05 (00000101 01000000 00101010 00000101) (86654981)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

延迟5s后开启偏向锁
//未加锁(匿名偏向,可偏向状态):已经预备可以偏向,线程可以来拿
00000101 00000000 00000000 00000000 ==>    00000000 00000000 00000000  00000101 
//加完锁(一旦有线程进入,马上偏向当前线程)
00000101 01000000 00101010 00000101 ==>    00000101 00101010 01000000  00000101 

Synchronized的8锁现象

        当一个线程试图去访问同步代码块时,它首先必须获得锁,退出或者抛出异常时释放锁.
也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后,才能获取锁;可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以,不需要等待该实例对象已获取所得非静态同步方法释放锁就可以获取他们自己的锁.

所有的静态同步方法用的也是一把锁-类本身
        这两把锁是不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞争条件的.
但是一但一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后,才能获取锁

注意:是不是同一把锁(非静态同步方法锁的是调用者谁调用这个方法,静态同步方法锁的是类本身,主要区分这两点,就不会判断错误)

具体的代码实例可查看synchronized 八锁现象,搞懂 synchronized 的使用问题_小熊-CSDN博客 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值