本篇章是对
Java
并发中的synchronized
关键字进行深入探索。🚀本篇章代码 Demo
# 序言
- 线程安全是并发编程中的重要关注点,应该注意的是,造成线程安全问题的主要原因有以下两点:
- 1️⃣ 是存在共享数据 (临界资源)。
- 2️⃣ 存在多条线程共同操作共享数据。
- 为了解决这个问题,我们需要这样一个方案,当存在多个线程操作共享数据时,需要保证同一个时刻有且只有一个线程在操作共享数据,其它线程必须等到该线程处理完数据后再进行。
- 这种方式的名称叫
互斥锁
,即能达到互斥访问目的的锁,也就说当一个共享数据被当前正在访问的线程加上互斥锁
后,在同一个时刻,其它线程只能处于等待状态,直到当前线程处理完数据释放该锁。 - 在
Java
中关键字synchronized
可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块 (主要是对方法或者代码块中存在共享数据的操作)。 - 同时我们还应该注意到
synchronized
另外一个重要的作用,那就是synchronized
可以保证一个线程的变化 (共享数据的变化) 被其它线程所看到 (保证可见性,完全可以替代volatile
关键字),这点也是很重要。
# synchronized 的应用
synchronized
关键字主要有以下3
种应用方式:
# synchronized 修饰实例方法
所谓实例对象锁就是用
synchronized
修饰实例对象中的实例方法 (实例方法不包括静态方法),代码如下:
package top.rem.rain.synchronized_example; | |
/** | |
* @Author: LightRain | |
* @Description: synchronized 修饰实例方法 | |
* @DateTime: 2023-12-15 23:02 | |
* @Version:1.0 | |
**/ | |
public class SynchronizedData implements Runnable { | |
/** | |
* 共享资源 (临界资源) | |
*/ | |
private static int i = 0; | |
@Override | |
public synchronized void run() { | |
for (int j = 0; j < 1000000; j++) { | |
incremental(); | |
} | |
} | |
/** | |
* 递增方法 | |
*/ | |
private synchronized void incremental() { | |
i++; | |
} | |
public static void main(String[] args) throws InterruptedException { | |
SynchronizedData instance = new SynchronizedData(); | |
Thread t1 = new Thread(instance); | |
Thread t2 = new Thread(instance); | |
// 开始 | |
t1.start(); | |
t2.start(); | |
t1.join(); | |
t2.join(); | |
System.out.println(i); | |
/* | |
执行结果: 2000000 | |
*/ | |
} | |
} |
在上述代码中,我们开启了两个线程操作同一个共享资源 (即:变量
i
), 由于i++
并不是原子性操作,该操作是先读取值然后写回一个新值,相当于原来的值加上1
,分两步完成。如果第二个线程在第一个线程读取旧值和写会新值期间读取i
的阈值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1
操作,这也就造成了线程安全失败。因此对于incremental()
方法必须使用synchronized
来修饰,以便保证线程安全。此时我们应该注意到synchronized
修饰的是实例方法incremental()
, 在这样的情况下,当前线程的锁便是实例对象incremental
。
注意:在
Java
中线程同步锁可以是任意对象,从代码执行结果来看确实是正确的,倘若我们没有使用synchronized
关键字,其最终执行结果大概率是小于2000000
的,这便是synchronized
关键字的作用,这里我们还需要意识到,当一个线程正在访问一个对象的synchronized
实例方法时,那么其它线程不能访问该对象的其它synchronized
方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其它线程将无法再次获取该对象的锁,所以无法访问该对象的其它synchronized
实例方法,但是其它线程还是可以访问该实例对象的其它非synchronized
方法。
如果:一个线程
A
需要访问实例对象object1
的synchronized
的a1
方法 (当前对象锁是object1
),另一个线程B
需要访问实例对象object2
的synchronized
的a2
方法 (当前对象锁是object2
),这样是允许的,以为两个实例对象的锁并不相同,此时人工两个线程操作数据并非共享数据,此时线程是安全有保障的,遗憾的是如果两个线程操作的是共享数据那么线程安全就很有可能无法保证,下面是代码示例:
package top.rem.rain.synchronized_example; | |
/** | |
* @Author: LightRain | |
* @Description: 非线程安全使用共享数据 | |
* @DateTime: 2023-12-15 23:02 | |
* @Version:1.0 | |
**/ | |
public class SynchronizedDataError implements Runnable { | |
/** | |
* 共享资源 (临界资源) | |
*/ | |
private static int i = 0; | |
@Override | |
public void run() { | |
for (int j = 0; j < 1000000; j++) { | |
incremental(); | |
} | |
} | |
/** | |
* 递增方法 | |
*/ | |
private synchronized void incremental() { | |
i++; | |
} | |
public static void main(String[] args) throws InterruptedException { | |
// 创建新实例 | |
Thread t1=new Thread(new SynchronizedDataError()); | |
// 创建新实例 | |
Thread t2=new Thread(new SynchronizedDataError()); | |
// 开始线程 | |
t1.start(); | |
t2.start(); | |
//join 含义:当前线程 A 等待 thread 线程终止之后才能从 thread.join () 返回 | |
t1.join(); | |
t2.join(); | |
System.out.println(i); | |
/* | |
执行结果: 1658464 | |
每次执行的数值是随机的并不是固定的数值,这就是非线程安全操作共享数据 | |
*/ | |
} | |
} |
上述代码与前面的不同的是我们创建了两个新实例的
SynchronizedDataError
线程对象,然后启动两个不同的线程对共享数据变量i
进行操作,遗憾的是执行结果不是期望的2000000
,因此上述代码犯了严重的错误,虽然我们使用synchronized
修饰了incremental()
方法,但是创建的是两个不同的实例对象,这也就以为着存在着两个不同实例的对象锁,因此t1
和t2
都会进入各自的对象锁中,就是说t1
和t2
线程使用的是不同的锁,因而无法保证线程安全,解决这个问题的方式是将synchronized
作用于静态的incremental()
方法上,这样一来的话,对象锁就是当前类对象,由于无论创建多少个实例对象,但对于类对象拥有只有一个,所有在这样的情况下对象锁就是唯一的,修改后的代码如下:
package top.rem.rain.synchronized_example; | |
/** | |
* @Author: LightRain | |
* @Description: 线程安全使用共享数据 | |
* @DateTime: 2023-12-15 23:02 | |
* @Version:1.0 | |
**/ | |
public class SynchronizedDataError implements Runnable { | |
/** | |
* 共享资源 (临界资源) | |
*/ | |
private static int i = 0; | |
@Override | |
public void run() { | |
for (int j = 0; j < 1000000; j++) { | |
incremental(); | |
} | |
} | |
/** | |
* 递增方法 | |
*/ | |
private synchronized static void incremental() { | |
i++; | |
} | |
public static void main(String[] args) throws InterruptedException { | |
// 创建新实例 | |
Thread t1=new Thread(new SynchronizedDataError()); | |
// 创建新实例 | |
Thread t2=new Thread(new SynchronizedDataError()); | |
// 开始线程 | |
t1.start(); | |
t2.start(); | |
//join 含义:当前线程 A 等待 thread 线程终止之后才能从 thread.join () 返回 | |
t1.join(); | |
t2.join(); | |
System.out.println(i); | |
/* | |
执行结果: 2000000 | |
将 incremental () 方法修饰为 static 后就可以做到线程安全的了 | |
*/ | |
} | |
} |
# synchronized 修饰静态方法
当
synchronized
作用于静态方法时,其锁就是当前类的class
对象锁,由于静态成员不专属于任何一个实例对象,属于类成员,因此通过class
对象锁就可以控制静态成员的并发操作。需要注意的是如果:一个线程A
调用一个实例对象的非static synchronized
方法,而线程B
需要调用这个实例对象所属类的静态synchronized
方法,这样是允许的,不会发生互斥现象,因为访问静态synchronized
方法占用的锁是当前类的class
对象,而访问非静态synchronized
方法占用的锁是当前实例对象锁,示例代码如下:
package top.rem.rain.synchronized_example; | |
/** | |
* @Author: LightRain | |
* @Description: synchronized 修饰静态方法 | |
* @DateTime: 2023-12-16 12:33 | |
* @Version:1.0 | |
**/ | |
public class SynchronizedDataClass implements Runnable { | |
/** | |
* 共享资源 (临界资源) | |
*/ | |
private static int i = 0; | |
@Override | |
public void run() { | |
for (int j = 0; j < 1000000; j++) { | |
staticIncremental(); | |
} | |
} | |
/** | |
* 作用于静态方法,锁是当前 class 对象,也就是 | |
* SynchronizedDataClass 类对应的 class 对象 | |
*/ | |
public static synchronized void staticIncremental() { | |
i++; | |
} | |
/** | |
* 非静态,访问时锁不一样不会发生互斥 | |
*/ | |
public synchronized void incremental() { | |
i++; | |
} | |
public static void main(String[] args) throws InterruptedException { | |
//new 新实例 | |
Thread t1 = new Thread(new SynchronizedDataClass()); | |
Thread t2 = new Thread(new SynchronizedDataClass()); | |
// 启动线程 | |
t1.start(); | |
t2.start(); | |
t1.join(); | |
t2.join(); | |
System.out.println(i); | |
/* | |
执行结果:2000000 | |
*/ | |
} | |
} |
由于
synchronized
关键字修饰的是静态staticIncremental()
方法,与修饰实例方法不同的是,其锁对象是当前类的class
对象,注意代码中的incremental()
方法是实例方法,其对象锁是当前实例对象,如果别的线程调用该方法,将不会产生互斥现象,毕竟锁对象不同,但我们应该意识到这种情况下可能会发现线程安全问题 (操作共享资源)。
# synchronized 同步代码块
除了使用
synchronized
关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,就会非常耗时,这种情况我们就可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,示例代码如下:
package top.rem.rain.synchronized_example; | |
/** | |
* @Author: LightRain | |
* @Description: synchronized 同步代码块 | |
* @DateTime: 2023-12-15 23:02 | |
* @Version:1.0 | |
**/ | |
public class SynchronizedCodeBlock implements Runnable { | |
private static final SynchronizedCodeBlock INSTANCE = new SynchronizedCodeBlock(); | |
/** | |
* 共享资源 (临界资源) | |
*/ | |
private static int i = 0; | |
/** | |
* synchronized 同步代码块,锁对象是 INSTANCE | |
*/ | |
@Override | |
public void run() { | |
synchronized (INSTANCE){ | |
for (int j = 0; j < 1000000; j++) { | |
i++; | |
} | |
} | |
} | |
public static void main(String[] args) throws InterruptedException { | |
Thread t1 = new Thread(INSTANCE); | |
Thread t2 = new Thread(INSTANCE); | |
// 开始 | |
t1.start(); | |
t2.start(); | |
t1.join(); | |
t2.join(); | |
System.out.println(i); | |
/* | |
执行结果: 2000000 | |
*/ | |
} | |
} |
从上面代码中可以看出,将
synchronized
作用于一个给定的实例对象INSTANCE
,即当前实例对象就是锁对象,,每次当线程进入synchronized
包裹的代码块时就会要求当前线程持有INSTANCE
实例的对象锁,如果当前有其它线程持有该对象锁,那么新到的线程就必须等待,这样就保证了每次只有一个线程执行i++
操作。当然除了INSTANCE
作为对象外,我们还可以使用this
对象 (代表当前实例) 或者当前类的class
对象作为锁,示例代码如下:
package top.rem.rain.synchronized_example; | |
/** | |
* @Author: LightRain | |
* @Description: synchronized 同步代码块 this | |
* @DateTime: 2023-12-15 23:02 | |
* @Version:1.0 | |
**/ | |
public class SynchronizedCodeBlockThis implements Runnable { | |
/** | |
* 共享资源 (临界资源) | |
*/ | |
private static int i = 0; | |
@Override | |
public void run() { | |
//this: 当前实例对象锁 | |
synchronized (this){ | |
for (int j = 0; j < 1000000; j++) { | |
i++; | |
} | |
} | |
//class 对象锁 | |
synchronized (SynchronizedCodeBlockThis.class){ | |
for (int j = 0; j < 1000000; j++) { | |
i++; | |
} | |
} | |
} | |
} |
了解完了
synchronized
的基本含义和使用方式后,下面将进一步深入synchronized
的底层实现原理。
# synchronized 底层语义
Java
虚拟机中的同步 (Synchronization
) 基于进入和退出管程 (Monitor
) 对象实现,无论是显示同步还是隐式同步都是如此。- 显示同步是指有明确的
monitorenter
和monitorexit
指令,即:同步代码块。
- 显示同步是指有明确的
- 在
Java
中,synchronized
用的最多的地方就是被synchronized
修饰的同步方法。 - 同步方法并不是由
monitorenter
和monitorexit
指令来实现同步的,而是由方法调用指令读取运行时常量池中的ACC_SYNCHRONIZED
标志来隐式实现的。 - 下面先来了解一下
Java
对象头概念,这对理解synchronized
实现原理很重要。
# Java 对象头与 Monitor
JVM
中对象在内存里分布为三块区域:对象头、实例数据、对齐填充。对象头
:对象头重点分析。实例数据
:这部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。对齐填充
:由于虚拟机要求对象起始地址必须是8
字节的整数倍,对齐填充并不是必须存在的,仅仅是为了字节对齐。
# 对象头重点分析
对象头它是实现
synchronized
锁对象的基础,一般而言synchronized
使用的锁对象是存储在Java
对象头中的,JVM
采用两个字来存储对象头,其主要结构是由Mark Word
和Class Metadata Address
组成,如果对象是数组则会分配三个字,多出来的一个字用于记录数组长度,结构说明请看下表:
虚拟机位数 | 对象头结构 | 描述 |
---|---|---|
32/64bit | Mark Word | 存储对象的 hashCode 、锁信息或分代年龄或 GC 标志等信息 |
32/64bit | Class Metadata Address | 类型指针指向对象的类元数据, JVM 通过这个指针确定该对象是哪个类的实例 |
Mark Word
在默认情况下存储着对象的hashCode
分代年龄
锁状态标记
等,下表是32
位JVM
的Mark Word
默认存储结构。
锁状态 | 25bit | 4bit | 1bit 是否是偏向锁 | 2bit 锁标志位 |
---|---|---|---|---|
无锁状态 | 对象 hashCode | 对象分代年龄 | 0 | 01 |
由于对象头信息是对象自身定义的数据没有关系的额外储存成本,因此考虑到
JVM
的空间效率,Mark Word
被设计成一个非固定的数据结构,以便存储更多有效数据,它会根据对象本身状态来复用自己的存储空间,如:32
位的JVM
除了上述列出的Mark Word
默认存储结构外,还有可能变为如下结构:
轻量级锁和偏向锁是
Java 6
对synchronized
进行优化后新增的,在这我们先分析重量级锁也就是synchronized
的对象锁,锁状态标记为10
其中指针指向的是monitor
对象 (管程或监视器锁) 的起始地址。每个对象都存在着一个monitor
与之关联,对象与其monitor
之间的关系存在多种实现方式。
如:
monitor
可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个monitor
被某个线程持有后,它便处于锁定状态。在Java
虚拟机 (HotSpot
) 中,monitor
是由ObjectMonitor
来实现的,其主要数据结构如下,位于HotSpot
虚拟机源码ObjectMonitor.hpp
文件,由C++
实现。
ObjectMonitor() { | |
_header = NULL; | |
_count = 0; // 记录个数 | |
_waiters = 0, | |
_recursions = 0; // 线程的重入次数 | |
_object = NULL; | |
_owner = NULL; // 标识拥有该 monitor 的线程 | |
_WaitSet = NULL; // 处于 wait 状态的线程,会被加入到_WaitSet | |
_WaitSetLock = 0 ; | |
_Responsible = NULL ; | |
_succ = NULL ; | |
_cxq = NULL ; | |
FreeNext = NULL ; | |
_EntryList = NULL ; // 处于等待锁 block 状态的线程,会被加入到该列表 | |
_SpinFreq = 0 ; | |
_SpinClock = 0 ; | |
OwnerIsThread = 0 ; | |
_previous_owner_tid = 0; | |
} |
在
ObjectMonitor
中有两个队列分别是_WaitSet
和_EntryList
, 用于保存ObjectWaiter
对象列表,而每个等待锁的线程都会被封装成ObjectWaiter
对象,_owner
指向持有ObjectMonitor
对象的线程,当多个线程同时访问一段同步代码时,首先会进入_EntryList
集合,当线程获取到对象的monitor
后将进入_owner
区域并把monitor
中的_owner
变量设置为当前线程,同时monitor
中的计数器count
将会+1
,若线程调用wait()
方法,将释放当前持有的monitor
,_owner
变量恢复为Null
然后count
会-1
,该线程将会进入_WaitSet
集合中等待被唤醒。若当前线程执行完毕将释放monitor
并复位变量的值,以便其它线程进入获取monitor
锁。
由此可以看出
monitor
对象存在于每个Java
对象的对象头中 (存储的指针指向),synchronized
便是通过这种方式获取锁的,这也是为什么Java
中任意对象可以作为锁的原因,同时也是noify
、notifyAll
、wait
等方法存在于顶级对象Object
中的原因,在了解了上面的知识基础后,将进一步来分析synchronized
在字节码层面的具体语义实现。
# synchronized 代码块底层原理
现在重新来定义一个
synchronized
同步代码块,在代码块中操作共享变量,代码示例如下:
package top.rem.rain.synchronized_example; | |
/** | |
* @Author: LightRain | |
* @Description: synchronized 同步代码块用于 javap 反编译 | |
* @DateTime: 2023-12-16 22:30 | |
* @Version:1.0 | |
**/ | |
public class SyncCodeBlock { | |
private static int i; | |
public void syncTask(){ | |
// 同步代码块 | |
synchronized (this){ | |
i++; | |
} | |
} | |
} |
编译上述代码并使用
javap
命令来反编译得到字节码,反编译命令如下:
javap -p -s -l -v -constants -c -sysinfo SyncCodeBlock.class |
Classfile /D:/项目/gitee/synchronized-example/target/classes/top/rem/rain/synchronized_example/SyncCodeBlock.class | |
Last modified 2023年12月17日; size 544 bytes | |
SHA-256 checksum 448b5a7d5bcb275bb0612dbae81d517f7fa275735caecef1ace76e30f695a070 | |
Compiled from "SyncCodeBlock.java" | |
public class top.rem.rain.synchronized_example.SyncCodeBlock | |
minor version: 0 | |
major version: 61 | |
flags: (0x0021) ACC_PUBLIC, ACC_SUPER | |
this_class: #8 // top/rem/rain/synchronized_example/SyncCodeBlock | |
super_class: #2 // java/lang/Object | |
interfaces: 0, fields: 1, methods: 2, attributes: 1 | |
Constant pool: | |
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V | |
#2 = Class #4 // java/lang/Object | |
#3 = NameAndType #5:#6 // "<init>":()V | |
#4 = Utf8 java/lang/Object | |
#5 = Utf8 <init> | |
#6 = Utf8 ()V | |
#7 = Fieldref #8.#9 // top/rem/rain/synchronized_example/SyncCodeBlock.i:I | |
#8 = Class #10 // top/rem/rain/synchronized_example/SyncCodeBlock | |
#9 = NameAndType #11:#12 // i:I | |
#10 = Utf8 top/rem/rain/synchronized_example/SyncCodeBlock | |
#11 = Utf8 i | |
#12 = Utf8 I | |
#13 = Utf8 Code | |
#14 = Utf8 LineNumberTable | |
#15 = Utf8 LocalVariableTable | |
#16 = Utf8 this | |
#17 = Utf8 Ltop/rem/rain/synchronized_example/SyncCodeBlock; | |
#18 = Utf8 syncTask | |
#19 = Utf8 StackMapTable | |
#20 = Class #21 // java/lang/Throwable | |
#21 = Utf8 java/lang/Throwable | |
#22 = Utf8 SourceFile | |
#23 = Utf8 SyncCodeBlock.java | |
{ | |
private static int i; | |
descriptor: I | |
flags: (0x000a) ACC_PRIVATE, ACC_STATIC | |
// 构造函数 | |
public top.rem.rain.synchronized_example.SyncCodeBlock(); | |
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 9: 0 | |
LocalVariableTable: | |
Start Length Slot Name Signature | |
0 5 0 this Ltop/rem/rain/synchronized_example/SyncCodeBlock; | |
// 主要来看 syncTask 方法实现 | |
public void syncTask(); | |
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: getstatic #7 // Field i:I | |
7: iconst_1 | |
8: iadd | |
9: putstatic #7 // Field i:I | |
12: aload_1 | |
13: monitorexit // 此处退出同步方法 | |
14: goto 22 | |
17: astore_2 | |
18: aload_1 | |
19: monitorexit // 此处退出同步方法 | |
20: aload_2 | |
21: athrow | |
22: return | |
Exception table: | |
from to target type | |
4 14 17 any | |
17 20 17 any | |
LineNumberTable: | |
line 14: 0 | |
line 15: 4 | |
line 16: 12 | |
line 17: 22 | |
LocalVariableTable: | |
Start Length Slot Name Signature | |
0 23 0 this Ltop/rem/rain/synchronized_example/SyncCodeBlock; | |
StackMapTable: number_of_entries = 2 | |
frame_type = 255 /* full_frame */ | |
offset_delta = 17 | |
locals = [ class top/rem/rain/synchronized_example/SyncCodeBlock, class java/lang/Object ] | |
stack = [ class java/lang/Throwable ] | |
frame_type = 250 /* chop */ | |
offset_delta = 4 | |
} | |
SourceFile: "SyncCodeBlock.java" |
从字节码中可以看出同步代码块的实现使用的是
monitorenter
和monitorexit
指令,其中monitorenter
指令指向同步代码块的开始位置,而monitorexit
指令则指明同步代码块的结束位置,当执行monitorenter
指令时,当前线程将试图获取objectref
(对象锁) 所对应的monitor
的持有权,当objector
的monitor
进入时计数器为0
,那么线程就可以成功获取到monitor
,并将计数器设置为1
则代表取锁成功。
如果当前线程已持有
objectref
的monitor
持有权,那它可以重入这个monitor
(重入将在后面介绍),重入时计数器的值也会+1
。倘若其它线程已拥有objectref
的monitor
持有权,那么当前线程将被阻塞,直到正在执行的线程执行完毕 (即:monitorexit
指令被执行),执行完monitorexit
指令后线程将释放monitor
锁并设置计数器为0
,其他线程将有机会持有monitor
。
这里需要注意的是编译器将会确保,无论方法通过任何一种方式完成, 方法中调用过的每条
monitorenter
指令都有其对应的monitorexit
指令执行,而无论这个方法是否正常结束还是异常结束,为了确保在方法异常完成时monitorenter
和monitorexit
指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行monitorexit
指令,从字节码中可以看出多了一个monitorexit
指令,它就是异常结束时被执行释放monitor
的指令。
# synchronized 方法底层原理
synchronized
方法级的同步是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作中,JVM
可以从常量池中的方法结构表中访问ACC_SYNCHRONIZED
标志来区分一个方法是否是同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED
访问标志是否被设置,如果设置了ACC_SYNCHRONIZED
访问标志,执行线程将先持有monitor
(在虚拟机规范中代表管程),然后再执行方法,最后在方法完成时无论是否正常完成都会释放monitor
。在方法执行期间,执行线程持有了monitor
其它线程都将无法再获取到同一个monitor
。
如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常时,那么这个同步方法所持有的
monitor
将在异常抛到同步方法之外时自动释放,字节码层如下:
package top.rem.rain.synchronized_example; | |
/** | |
* @Author: LightRain | |
* @Description: synchronized 同步方法用于 javap 反编译 | |
* @DateTime: 2023-12-17 10:46 | |
* @Version:1.0 | |
**/ | |
public class SyncMethod { | |
public int i; | |
public synchronized void syncTask(){ | |
i++; | |
} | |
} |
编译上述代码并使用
javap
命令来反编译得到字节码,反编译命令如下:
javap -p -s -l -v -constants -c -sysinfo SyncMethod.class |
Classfile /D:/项目/gitee/synchronized-example/target/classes/top/rem/rain/synchronized_example/SyncMethod.class | |
Last modified 2023年12月17日; size 431 bytes | |
SHA-256 checksum 6b6d0142250883ab1592853d0f9ef17372bb1fb40c43ab9b5dd2a512653b0bc9 | |
Compiled from "SyncMethod.java" | |
public class top.rem.rain.synchronized_example.SyncMethod | |
minor version: 0 | |
major version: 61 | |
flags: (0x0021) ACC_PUBLIC, ACC_SUPER | |
this_class: #8 // top/rem/rain/synchronized_example/SyncMethod | |
super_class: #2 // java/lang/Object | |
interfaces: 0, fields: 1, methods: 2, attributes: 1 | |
Constant pool: | |
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V | |
#2 = Class #4 // java/lang/Object | |
#3 = NameAndType #5:#6 // "<init>":()V | |
#4 = Utf8 java/lang/Object | |
#5 = Utf8 <init> | |
#6 = Utf8 ()V | |
#7 = Fieldref #8.#9 // top/rem/rain/synchronized_example/SyncMethod.i:I | |
#8 = Class #10 // top/rem/rain/synchronized_example/SyncMethod | |
#9 = NameAndType #11:#12 // i:I | |
#10 = Utf8 top/rem/rain/synchronized_example/SyncMethod | |
#11 = Utf8 i | |
#12 = Utf8 I | |
#13 = Utf8 Code | |
#14 = Utf8 LineNumberTable | |
#15 = Utf8 LocalVariableTable | |
#16 = Utf8 this | |
#17 = Utf8 Ltop/rem/rain/synchronized_example/SyncMethod; | |
#18 = Utf8 syncTask | |
#19 = Utf8 SourceFile | |
#20 = Utf8 SyncMethod.java | |
{ | |
public int i; | |
descriptor: I | |
flags: (0x0001) ACC_PUBLIC | |
public top.rem.rain.synchronized_example.SyncMethod(); | |
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 9: 0 | |
LocalVariableTable: | |
Start Length Slot Name Signature | |
0 5 0 this Ltop/rem/rain/synchronized_example/SyncMethod; | |
// 主要来看 syncTask 方法 | |
public synchronized void syncTask(); | |
descriptor: ()V | |
// 方法标识 ACC_PUBLIC 代表 public 修饰,ACC_SYNCHRONIZED 指明该方法为同步方法 | |
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED | |
Code: | |
stack=3, locals=1, args_size=1 | |
0: aload_0 | |
1: dup | |
2: getfield #7 // Field i:I | |
5: iconst_1 | |
6: iadd | |
7: putfield #7 // Field i:I | |
10: return | |
LineNumberTable: | |
line 13: 0 | |
line 14: 10 | |
LocalVariableTable: | |
Start Length Slot Name Signature | |
0 11 0 this Ltop/rem/rain/synchronized_example/SyncMethod; | |
} | |
SourceFile: "SyncMethod.java" |
在字节码中可以看出,
synchronized
修饰的方法并没有monitorenter
和monitorexit
指令,取而代之的是ACC_SYNCHRONIZED
标识,此标识指明了该方法是一个同步方法,JVM
通过该ACC_SYNCHRONIZED
标识来分辨一个方法是否被声明为同步方法,从而执行相应的同步调用,这便是synchronized
锁在同步代码块和同步方法上的基本实现原理。
我们需要必须注意的是在
Java
早期版本中,synchronized
属于重量级锁,由此效率很低,因为监视器锁 (monitor
) 是依赖于底层操作系统的Mutex Lock
来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对较长的时间,时间成本消耗会很高,这也是为什么早期的synchronized
效率很低的原因。
好在
Java 6
之后官方从JVM
层面对synchronized
做了较大的优化,所以现在的synchronized
锁效率优化得很不错了,Java 6
之后为了减少获得锁和释放锁时带来的性能消耗,便引入了轻量级锁和偏向锁,下面接着将来了解一下Java
在JVM
层对synchronized
的优化。
# JVM 对 synchronized 的优化
- 锁的状态一共有四种分别是:
- 锁的状态可以从偏向锁升级到轻量级锁,再升级到重量级锁,但是锁的升级是单向的,就是说只能从低到高,不会出现锁的降级。
- 下面将介绍偏向锁和轻量级锁以及
JVM
的其它优化手段,如需详细阅读请查阅《深入理解java虚拟机》
。
# 偏向锁
偏向锁是在
Java 6
之后新增的锁,它是一种针对加锁操作的优化手段,经过研究发现在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获取,因此为了减少同一个线程获取锁 (涉及CAS
操作) 的代价而引入的偏向锁。
偏向锁核心思想是,如果一个线程获得了锁,那么锁就会进入偏向模式,此时
Mark Word
的结构也将变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作 (即获取锁的过程),这样就省去了大量有关 “锁” 申请的操作,从而提高了程序的性能。
对于没有锁竞争的场合,偏向锁有很好得优化效果,毕竟极有可能连续多次是同一个线程申请相同锁。但是对于锁竞争激烈的场合,偏向锁就将失效了,因为这种场合极有可能每次申请锁的线程都是不同的,因此在这种情况下就不应该使用偏向锁,偏向锁失败后并不会立即升级到重量级锁,而是先将锁升级为轻量级锁。
# 轻量级锁
偏向锁失败后,
JVM
并不会立即升级为重量级锁,它首先会尝试一种称为轻量级锁的优化手段 (自1.6
之后新增的一种锁),此时Mark Word
的结构将变为轻量级锁结构。轻量级锁能够提升程序性能的依据是:对绝大部分的锁,在整个同步周期内都不存在竞争。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一个锁的场景,将会导致轻量级锁升级为重量级锁。
# 自旋锁
轻量级锁失败后,
JVM
为了避免线程真实地在操作系统层面被挂起,将会进行一种称为自旋锁的优化手段。这是基于大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程将会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前线程可以获得锁,虚拟机会让当前想要获得锁的线程做几个空循环 (这也是被称为自旋的原因),一般不会太久,大概是在50~100
个循环,经过若干次循环后,如果得到锁就顺利进入临界区。
如果:经过若干次循环后还不能获得锁,那么线程将会在操作系统层面被挂起,这就是自旋锁的优化方式,这种方式确实可以提升效率,在最后没办法的情况时也就只能升级为重量级锁了。
# 锁消除
锁消除是虚拟机的另外一种对锁的优化,这种优化更彻底,
Java
虚拟机在JIT
编译时 (即时编译,可以理解为当某段代码即将第一次被执行时进行编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式来消除没有必要的锁,可以省去毫无意义的加锁时间,示例代码如下:
package top.rem.rain.synchronized_example; | |
/** | |
* @Author: LightRain | |
* @Description: 锁消除示例:消除 StringBuffer 同步锁 | |
* @DateTime: 2023-12-17 15:55 | |
* @Version:1.0 | |
**/ | |
public class StringBufferRemoveSynchronized { | |
public void add(String str1, String str2) { | |
// StringBuffer 是线程安全的,由于 sb 只会在 append 方法中使用,不可能被其他线程引用 | |
// 因此 sb 属于不可能共享的资源,JVM 会自动消除内部的锁 | |
StringBuffer sb = new StringBuffer(); | |
sb.append(str1).append(str2); | |
} | |
public static void main(String[] args) { | |
StringBufferRemoveSynchronized rsync = new StringBufferRemoveSynchronized(); | |
for (int i = 0; i < 10000000; i++) { | |
rsync.add("abc", "123"); | |
} | |
} | |
} |
上述代码中
StringBuffer
的append()
方法是一个同步方法,但是在add()
方法中的StringBuffer
属于局部变量,并且不会被其它线程所使用,因此StringBuffer
不会存在共享资源被竞争的情景,JVM
会自动将其内部的锁进行消除。
# synchronized 的可重入性
从互斥锁的设计上来说,当一个线程试图操作一个由其它线程持有对象锁的临界资源时将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况就属于 “重入锁” 请求将会成功,在
Java
中synchronized
是基于原子性内部锁机制,是可重入的,因此在一个线程调用synchronized
方法的同时在其它方法体内调用该对象的另一个synchronized
方法,也就是说一个线程得到一个对象锁后再次请求该对象锁这是允许的,这就是synchronized
的可重入性,示例代码如下:
package top.rem.rain.synchronized_example; | |
/** | |
* @Author: LightRain | |
* @Description: synchronized 的可重入性 | |
* @DateTime: 2023-12-15 23:02 | |
* @Version:1.0 | |
**/ | |
public class SynchronizedData2 implements Runnable { | |
private static final SynchronizedData2 INSTANCE = new SynchronizedData2(); | |
/** | |
* 共享资源 (临界资源) | |
*/ | |
private static int i = 0; | |
private static int ii = 0; | |
@Override | |
public void run() { | |
synchronized (INSTANCE){ | |
for (int j = 0; j < 1000000; j++) { | |
i++; | |
//synchronized 的可重入性 | |
incremental(); | |
} | |
} | |
} | |
/** | |
* 递增方法 | |
*/ | |
private synchronized void incremental() { | |
ii++; | |
} | |
public static void main(String[] args) throws InterruptedException { | |
Thread t1 = new Thread(INSTANCE); | |
Thread t2 = new Thread(INSTANCE); | |
// 开始 | |
t1.start(); | |
t2.start(); | |
t1.join(); | |
t2.join(); | |
System.out.println(i); | |
System.out.println(ii); | |
} | |
} |
在获取到当前实例对象锁后进入
synchronized
代码块执行同步代码,并在代码块中调用当前实例对象的另一个synchronized
方法,再次请求当前实例锁时将被允许,进而执行方法体中的代码,这就是重入锁最直接的提现。需要注意另外一种情况,当子类继承父类是,子类也是可以通过可重入锁调用父类中的同步方法,注意:由于synchronized
是基于monitor
实现的,因此每次重入时monitor
中的计数器仍会+1
。
# synchronized 与线程中断
# 线程中断
线程中断就是在线程运行 (
run()
方法) 期间打断它,在Java
中,提供了三种有关线程中断的方法。
// 中断线程 (实例方法) | |
public void Thread.interrupt(); | |
// 判断线程是否被中断 (实例方法) | |
public boolean Thread.isInterrupted(); | |
// 判断是否被中断并清除当前中断状态 (实例方法) | |
public static boolean Thread.interrupted(); |
下面是这三种方法的源码,如下:
package java.lang; | |
public class Thread implements Runnable { | |
// 中断线程 (实例方法) | |
public void interrupt() { | |
if (this != Thread.currentThread()) { | |
checkAccess(); | |
// thread may be blocked in an I/O operation | |
synchronized (blockerLock) { | |
Interruptible b = blocker; | |
if (b != null) { | |
interrupted = true; | |
interrupt0(); // inform VM of interrupt | |
b.interrupt(this); | |
return; | |
} | |
} | |
} | |
interrupted = true; | |
// inform VM of interrupt | |
interrupt0(); | |
} | |
// 判断线程是否被中断 (实例方法) | |
public boolean isInterrupted() { | |
return interrupted; | |
} | |
// 判断是否被中断并清除当前中断状态 (实例方法) | |
public static boolean interrupted() { | |
Thread t = currentThread(); | |
boolean interrupted = t.interrupted; | |
// We may have been interrupted the moment after we read the field, | |
// so only clear the field if we saw that it was set and will return | |
// true; otherwise we could lose an interrupt. | |
if (interrupted) { | |
t.interrupted = false; | |
clearInterruptEvent(); | |
} | |
return interrupted; | |
} | |
} |
当一个线程处于被阻塞状态或试图执行一个阻塞操作时,使用
Thread.interrupt()
方法可中断该线程,注意:此时将会抛出一个InterruptedException(中断异常)
的异常,同时中断状态将会被复位 (由中断状态改为非中断状态),示例代码如下:
package top.rem.rain.synchronized_example; | |
import java.util.concurrent.TimeUnit; | |
/** | |
* @Author: LightRain | |
* @Description: 线程中断示例 | |
* @DateTime: 2023-12-17 21:30 | |
* @Version:1.0 | |
**/ | |
public class InterruptSleepThread { | |
public static void main(String[] args) throws InterruptedException { | |
Thread t1 = new Thread(){ | |
@Override | |
public void run() { | |
//while 在 try 中,通过异常中断就可以退出 run 循环 | |
try { | |
while (true){ | |
// 当前线程处于阻塞状态,异常必须捕捉处理,无法往外抛出 | |
TimeUnit.SECONDS.sleep(2); | |
} | |
} catch (InterruptedException e) { | |
System.out.println("Interrupt When Sleep"); | |
boolean interrupt = this.isInterrupted(); | |
// 中断状态被复位 | |
System.out.println("interrupt:"+interrupt); | |
} | |
} | |
}; | |
t1.start(); | |
TimeUnit.SECONDS.sleep(2); | |
// 中断处于阻塞状态的线程 | |
t1.interrupt(); | |
/* | |
执行结果: | |
Interrupt When Sleep | |
interrupt:false | |
*/ | |
} | |
} |
如上述代码所示,创建一个线程并在线程中调用了
sleep()
方法从而使线程进入阻塞状态,启动线程后调用线程实例对象的interrupt()
方法来中断线程阻塞异常,并抛出InterruptedException
异常,此时中断状态也将被复位。为了编码清晰建议使用TimeUnit.SECONDS.sleep()
, 此方法内部实现最终还是调用了Thread.sleep()
方法。
除了阻塞中断的情景,我们还可能会遇到处于运行期间且非阻塞状态的线程,在这种情况下,直接调用
Thread.interrupt()
方法中断线程是不会得到任何响应的,如下代码将无法中断非阻塞状态下的线程:
package top.rem.rain.synchronized_example; | |
import java.util.concurrent.TimeUnit; | |
/** | |
* @Author: LightRain | |
* @Description: interrupt () 方法示例 | |
* @DateTime: 2023-12-17 21:51 | |
* @Version:1.0 | |
**/ | |
public class InterruptThread { | |
public static void main(String[] args) throws InterruptedException { | |
Thread t1 = new Thread() { | |
@Override | |
public void run() { | |
while (true) { | |
System.out.println("未被中断"); | |
} | |
} | |
}; | |
t1.start(); | |
TimeUnit.SECONDS.sleep(2); | |
t1.interrupt(); | |
/* | |
执行结果 - 无限执行: | |
未被中断 | |
未被中断 | |
未被中断 | |
...... | |
*/ | |
} | |
} |
虽然我们在代码中调用了
interrupt()
方法,但线程并未被中断,因为当前线程处于非阻塞状态下,这种情况需要我们手动去进行中断检测并结束程序,修改后的代码如下:
package top.rem.rain.synchronized_example; | |
import java.util.concurrent.TimeUnit; | |
/** | |
* @Author: LightRain | |
* @Description: interrupt () 方法示例 | |
* @DateTime: 2023-12-17 21:51 | |
* @Version:1.0 | |
**/ | |
public class InterruptThread2 { | |
public static void main(String[] args) throws InterruptedException { | |
Thread t1=new Thread(){ | |
@Override | |
public void run(){ | |
while(true){ | |
// 判断当前线程是否被中断 | |
if (this.isInterrupted()){ | |
System.out.println("线程中断"); | |
break; | |
} | |
} | |
System.out.println("已跳出循环,线程中断!"); | |
} | |
}; | |
t1.start(); | |
TimeUnit.SECONDS.sleep(2); | |
t1.interrupt(); | |
/* | |
执行结果: | |
线程中断 | |
已跳出循环,线程中断! | |
*/ | |
} | |
} |
我们在上述代码中使用了实例方法
isInterrupted()
来判断线程是否已被中断,如果被中断则跳出循环以此来结束当前线程,注意:非阻塞状态下调用interrupt()
方法并不会导致中断状态重置。
总结:中断线程有两种情况,第一种是当线程处于阻塞状态或试图执行一个阻塞操作时,我们可以使用实例方法
interrupt()
进行线程中断,执行中断操作后将会抛出InterruptedException
异常 (该异常必须进行捕捉无法抛出) 并将中断状态复位。第二种是当线程处于运行状态时,我们也可以调用实例方法interrupt()
进行线程中断,但同时必须手动去判断中断状态,并编写中断线程的代码 (其就是来结束run()
方法体的代码)。有时我们在编写代码时可能需要兼顾以上两种情况,那么就可以如下编写:
public void run(){ | |
try { | |
// 判断当前线程是否已中断,注意 interrupted 方法是静态的,执行后会对中断状态进行复位,Thread.interrupted () 默认为 false | |
while (!Thread.interrupted()) { | |
TimeUnit.SECONDS.sleep(2); | |
} | |
} catch (InterruptedException e) { | |
} | |
} |
# 中断与 synchronized
事实上线程的中断操作对于正在等待获取锁对象的
synchronized
方法或代码块并不起作用,也就是说对于synchronized
来说,如果一个线程正在等待锁,那么结果就只有两种可能,第一要么它获得这把锁继续执行,第二要么它就保持等待,即使调用了中断线程的方法也并不会生效。示例代码如下:
package top.rem.rain.synchronized_example; | |
import java.util.concurrent.TimeUnit; | |
/** | |
* @Author: LightRain | |
* @Description: 中断与 synchronized | |
* @DateTime: 2023-12-17 22:30 | |
* @Version:1.0 | |
**/ | |
public class SynchronizedBlocked implements Runnable{ | |
public synchronized void f() { | |
System.out.println("Trying to call f()"); | |
// Never releases lock | |
while(true) | |
Thread.yield(); | |
} | |
/** | |
* 在构造器中创建新线程并启动获取对象锁 | |
*/ | |
public SynchronizedBlocked() { | |
// 该线程已持有当前实例锁 | |
new Thread() { | |
public void run() { | |
// Lock acquired by this thread | |
f(); | |
} | |
}.start(); | |
} | |
public void run() { | |
// 中断判断 | |
while (true) { | |
if (Thread.interrupted()) { | |
System.out.println("中断线程!!"); | |
break; | |
} else { | |
f(); | |
} | |
} | |
} | |
public static void main(String[] args) throws InterruptedException { | |
SynchronizedBlocked sync = new SynchronizedBlocked(); | |
Thread thread = new Thread(sync); | |
// 启动后调用 f () 方法,无法获取当前实例锁处于等待状态 | |
thread.start(); | |
TimeUnit.SECONDS.sleep(1); | |
// 中断线程,无法生效 | |
thread.interrupt(); | |
/* | |
执行结果: | |
Trying to call f () | |
...... | |
当前程序并不会自动停止 | |
*/ | |
} | |
} |
我们在
SynchronizedBlocked
构造函数中创建了一个新线程并启动调用f()
方法获取到哦当前实例锁,由于SynchronizedBlocked
自身也是线程,启动后在其run()
方法中也调用了f()
方法,但是由于对象锁被其它线程所占用,导致thread
线程只能等待锁,此时我们调用了thread.interrupt()
方法,但并不能中断线程。
# synchronized 与等待唤醒机制
所谓的等待唤醒机制主要指的是
notify
、notifyAll
、wait
方法,在使用这三个方法时,必须处于synchronized
代码块或synchronized
方法中,否则将会抛出IllegalMonitorStateException
异常,这是因为调用者几个方法前必须拿到当前对象的监视器也就是monitor
对象,就是说notify
、notifyAll
、wait
方法依赖于monitor
对象,我们知道monitor
存在于对象头的Mark Word
中 (存储monitor
引用指针),而synchronized
关键字可以获取monitor
对象,这也就是为什么notify
、notifyAll
、wait
方法必须在synchronized
代码块或synchronized
方法中调用的原因。
synchronized (object){ | |
object.notify(); | |
object.notifyAll(); | |
object.wait(); | |
} |
最后需要特别注意的是,与
sleep()
方法不同的是wait()
方法调用完成后,线程将被暂停,但wait()
方法将会释放当前持有的监视器锁 (monitor
), 直到有新线程调用了notify
或notifyAll
方法后才能继续执行,而sleep()
方法只是让线程休眠并不释放锁。同时notify
、notifyAll
方法调用后,并不会马上释放监视器锁,而是在相应的synchronized
代码块或synchronized
方法执行结束后才会自动将锁释放。