对象锁——synchronized关键字
jdk1.6之后对synchronized进行了优化,为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,协调线程安全性和性能的平衡。这种优化主要解决上下文频繁切换,由于Java层面的线程与操作系统的原生线程有映射关系,如果要将一个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内核态来执行,这种切换代价十分昂贵,很耗处理器时间。
* 可重入锁,主要解决自己锁死自己的问题。
* 非公平锁,提高执行性能。
* 悲观锁,不管是否存在竞争,都会加锁。乐观锁:CAS,它涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。它具有原子性,由CPU硬件指令实现保证,即使用JNI调用Native方法调用由C++编写的硬件级别指令。
1、锁的原理
synchronized也叫对象锁(类级别则是Class对象),因为它是通过对对象加锁来实现的。
(1)对象内存布局
对象在内存中的布局可以分为:对象头、实例数据、对齐填充。
(2)对象头中的Mark Word(对象标记)
Mark Word记录了对象和锁有关的信息,当某个对象被synchronized当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关系。Mark Word在32位虚拟机的长度是32bit,在64位虚拟机的长度是64bit。主要存储着该对象锁的当前状态数据:
锁状态 | 25bit | 4bit | 1bit | 2bit | |
23bit | 2bit | 是否偏向锁 | 锁标志位 | ||
无锁 | 对象的hashcode | 分代年龄 | 0 | 01 | |
偏向锁 | 线程id | Epoch | f分代年龄 | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 | |||
重量级锁 | 指向重量级锁的指针 | 10 | |||
GC标记 | 空 | 11 |
线程在获取锁的时候,实际上就是获得一个监视器对象(monitor),monitor可以认为是一个同步对象,所有的java对象都有一个monitor。多个线程访问同步代码块时,就是去争抢锁对象的监视器去修改对象中的锁标识。
2、synchronized锁的升级
锁升级的过程:
(1)偏向锁(适合竞争较少的场景)
偏向锁的获取:
当对象锁第一次被获取时,在Mark Word中记录下该线程的ID,这个线程称为偏向线程。
* 当有线程获取该对象锁时,首先根据对象头判断是否处于可偏向状态(biased_lock=1,且线程ID为空);
* 如果是可偏向状态,则通过cas操作,尝试把当前线程ID写入到Mark Word。
——cas写入成功,表示已经获得了所对象的偏向锁;
——cas写入失败,表示其他线程已经获得了偏向锁,存在竞争,需要撤销已获得偏向锁的线程,并把该对象锁升级为轻量级锁(注意:这个操作需要在全局安全点,也就是没有线程在执行字节码的时候才能执行)
* 如果是已偏向状态,检查Mark Word中存储的线程ID是否等于当前线程ID
——相等,则不需再次获得锁,直接执行同步代码块;
——不相等,说明当前锁偏向于其他线程,存在竞争,需要撤销已获得偏向锁的线程,并把该对象锁升级为轻量级锁(执行时机同上)。
偏向锁的撤销:
对原持有偏向锁的线程进行撤销时,原偏向线程有两种情况:
* 偏向线程同步代码块执行完了,判断Epoch是否达到阈值(默认40),达到阈值则升级为轻量级锁,没达到则会把对象头设置为无锁状态,Epoch+1,所有线程可以基于CAS重新获取该偏向锁。
* 偏向线程同步代码块还没执行完,此时会把原偏向线程持有的偏向锁升级为轻量级锁后,继续执行同步代码块。
注意:在实际应用开发中,绝大部分情况下一定会存在 2 个以 上的线程竞争,那么如果开启偏向锁,反而会提升获取锁 的资源消耗。所以可以通过jvm参数 UseBiasedLocking 来设置开启或关闭偏向锁。
(2)轻量级锁(适合同步代码块执行时间很短的场景)
锁升级为轻量级锁之后,锁对象的Mark Word也会进行相应的变化,升级为轻量级锁的过程:
* 线程在自己的线程栈中创建锁记录Lock Record。
* 将锁对象的对象头中的Mark Word复制到线程刚刚创建的锁记录中。
* 将锁记录中的owner指针指向锁对象。
* 将锁对象的对象头中的Mark Word替换为指向锁记录的指针。
自旋锁:
轻量级锁在加锁过程中用到了自旋锁。所谓自旋,就是当有线程A来竞争锁时,A线程会在原地循环等待,而不是把A线程阻塞(这里阻塞指将线程挂起)。注意:自旋相当于执行一个空的for循环,也是会消耗CPU资源,所以轻量级锁适合同步代码块执行时间很快的场景,这样自旋时间不会太长。jdk1.6之前自旋是有一定次数限制的,默认10次,可以通过preBlockSpin来修改,jdk1.6之后引入了自适应自旋锁。
自适应自旋锁:自适应意味着自旋的次数不是固定的,而是根据前一次在同一个锁上自旋的时间及锁拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,那么虚拟机会认为这次自旋也可能成功,并允许这次自旋等待相对更长的时间;如果对于某个锁,自旋很少成功过,那在以后尝试获取该锁时将省略自旋过程,直接阻塞线程,避免浪费CPU资源。
轻量级锁与自旋锁的不同:
* 轻量级锁每次在退出同步代码块时都需要释放锁,而偏向锁只有在竞争时才释放锁。
* 轻量级锁进入和退出同步代码块(加锁和解锁)时都要通过cas操作更新对象头的Mark Word。
* 线程争夺轻量级锁失败时,会自旋尝试争抢。
轻量级锁加锁和释放锁的流程:
(3)重量级锁
对于重量级锁,线程只能被挂起阻塞来等待被唤醒再去重新竞争锁。
如果升级为重量级锁,synchronized作用在不同位置,底层实现原理不同:
* 加在代码块上,是通过监视器monitorenter(monitor+1)和monitorexit(monitor-1)控制的。
* 加在方法上,是通过ACC_SYNCHRONIZED(存储在运行时常量池中的method-info结构中)标志位来控制,如果一个线程进入同步方法,判断该标志位被设置,会尝试获取monitor。
monitor依赖操作系统的MutexLock(互斥锁)来实现,线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态和内核态之间来回切换,严重影响性能。
3、wait和notify/notifyAll
一般禁用偏向锁,以及偏向锁的撤销可以参考这篇文章:https://blog.csdn.net/qq_42046105/article/details/122374767?spm=1001.2014.3001.5501