JUC源码解析: 深入解读 synchronize锁!
JUC源码解析: 深入解读 synchronize锁!
synchronize 是常用的锁,我们知道它可是用在方法上、代码块上,用法简单,但深入起来可要大费周章,下面跟着我深入一下吧。
synchronize 影响线程状态
在 Thread.Sate.class 源码中, 线程被定义为六种状态,(也可以更细分为七种, 详见我的另一篇博文 JUC源码讲解:线程状态转换)
线程进入 sync 时, 会进入 BLOCKED 状态, 而 local() 锁不会让线程进入 BLOCKED 状态, 这是一个简单的区别
退出 sync 时, 线程会进入 RUNNABLE 状态, 这个状态又可细分
RUNNABLE: 线程正在运行, 细分的两个状态会互相转换
- RUNNING: 线程拿到了CPU时间片, 使用yield() 或没有拿到CPU时间片会进入 READY状态
- READY: 线程没有拿到CPU时间片, 进入等待队列, 获取到时间片后会进入 RUNNING 状态
这就是 sync 对线程状态的影响了
synchronize 的三种使用方法
sync 有三种使用方式
- 在普通方法上
- 在静态方法上
- 在代码块上
那么,这样使用的时候,sync 是用什么作为锁的呢?
sync 可以使用在普通方法上:
public synchronized void syncMethod() {}
这时候,要想使用普通方法一定要new一个对象,它的锁是new的那个对象的锁
例如 new Main().syncMethod()
, 它使用的锁就是 new Main()
这个对象
sync 可以使用在静态方法上:
public static synchronized void staticSyncMethod() {}
它使用的锁, 是 类.class , 这个 class 在 jvm 中是唯一的
sync 可以使用在代码块上
public void method() {
synchronized (this) {
}
}
它锁是我们手动指定的
接下来,我们看看 jvm 里对于 sync锁是怎么标记的吧.
在资源管理器中找到我们写的这个java文件,使用 javap -v xxx.java
命令看看
像我这样,我是把结果输出到一个文件里了
javap -v Main.class >> ./1.txt
我先把输出贴出来,然后再大概讲解一下:
Classfile /D:/Code/Java/netty-study/SY-IM/target/classes/yihe/de/yy/Main.class
Last modified 2024年3月14日; size 683 bytes
SHA-256 checksum f32532942a1c25993483b3489e6400c45fa0e7922e391ab9251edbc7342637a6
Compiled from "Main.java"
public class yihe.de.yy.Main
minor version: 0
major version: 51
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #2 // yihe/de/yy/Main
super_class: #3 // java/lang/Object
interfaces: 0, fields: 0, methods: 5, attributes: 1
Constant pool:
#1 = Methodref #3.#24 // java/lang/Object."<init>":()V
#2 = Class #25 // yihe/de/yy/Main
#3 = Class #26 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Lyihe/de/yy/Main;
#11 = Utf8 syncMethod
#12 = Utf8 staticSyncMethod
#13 = Utf8 method
#14 = Utf8 StackMapTable
#15 = Class #25 // yihe/de/yy/Main
#16 = Class #26 // java/lang/Object
#17 = Class #27 // java/lang/Throwable
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 args
#21 = Utf8 [Ljava/lang/String;
#22 = Utf8 SourceFile
#23 = Utf8 Main.java
#24 = NameAndType #4:#5 // "<init>":()V
#25 = Utf8 yihe/de/yy/Main
#26 = Utf8 java/lang/Object
#27 = Utf8 java/lang/Throwable
{
public yihe.de.yy.Main();
descriptor: ()V
flags: (0x0001) 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 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lyihe/de/yy/Main;
public synchronized void syncMethod();
descriptor: ()V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 13: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lyihe/de/yy/Main;
public static synchronized void staticSyncMethod();
descriptor: ()V
flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 15: 0
public void method();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_1
5: monitorexit
6: goto 14
9: astore_2
10: aload_1
11: monitorexit
12: aload_2
13: athrow
14: return
Exception table:
from to target type
4 6 9 any
9 12 9 any
LineNumberTable:
line 18: 0
line 20: 4
line 21: 14
LocalVariableTable:
Start Length Slot Name Signature
0 15 0 this Lyihe/de/yy/Main;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 9
locals = [ class yihe/de/yy/Main, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 26: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 args [Ljava/lang/String;
}
SourceFile: "Main.java"
先看看普通方法 syncMethod() 在 jvm 中是怎么标记 sync 的:
再看看静态方法 staticSyncMethod() 在jvm 中是怎么标记 sync 的:
和普通方法差不多
再看看在代码块中是怎么标记 sync 的:
找到 sync 所在的 method() 方法,我们看!
使用 monitorenter 标记 sync 的入口, monitorexit 标记 sync 的出口.
有两个 monitorexit , 是为了防止在发生异常后不能释放锁
这就是对sync三种用法的说明了
synchronize 的四种特性
sync有三种特性, 我做一下讲解:
有序性: 读读, 读写, 写读, 写写 互斥
- sync 让一个线程对其他线程完全互斥,以保障线程间的有序性
可见性: sync 的可见性体现在两个方面
- 锁状态对其他线程可见: 上一节提到过, 在 jvm 中,通过标记 ACC_SYNCHRONIZE, monitorenter, monitorexit 来保证锁状态对其他线程是可见的
- 变量的可见, sync 在释放锁之前会将变量的修改刷新到共享内存中, 是可见的
原子性: 因为对其他线程的完全互斥, 保障了原子性
可重入性: sync 可以嵌套
synchronize 的 Mark Word 锁标记
synchronize 有好几种状态, 在 32位虚拟机中是这样标记的
- 无锁状态: 锁标志位 01, 是否为偏向锁 0
- 偏向锁: 锁标志位 01(和无锁状态相同), 是否为偏向锁 1
- 偏向锁里会存储偏向线程的id
- 轻量级锁: 锁标志位 00
- 重量级锁: 锁标志位 10
- GC标记: 锁标志位 11 (要被GC回收了,会有这个标记)
如图: 着重看 "1bit是否为偏向锁" 和 "2bit锁标志位" 两个参数就好
看偏向锁这一栏, 没有地方存指针,而是存了 线程ID 和 Epoch , 这一点尤为关键, 意味着偏向锁不能存储对象头的 hashCode, 而其他锁是有地方存的. 也就意味着, 当锁对象被隐式(父类)或显试调用了 hashcode 时, 就不能进入偏向锁! , 之后我会在锁升级的章节中着重介绍