Synchronized
又到周末了,最近的话(有点子小日子不好过,哈哈哈!~)但是,我还是报之以歌哈哈哈
本次写关于并发_Synchronized
的优化以及底层实现原理
说说心里话~其实是非常的累,原因应该怎么说呢?我发现自己在如今的这家公司,我处于一种活多钱少以及关键现在给的或自己不想干,因为没有一点儿子的技术性
你可能会问:那就跳呗!~特么现在技术还不够啊,哈哈哈,真的是无语坏了,~还说鸡毛,哈哈哈,就是吐槽!~
好吧~进入正文
本文总纲
1.类锁和对象锁
在上篇的总纲中我们已经介绍,这里我们复习下概念(来自官方解释~哈哈哈!)
在Java多线程编程中,锁主要用于控制对共享资源的访问,以保证数据的一致性和完整性。
类锁和对象锁是两种不同粒度的锁。
- 对象锁:
- 在Java中,每个对象都有一个内置锁(也称为监视器锁),
- 当一个线程试图访问某个对象的
synchronized
代码块或方法时,该线程必须先获得该对象的锁。 - 同一时刻,只能有一个线程持有对象锁,其他线程必须等待。
- 换句话说(大白话),对象锁是针对具体对象实例的,用于保护对象实例的并发安全。
例如:
public class MyClass {
public synchronized void method() {
// 同一时间只有一个线程可以执行此方法
}
}
或者
public class MyClass {
public void method() {
synchronized (this) {
// 同一时间只有一个线程可以执行此代码块
}
}
}
- 类锁:类锁也是通过 synchronized 关键字来实现的,但不是作用于对象实例上,而是作用在整个类的Class对象上。在Java中,每个类在JVM中只有一个Class对象,所以类锁也是全局的,是一种粗粒度的锁。
例如:
public class MyClass {
private static synchronized void classMethod() {
// 同一时间只有一个线程可以执行此静态方法,不论多少个对象实例
}
}
或者
public class MyClass {
private static final Object classLock = new Object();
public void instanceMethod() {
synchronized (MyClass.classLock) {
// 同一时间只有一个线程可以通过任何对象实例进入此同步代码块
}
}
}
总的来说,对象锁用于控制单个对象实例的并发访问,而类锁则用于控制所有对象实例对该类的静态成员或代码块的并发访问。
2.SYNCHRONIZED
的优化
这里说得优化:主要是JDK
的官方所说的三个优化:(在JDK1.6
,JDK团队对Synchronized
做的大量优化,因为在JDK1.5
时候,被ReentrantLock
完虐,被迫优化了,哈哈哈)
- 锁消除
- 锁膨胀
- 锁升级
1.锁消除(Lock Elimination)
在Synchronized
修饰的代码中,如果不存在操作临界资源的情况,会触发锁消除,
啥意思呢?就是即便你写了Synchronized
,也不会触发(~就是这么的豪横!,哈哈)
具体点解释
是Java
虚拟机(JVM)的一项优化技术,主要应用于并发编程场景。
- 在某些情况下,JVM能够检测到某个同步块内的数据在该同步块的执行过程中没有发生竞争,
- 也就是说,不存在多个线程同时访问这段代码和共享数据的情况。
- 此时,JVM就可以安全地消除对该同步块的锁定,从而提高程序运行效率。
举个例子:
假设array
数组的元素都是不同的字符串对象,并且这段代码中的同步块只对局部变量localString
进行操作,没有改变任何共享状态或与其他线程交互,
那么JVM通过分析可以判断这个同步块实际上是不必要的,因此可以进行锁消除。
public void doSomething(int index) {
String localString = array[index];
synchronized (localString) {
// 对localString进行操作,但并未涉及任何共享状态或与其他线程的交互
}
}
2.锁膨胀(Lock Coarsening)
如若在一个循环中,频繁的获取和释放资源,这样会带来很大的消耗!
为了避免和减少这种状况,锁膨胀就出现了,就是将锁的范围扩大,避免频繁的竞争和获取锁资源带来的不必要消耗
官方术语
锁膨胀:
- 是Java虚拟机(JVM)进行并发优化的一种手段,
- 与锁消除相反,它不是去除不必要的锁,而是合并多个细粒度的锁为一个粗粒度的锁,
- 以减少锁竞争和上下文切换开销,提高并发性能。
在多线程编程中,
- 如果一段代码涉及到对多个独立对象的同步操作,可能会导致频繁的锁获取和释放操作,增加系统开销。
- 锁膨胀就是将这些原本独立的对象锁合并成一个更粗粒度的锁,比如使用同一个锁来保护一组相关对象的操作,使得在多线程环境下,可以减少锁的竞争次数,提升系统的并发性能。
例如
假设有一个场景,程序需要对两个独立的对象A和B
进行同步访问:
synchronized (objectA) {
// 对objectA进行操作
}
synchronized (objectB) {
// 对objectB进行操作
}
在高并发场景下,不同的线程可能交替对A、B对象进行加锁,造成锁竞争激烈。
通过锁膨胀优化,可以将上述代码改为(实际上还是之前的上面的代码,只是JDK在这被优化如下面这串代码):
我们清晰的可看到:锁的范围扩大了(所有对A和B对象的同步操作都由一个共享的锁来控制,从而减少了锁竞争的可能性。)
private static final Object sharedLock = new Object();
synchronized (sharedLock) {
synchronized (objectA) {
// 对objectA进行操作
}
synchronized (objectB) {
// 对objectB进行操作
}
}
3.锁升级(本文的重点!)(后面会说)
我们先说下背景:(这是为了更快的掌握嘛!~别急,慢慢来,比较快!)
在Java中,synchronized
关键字用于实现线程间的同步控制,它提供了内置的锁机制来确保数据的并发访问安全。随着JDK版本的发展,尤其是从Java 6开始,为了优化synchronized在不同场景下的性能表现,引入了锁升级的概念,即根据竞争情况动态地将锁从一种状态转换为另一种状态。
- 这种锁升级机制可以更高效地利用
CPU
资源,在无竞争或竞争不激烈的情况下提供更好的性能, - 而在高并发竞争情况下则退化为传统的重量级锁保证线程安全。
- 需要注意的是,具体的锁升级策略和细节可能因不同的
Java
虚拟机实现而略有差异。(但是都差不多)
synchronized锁的升级过程主要包括以下四个阶段:
- 无锁状态(Unlocked)
- 当对象没有被任何线程锁定时,对象头中的锁标志位是未锁定的状态。
- 偏向锁(Biased Locking)
- 在只有一个线程进入同步代码块的情况下,JVM会把锁设置为偏向模式,将锁绑定到当前获得锁的线程上,这样后续该线程再次进入同步代码块时无需再进行同步操作,从而减少获取锁和释放锁带来的开销。
- 轻量级锁(Lightweight Locking)
- 当有第二个线程尝试进入已经被第一个线程持有偏向锁的方法或代码块时,偏向锁会被撤销,并升级为轻量级锁。轻量级锁采用
CAS
(Compare and Swap)操作尝试获取锁,如果获取失败,则通过自旋等待一段时间尝试重新获取,如果经过一定次数的自旋仍然无法成功获取锁,说明存在较为激烈的锁竞争,此时锁会进一步升级。
- 重量级锁(Heavyweight Locking)
- 当自旋获取轻量级锁失败后,锁会升级为重量级锁。重量级锁会导致线程阻塞并进入操作系统层面的线程调度,直至锁被释放。此时,其他竞争线程将进入阻塞队列等待,持有锁的线程执行完毕后唤醒等待队列中的下一个线程继续执行。
3.Synchronized的实现原理
整个过程中,ObjectMonitor
扮演了关键角色,负责管理和调度多个线程对同一锁的竞争与协作,确保了并发环境下的数据一致性。而这种基于操作系统互斥原语实现的锁被称为“重量级锁”,因为它涉及到线程上下文切换等昂贵的操作,在高并发场景下可能成为性能瓶颈。