synchronized的理解

       synchronized是Java内置关键字,提供了一种独占加锁方式。它可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。但是,synchronized也存在一定的局限性。

1)当线程尝试获取锁的时候,如果获取不到会一直阻塞。

2)如果持有锁的线程进入阻塞或者休眠时,其他线程尝试获取锁时必须一直等待,除非当前持有锁的线程发生异常。

在Java中每个对象都可以作为锁,主要如下:

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

    2)静态同步方法,锁是当前类的class对象

    3)同步方法快,锁是括号里面的对象

先写一段简单的代码:
public class Main {

    public static void main(String[] args) {
        synchronized (Main.class){

        }
    }

}
利用javap命令查看生成class文件来进行synchronized的实现。
javap -v Main.class
查看结果:
public class com.entity.Main
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#22         // java/lang/Object."<init>":()V
   #2 = Class              #23            // com/entity/Main
   #3 = Class              #24            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               LocalVariableTable
   #9 = Utf8               this
  #10 = Utf8               Lcom/entity/Main;
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               args
  #14 = Utf8               [Ljava/lang/String;
  #15 = Utf8               StackMapTable
  #16 = Class              #14            // "[Ljava/lang/String;"
  #17 = Class              #24            // java/lang/Object
  #18 = Class              #25            // java/lang/Throwable
  #19 = Utf8               MethodParameters
  #20 = Utf8               SourceFile
  #21 = Utf8               Main.java
  #22 = NameAndType        #4:#5          // "<init>":()V
  #23 = Utf8               com/entity/Main
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/Throwable
{
  public com.jv.catalog.entity.Main();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/entity/Main;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // class com/entity/Main
         2: dup
         3: astore_1
         4: monitorenter
         5: aload_1
         6: monitorexit
         7: goto          15
        10: astore_2
        11: aload_1
        12: monitorexit
        13: aload_2
        14: athrow
        15: return
      Exception table:
         from    to  target type
             5     7    10   any
            10    13    10   any
      LineNumberTable:
        line 6: 0
        line 8: 5
        line 9: 15
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  args   [Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 10
          locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4
    MethodParameters:
      Name                           Flags
      args
}
SourceFile: "Main.java"

通过查看的结果来可以看出,同步代码块是使用monitorenter和monitorexit指令实现的。

       指令monitorenter插入到同步代码块的开始位置,指令monitorexit插入到同步代码块的结束位置。JVM需要保证每一个monitorenter都会有一个与之对应的monitorexit。任何对象都会有一个Monitor与之关联一起,并且当且一个Monitor被持有之后,它会处在锁定的状态。在线程执行到monitorenter指令的时候,将会去尝试获取对象对应的Monitor的所有权(也就是对象锁)。

       观察上面会发现有两个monitorexit,为什么会有两个monitorexit呢,一个是正常执行办法后的monitorexit,一个是发生异常后的monitorexit。


实现synchronized的基础是Java对象头和Monitor,下面对这两个概念进行介绍。

补充:

在JVM中,对象在内存中的布局分成三块区域:对象头、示例数据和对齐填充。

对象头: 对象头主要存储对象的hashCode、锁信息、类型指针、数组长度(若是数组的话)等信息。

示例数据:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组长度,这部分内存按4字节对齐。

填充数据:由于JVM要求对象起始地址必须是8字节的整数倍,当不满足8字节时会自动填充。

1、Java对象头

Java对象头中可以存放synchronized用的锁。jvm的对象头主要由标记字段(Mark Word)和类型指针(Klass Pointer)两部分数据组成。

标记字段(Mark Word)是用于存储对象自身的运行时数据,如GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳、hashcode等。

类型指针(Klass Pointer)是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

JVM中对象头的方式有以下两种(以32位JVM为例):

1)普通对象

|-----------------------------------------------------------------|
|                     Object Header (64 bits)                     |
|------------------------------------|----------------------------|
|        Mark Word (32 bits)         |    Klass Pointer (32 bits) |
|------------------------------------|----------------------------|

2)数组对象

|---------------------------------------------------------------------------------|
|                                 Object Header (96 bits)                         |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(32bits)       | Klass Pointer(32bits) |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|

 

长度内容说明
32/64bitMark Word存储对象的hashCode或者锁信息或GC 分代年龄等
32/64bitKlass Pointer存储到对象类型数据的指针
32/64bitArrayLength数组的长度(当当前对象是数组)

synchcronized的锁是存放在Java对象头中的

如果对象是数组类型,JVM用3个子宽(Word)存储对象头,否则是用2个子宽

在32位虚拟机中,1子宽等于4个字节,即32bit;64位的话就是8个字节,即64bit

2、Monitor

Monitor其实是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象。

特点:

      1)互斥:同一时刻一个Monitor锁只能被一个线程占用,其他线程无法占用。

      2)信号机制(singal):占用Monitor锁失败的线程会暂时放弃并且等待某个位于成真(条件变量),但该条件成立后,当前线程会通过释放锁通知正在等待这个条件变量的其他线程,让其可以重新竞争锁。

  • 每个对象都有一个监视器,在同步代码块中,JVM通过monitorenter和monitorexist指令实现同步锁的获取和释放功能
  • 当一个线程获取同步锁时,即是通过获取monitor监视器进而等价为获取到锁
  • monitor的实现类似于操作系统中的管程

monitorenter指令:

       每个对象都有一个monitor。当该monitor被占用时(锁定状态或者说获取监视器即是获得同步锁)。线程执行monitorenter指令时会尝试获取监视器的所有权,过程如下:

  • 若该monitor的进入次数为0,则该线程进入monitor并将进入次数设置为1,此时该线程即为该monitor的所有者。
  • 若线程已经占有该monitor并重入,则进入次数+1
  • 若其他线程已经占有该monitor,则线程会被阻塞直到monitor的进入次数为0,之后线程间会竞争获取该monitor的所有权
  • 只有首先获得锁的线程才能允许继续获取多个锁

monitorexit指令:

   执行monitorexit指令将遵循以下步骤:

  • 执行monitorexit指令的线程必须是对象实例所对应的monitor的所有者。
  • 指令执行时,线程会先将进入次数-1,若-1之后进入次数变成0,则线程退出monitor(即释放锁)。
  • 其他阻塞在该monitor的线程可以重新竞争该monitor的所有权。

补充:

Mesa派的signal机制

  • Mesa派的signal机制又称"Non-Blocking condition variable"
  • 占有Monitor锁的线程发出释放通知时,不会立即失去锁,而是让其他线程等待在队列中,重新竞争锁
  • 这种机制里,等待者拿到锁后不能确定在这个时间差里是否有别的等待者进入过Monitor,因此不能保证谓词一定为真,所以对条件的判断必须使用while
  • Java中采用就是Mesa派的singal机制,即所谓的notify(这也是执行notify随机唤醒一个线程的原因所在)

注:notify唤醒的是其所在锁所阻塞的线程

下面在介绍一下Monitor Record(MR),它是Java线程私有的数据结构。它的结构以及对应功能描述如下:

MR描述
Owner

1)初始值为null,若该值为null则表示没有任何线程拥有该Monitor。

2)若线程成功拥有锁(Monitor record)后保存线程唯一标识,当锁被释放时又设置为null。

EntryQ关联一个系统互斥锁(semaphore),阻塞所有竞争该锁(monitor record)失败的线程。
ReThis阻塞或等待在该锁(monitor record)的线程个数-----被阻塞/等待的线程被存入同步/等待队列中。
Nest记录冲入次数。
Hashcode保存从对象头拷贝过来的hashcode值(可能还包含GC age)。
Candidate

1)用来避免不必要的阻塞或等待线程唤醒。

2)只有两种可能的值,0表示没有需要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。

      原因:因为每一次只有一个线程能够成功拥有锁,若每次前一个释放锁的线程唤醒所有正在阻塞或等待的线。程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争失败又被阻塞)从而导致性能严重下降。

  • 每一个线程都有一个可用的MR列表,同时还有一个全局的可用列表。
  • 一个被锁住的对象都会和一个MR关联(对象头的MarkWord中的LockWord指向MR的起始地址)。
  • MR中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

MR的工作机制示意图如下:

  • 获得监视锁成功的线程,将成为该监视锁对象的拥有者
  • 在同一时刻,监视器对象只属于一个活动线程(Owner)
  • 拥有者可以调用wait方法自动释放监视锁,进入等待状态

 

参考:

        Java 8 并发篇 - 冷静分析 Synchronized(下)

        Synchronized的那些事

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值