Synchronized&Lock&AQS详解
# 概念
# 临界资源
多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是:对象、变量、文件等。 共享:资源可以由多个线程同时访问。
可变:资源可以在其生命周期内被修改
引出的问题:
由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问,那么我们怎么解决线程并发安全问题?
实际上,所有的并发模式在解决线程安全问题时,采用的方案都是 序列化访问临界资源
。
# 同步互斥访问
即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock
同步器的本质就是加锁, 加锁目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问),将并发请求转变成了串行化执行,会降低部分性能。
不过有一点需要区别的是:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题
# 显示锁和隐式锁
所谓的显示和隐式就是在使用的时候,使用者要不要手动写代码去获取锁和释放锁的操作。Synchronized
为Jvm内置锁,不需要手动加锁与解锁,Jvm会自动加锁跟解锁,即为隐式锁。ReentrantLock
读法/rɪˈentrənt/ ,实现juc里Lock,实现是基于AQS实现,需要手动加锁跟解锁ReentrantLock lock(),unlock(); 其为显式锁
参考:Java并发之显式锁和隐式锁的区别 - kaizi1992 - 博客园 (cnblogs.com) (opens new window)
# CAS
CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。
CAS算法涉及到三个操作数:
- 需要读写的内存值 V。
- 进行比较的值 A。
- 要写入的新值 B。
当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。
java.util.concurrent包中的原子类AtomicXXX,就是通过CAS来实现了乐观锁
# 锁分类
参考:java中的各种锁详细介绍 - JYRoy - 博客园 (cnblogs.com) (opens new window)
# Synchronized详解
参考文章 synchronized底层实现原理及锁优化_Medlen-CSDN博客_synchronized底层原理 (opens new window)
# 1、synchronized作用
原子性:synchronized保证语句块内操作是原子的 可见性:synchronized保证可见性(通过“在执行unlock之前,必须先把此变量同步回主内存”实现) 有序性:synchronized保证有序性(通过“一个变量在同一时刻只允许一条线程对其进行lock操作”)
# 2、加锁方式
①.同步实例方法,锁是当前实例对象 (不用static 修饰方法,spring容器对象的话,需要是单例才锁得住)
②.同步类方法,锁是当前类对象 (用static 修饰方法)
③.同步代码块,锁是括号里面的对象
public synchronized String syncMethod(){} // 锁类实例对象
public static synchronized String syncMethod(){} // 锁类对象
private static Object obj=new Object();
public String syncMethod(){
synchronized(obj){ // obj为锁
...
}
}
2
3
4
5
6
7
8
9
# 3、synchronized底层原理
synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)
实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock
(互斥锁)实现,它是一个重量级锁性能较低。每个对象都有一个自己的Monitor(监视器锁)
当然,JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,,内置锁的并发性能已经基本与Lock持平。
synchronized关键字被编译成字节码后会被翻译成monitorenter 和monitorexit 两条指令分别在同步块逻辑代码的起始位置与结束位置,其实质为jmm的8大内存原子操作中的lock与unlock。
每个同步对象都有一个自己的Monitor(监视器锁),加锁过程如下图所示:
那么有个问题来了,我们知道synchronized加锁加在对象上,对象是如何记录锁状态的呢? 答案是锁状态是被记录在每个对象的对象头(Mark Word)中,下面我们一起认识一下对象的内存布局
/**
* synchronized原理:查看字节码 MONITORENTER MONITOREXIT
* @author zxp
* @date 2021-08-20 10:33
*/
public class SynchronizedTest {
private Object object = new Object();
public synchronized void method1(){
System.out.printf("锁对象的某个实例");
}
public synchronized static void method2(){
System.out.printf("锁类Class");
}
public void method3(){
synchronized (object) {
System.out.printf("锁代码块");
}
}
public static void main(String[] args) {
System.out.println(11);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 对象内存结构
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
- 对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等
- 实例数据:即创建对象时,对象中成员变量,方法等
- 对齐填充:对象的大小必须是8字节的整数倍
# 锁的膨胀升级过程
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单 向的,也就是说只能从低到高升级,不会出现锁的降级。下图为锁的升级全过程:
