synchronized的理解

匿名 (未验证) 提交于 2019-12-02 23:49:02

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

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

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

 先写一段简单的代码: 
 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指令实现的。


实现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/64bit Mark Word 存储对象的hashCode或者锁信息或GC 分代年龄等
32/64bit Klass Pointer 存储到对象类型数据的指针
32/64bit ArrayLength 数组的长度(当当前对象是数组)

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

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

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

2、Monitor

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

特点:

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

monitorenter指令:

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

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唤醒的是其所在锁所阻塞的线程



参考:

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

Synchronized的那些事

文章来源: https://blog.csdn.net/ProMonkey_chen/article/details/96479285
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!