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