无锁、偏向锁、轻量级锁、重量级锁
1、偏向锁、轻量级锁、重量级锁适用于不同的并发场景
-
偏向锁:无实际的锁竞争,且将来只有第一个申请锁的线程会使用锁。偏向锁只有初始化时需要一次CAS
-
轻量级锁:无实际的锁竞争,多个线程交替使用锁,允许短时间的锁竞争。轻量级锁每次申请、释放锁都至少需要一次CAS
-
重量级锁:有实际的锁竞争,且锁竞争时间长。
2、锁升级、撤销的流程图
3、内置锁和显示锁
内置锁:也就是Synchronized锁
-
由Synchronized实现的内置锁才能锁升级,从偏向锁-轻量级锁-重量级锁
-
执行完同步代码,会自动的释放锁
-
基于JVM实现,可以对Synchronized锁进行增加锁粒度和减低锁粒度(修饰实例方法、修饰静态方法、修饰代码块
-
相对显示锁来说,内置锁还是过重,因为内置锁是一个互斥锁,不仅读写互斥并且读读也互斥,最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁。
-
synchronized关键字不能继承,父类方法中加了synchronized,在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,需要重新加锁。
-
内置锁是非公平锁,线程在竞争synchronized锁时并不遵循先到等待队列就先获得锁,如果一个线程来请求锁,刚好该锁被释放了,那么这个线程可能会跳过在等待队列中的其它线程直接获得该锁。
-
内置锁是可重入锁,如果已经获取了一个锁对象,在还没释放时又要执行该锁对象的另一个代码块或方法,则不需要再次给这个对象加锁就可以直接执行。
-
sychronized 作用于实例方法时,锁对象是this
-
sychronized 作用于静态方法时,锁对象是Class对象
-
sychronized 作用于代码块时,锁对象是sychronized(obj)中的obj
显示锁:ReentrantLock,必须手动的释放锁
4、内置锁与显示锁的区别:
1、锁的释放 显示锁必须调用unlock方法才能释放锁,内置锁只要运行到同步代码块之外就会释放锁 2、公平性 显示锁可以指定公平策略,默认为不公平锁 内置锁不可以选择公平策略,只能是不公平锁 3、可中断申请 显示锁提供可中断申请(lock.lockInterruptibly();可中断申请, 在申请锁的过程中如果当前线程被中断, 将抛出InterruptedException异常) 内置锁不可中断。(在申请锁时被其它线程持有,那么当前线程后挂起,挂起其间不可中断) 4、可尝试申请、可定时申请 显示锁提供尝试型申请方法(Lock.tryLock和Lock.tryLock(long time, TimeUnit unit)), 内置锁不提供这种特性 5、是否可以精确唤醒特定线程 显示锁可以通过Condition对象(由显示锁派生出来),调用Condition.singal或Condition.singalAll方法可以唤醒在该Condition对象上等待的线程。以此来唤醒指定线程。 内置锁的notify或notifyAll方法唤醒在其上等待的线程,但无法指定特定线程。 总结,内置锁够解决大部分需要的场景,只有在需要额外的灵活时,比如公平、可中断、可尝试、可定时、可唤醒特定线程时,我们才考虑用显示锁。 6、偏向锁
流程讲解
当JVM启用了偏向锁模式(JDK6以上默认开启),新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。
偏向锁逻辑 1.线程A第一次访问同步块时,先检测对象头Mark Word中的标志位是否为01,依此判断此时对象锁是否处于无所状态或者偏向锁状态(匿名偏向锁);
2.然后判断偏向锁标志位是否为1,如果不是,则进入轻量级锁逻辑(使用CAS竞争锁),如果是,则进入下一步流程;
3.判断是偏向锁时,检查对象头Mark Word中记录的Thread Id是否是当前线程ID,如果是,则表明当前线程已经获得对象锁,以后该线程进入同步块时,不需要CAS进行加锁,只会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,用来统计重入的次数(如图为当对象所处于偏向锁时,当前线程重入3次,线程栈帧中Lock Record记录)。
偏向锁重入
退出同步块释放偏向锁时,则依次删除对应Lock Record,但是不会修改对象头中的Thread Id;
注:偏向锁撤销是指在获取偏向锁的过程中因不满足条件导致要将锁对象改为非偏向锁状态,而偏向锁释放是指退出同步块时的过程。
4.如果对象头Mark Word中Thread Id不是当前线程ID,则进行CAS操作,企图将当前线程ID替换进Mark Word。如果当前对象锁状态处于匿名偏向锁状态(可偏向未锁定),则会替换成功(将Mark Word中的Thread id由匿名0改成当前线程ID,在当前线程栈中找到内存地址最高的可用Lock Record,将线程ID存入),获取到锁,执行同步代码块;
5.如果对象锁已经被其他线程占用,则会替换失败,开始进行偏向锁撤销,这也是偏向锁的特点,一旦出现线程竞争,就会撤销偏向锁;
6.偏向锁的撤销需要等待全局安全点(safe point,代表了一个状态,在该状态下所有线程都是暂停的),暂停持有偏向锁的线程,检查持有偏向锁的线程状态(遍历当前JVM的所有线程,如果能找到,则说明偏向的线程还存活),如果线程还存活,则检查线程是否在执行同步代码块中的代码,如果是,则升级为轻量级锁,进行CAS竞争锁;
注:每次进入同步块(即执行monitorenter)的时候都会以从高往低的顺序在栈中找到第一个可用的Lock Record,并设置偏向线程ID;每次解锁(即执行monitorexit)的时候都会从最低的一个Lock Record移除。所以如果能找到对应的Lock Record说明偏向的线程还在执行同步代码块中的代码。
7.如果持有偏向锁的线程未存活,或者持有偏向锁的线程未在执行同步代码块中的代码,则进行校验是否允许重偏向,如果不允许重偏向,则撤销偏向锁,将Mark Word设置为无锁状态(未锁定不可偏向状态),然后升级为轻量级锁,进行CAS竞争锁;
8.如果允许重偏向,设置为匿名偏向锁状态,CAS将偏向锁重新指向线程A(在对象头和线程栈帧的锁记录中存储当前线程ID);
9.唤醒暂停的线程,从安全点继续执行代码。
以上便是偏向锁的整个逻辑了。
5、MarkWord的结构
6、批量重偏向和批量撤销
1)、批量重偏向与批量撤销
渊源:从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。 于是,就有了批量重偏向与批量撤销的机制。
撤销是偏向锁升级为轻量级锁或者是重量级锁的意思
2)、解决场景
批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。撤销了20次了,就要重新偏向其他线程了 批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。撤销了40了,可能已经不适合偏向锁了,所以就直接升级为轻量级锁了
3)、原理
以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。 每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该字段,其初始值为创建该对象时class中的epoch的值。 每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id。 当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。
4)、代码实现
前提是关闭延迟加载:开启偏向延迟,-XX:BiasedLockingStartupDelay=0
4.1)、批量重偏向
package com.robin.demospring.test; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; public class A { }
package com.robin.demospring.test; import lombok.extern.slf4j.Slf4j; import org.openjdk.jol.info.ClassLayout; import java.util.ArrayList; import java.util.List; import java.util.concurrent.locks.LockSupport; @Slf4j public class Test1 { static Thread t1; static Thread t2; static int loopFlag = 20; public static void main(String[] args) { final List<A> list = new ArrayList<>(); t1 = new Thread() { @Override public void run() { for (int i = 0; i < loopFlag; i++) { A a = new A(); list.add(a); log.debug("加锁前" + i + " " + ClassLayout.parseInstance(a).toPrintable()); synchronized (a) { log.debug("加锁中" + i + " " + ClassLayout.parseInstance(a).toPrintable()); } log.debug("加锁结束" + i + " " + ClassLayout.parseInstance(a).toPrintable()); } log.debug("============t1 都是偏向锁============="); //防止竞争 执行完后叫醒 t2 LockSupport.unpark(t2); } }; t2 = new Thread() { @Override public void run() { //防止竞争 先睡眠t2 LockSupport.park(); for (int i = 0; i < loopFlag; i++) { A a = list.get(i); //因为从list当中拿出都是偏向t1 log.debug("加锁前" + i + " " + ClassLayout.parseInstance(a).toPrintable()); synchronized (a) { //前20撤销偏向t1;然后升级轻量指向t2线程栈当中的锁记录 //后面的发送批量偏向t2 log.debug("加锁中 " + i + " " + ClassLayout.parseInstance(a).toPrintable()); } //因为前20是轻量,释放之后为无锁不可偏向 //但是后面的是偏向t2 释放之后依然是偏向t2 log.debug("加锁结束" + i + " " + ClassLayout.parseInstance(a).toPrintable()); } log.debug("新产生的对象" + ClassLayout.parseInstance(new A()).toPrintable()); } }; t1.start(); t2.start(); } }
查看第19次对象头打印
查看第20次对象头打印
可以看到底20次的时候,偏向了t2线程
4.2)、批量撤销
package com.robin.demospring.test; import lombok.extern.slf4j.Slf4j; import org.openjdk.jol.info.ClassLayout; import java.util.ArrayList; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.LockSupport; @Slf4j public class Test2 { static Thread t1; static Thread t2; static Thread t3; static int loopFlag = 40; public static void main(String[] args) { final List<A> list = new ArrayList<>(); ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 50, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); // threadPoolExecutor.execute(); t1 = new Thread() { @Override public void run() { for (int i = 0; i < loopFlag; i++) { A a = new A(); list.add(a); log.debug("加锁前" + i + " " + ClassLayout.parseInstance(a).toPrintable()); synchronized (a) { log.debug("加锁中" + i + " " + ClassLayout.parseInstance(a).toPrintable()); } log.debug("加锁结束" + i + " " + ClassLayout.parseInstance(a).toPrintable()); } log.debug("============t1 都是偏向锁============="); //防止竞争 执行完后叫醒 t2 LockSupport.unpark(t2); } }; t2 = new Thread() { @Override public void run() { //防止竞争 先睡眠t2 LockSupport.park(); for (int i = 0; i < loopFlag; i++) { A a = list.get(i); //因为从list当中拿出都是偏向t1 log.debug("加锁前" + i + " " + ClassLayout.parseInstance(a).toPrintable()); synchronized (a) { //前20撤销偏向t1;然后升级轻量指向t2线程栈当中的锁记录 //后面的发送批量偏向t2 log.debug("加锁中 " + i + " " + ClassLayout.parseInstance(a).toPrintable()); } //因为前20是轻量,释放之后为无锁不可偏向 //但是后面的是偏向t2 释放之后依然是偏向t2 log.debug("加锁结束" + i + " " + ClassLayout.parseInstance(a).toPrintable()); } log.debug("新产生的对象" + ClassLayout.parseInstance(new A()).toPrintable()); LockSupport.unpark(t3); } }; t3 = new Thread() { @Override public void run() { //防止竞争 先睡眠t2 LockSupport.park(); for (int i = 0; i < loopFlag; i++) { A a = list.get(i); log.debug("加锁前" + i + " " + ClassLayout.parseInstance(a).toPrintable()); synchronized (a) { log.debug("加锁中 " + i + " " + ClassLayout.parseInstance(a).toPrintable()); } log.debug("加锁结束" + i + " " + ClassLayout.parseInstance(a).toPrintable()); } log.debug("新产生的对象" + ClassLayout.parseInstance(new A()).toPrintable()); } }; t1.start(); t2.start(); t3.start(); } }
查看t3线程打印
新创建的a对象也是无锁不可偏向的
7、偏向锁和hashcode是否可以共存?
我们知道,Java对象头的结构如下:
内容 | 说明 | 备注 |
---|---|---|
Mark Word | 存储对象的Mark Word信息 | - |
Class Metadata Address | 存储指向对象存储类型的指针 | - |
Array Length | 数组的长度 | 只有数组对象有该属性 |
其中,在32位下,Mark Word的存储结构如下:
在64位下,Mark Word的存储结构如下:
由此可知,在无锁状态下,Mark Word中可以存储对象的identity hash code值。当对象的hashCode()方法(非用户自定义)第一次被调用时,JVM会生成对应的identity hash code值(生成方式参见参考博客2),并将该值存储到Mark Word中。后续如果该对象的hashCode()方法再次被调用则不会再通过JVM进行计算得到,而是直接从Mark Word中获取。只有这样才能保证多次获取到的identity hash code的值是相同的(由参考博客2可知,以jdk8为例,JVM默认的计算identity hash code的方式得到的是一个随机数,因而我们必须要保证一个对象的identity hash code只能被底层JVM计算一次)。
我们还知道,对于轻量级锁,获取锁的线程栈帧中有锁记录(Lock Record)空间,用于存储Mark Word的拷贝,官方称之为Displaced Mark Word,该拷贝中可以包含identity hash code,所以轻量级锁可以和identity hash code共存;对于重量级锁,ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中也可以存储identity hash code的值,所以重量级锁也可以和identity hash code共存。
对于偏向锁,在线程获取偏向锁时,会用Thread ID和epoch值覆盖identity hash code所在的位置。如果一个对象的hashCode()方法已经被调用过一次之后,这个对象还能被设置偏向锁么?答案是不能。因为如果可以的化,那Mark Word中的identity hash code必然会被偏向线程Id给覆盖,这就会造成同一个对象前后两次调用hashCode()方法得到的结果不一致。
HotSpot VM的锁实现机制是:
-
当一个对象已经计算过identity hash code,它就无法进入偏向锁状态;
-
当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为轻量级锁或者重量锁;
-
轻量级锁的实现中,会通过线程栈帧的锁记录存储Displaced Mark Word;重量锁的实现中,ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中可以存储identity hash code的值。
8、偏向锁相关的参数
启动默认是有延迟加载的,会有一点时间之后才会加载偏向锁成功
下面的这些参数可以控制偏向锁的开启,关闭等
# 开启偏向锁 -XX:+UseBiasedLocking # 关闭偏向锁 -XX:-UseBiasedLocking # 关闭偏向锁延迟 -XX:BiasedLockingStartupDelay=0 # 设备偏向锁的延迟时间是4000毫秒 -XX:BiasedLockingStartupDelay=4000 # 查看所有的 JVM 参数 -XX:+PrintFlagsFinal # 设置重偏向阈值 -XX:BiasedLockingBulkRebiasThreshold=20 # 批量重偏向距离上次批量重偏向的后重置的延迟时间 -XX:BiasedLockingDecayTime=25000 # 设置批量撤销阈值 -XX:BiasedLockingBulkRevokeThreshold=40
加入这个依赖,可以看到对象的mark word里面的字段
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.8</version> </dependency>
9、偏向状态:
-
无锁不可偏向:在支持偏向之前创建的对象都是无锁状态。001不可偏无锁
-
匿名偏向:支持可偏向之后,创建的对象就可以偏向于某一个线程了,但是此时没有偏向任何线程,这样就属于匿名偏向
public class BiasedLockDemo1 { public static void main(String[] args) throws InterruptedException { // JVM 虚拟机启动的时候创建 Model model1 = new Model(); // 无锁 System.out.println(ClassLayout.parseInstance(model1).toPrintable()); Thread.sleep(4001); // JVM 启动 4 秒后创建对象 Model model2 = new Model(); // 偏向锁 System.out.println(ClassLayout.parseInstance(model2).toPrintable()); } }
上面第一次的mark word 中的可偏向+锁标识,可偏向是1bit,偏向时为1,不偏向时为0;锁表示占2bit,01为无锁或偏向锁,00为轻量级锁,10为重量级锁,11为GC可以清楚的表示。
第一次的输出的可偏向和锁表示是:001
第二次的输出的可偏向和锁表示是:101
-
已偏向:此时如果有一个线程去使用这个对象,那么这个这个线程就是偏向锁了。这个对象的mark word中的就会保存这个线程的线程id
public class BiasedLockDemo2 { public static void main(String[] args) throws InterruptedException { // JVM 虚拟机启动的时候创建 Model model1 = new Model(); // 无锁 System.out.println(ClassLayout.parseInstance(model1).toPrintable()); Thread.sleep(4001); // JVM 启动 4 秒后创建对象 Model model2 = new Model(); // 偏向锁 System.out.println(ClassLayout.parseInstance(model2).toPrintable()); synchronized (model2) { // 偏向锁 System.out.println(ClassLayout.parseInstance(model2).toPrintable()); } } }
锁表示为偏向锁:101
可以在对象的mark word中看到偏向线程的id
无锁状态不能变成偏向锁!
在偏向锁还未启动的是,创建的对象会是无锁状态的,但是偏向锁启动成功之后,就是偏向锁了,但是成为偏向锁之后变为无锁,之后就不可能再变为偏向锁了
-
可重偏向:在持有偏向锁的class的epoch达到20之后,这个class的epoch就会失效,这时再有其他的线程t2操作这个class的时候,就会重新偏向t2线程。
10、轻量级锁
-
线程在执行同步块之前,会在栈帧里创建一个存储锁记录(Lock Record)的空间,并把对象头里的Mark Word复制到锁记录里(官方成为Displaced Mark Word),然后JVM会使用CAS操作将对象头里的Mark Word更改为指向锁空间的指针。
-
如果更新成功了就获取到这个对象的轻量级锁,
-
如果更新失败了首先会检查对象的Mark Word是否指向当前的线程,如果指向当前的线程,说明该线程已经获取这个这个对象的锁了,继续执行同步块代码。
-
如果不指向当前线程,表示有其他线程竞争锁,当前线程便尝试自旋获取锁。如果在这过程中获取到了,那就执行同步块代码。
-
如果自旋一定次数还没竞争到锁,就将锁升级为重量级锁,当前线程阻塞。
-
如果持有线程释放锁失败(CAS替换Mark Word,因为有其他线程在争夺锁),那么将释放锁并唤醒等待的线程
1)、轻量级锁的获取过程
(1)在代码进入同步块的时候,如果同步对象锁状态为偏向状态(就是锁标志位为“01”状态,是否为偏向锁标志位为“1”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。官方称之为 Displaced Mark Word(所以这里我们认为Lock Record和 Displaced Mark Word其实是同一个概念)。这时候线程堆栈与对象头的状态如图所示:
(2)拷贝对象头中的Mark Word复制到锁记录中。
(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象头的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向对象头的mark word。如果更新成功,则执行步骤(4),否则执行步骤(5)。
(4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如下所示:
(5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,现在是重入状态,那么设置Lock Record第一部分(Displaced Mark Word)为null,起到了一个重入计数器的作用。下图为重入三次时的lock record示意图,左边为锁对象,右边为当前线程的栈帧,重入之后然后结束。接着就可以直接进入同步块继续执行。
如果不是说明这个锁对象已经被其他线程抢占了,说明此时有多个线程竞争锁,那么它就会自旋等待锁,一定次数后仍未获得锁对象,说明发生了竞争,需要膨胀为重量级锁。
2)、轻量级锁的解锁过程
(1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
(2)如果替换成功,整个同步过程就完成了。
(3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。
11、重量级锁
重量级锁是通过互斥量(Mutex)来实现的 ,一个线程获取到锁进入同步块,在没有释放锁之前,会阻塞其他未获取锁的线程
1)、重量级锁加锁和释放锁机制
1.调用omAlloc分配一个ObjectMonitor对象,把锁对象头的mark word锁标志位变成 “10 ”,然后在mark word存储指向ObjectMonitor对象的指针
2.ObjectMonitor对象中有两个队列,WaitSet和EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象), 3.ObjectMonitor对象中也有一个变量owner,owner指向持有ObjectMonitor对象的线程(就是当前正在执行的线程)
当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合
当线程获取到ObjectMonitor对象后,会判断monitor计数器是否是0,如果不是,在进入到_EntryList等待集合里面,如果是0,在表示可以执行同步代码,此时会把monitor中的owner变量设置为当前线程,同时把monitor计数器count加1
若线程调用wait()方法,将释放当前持有的monitor,owner变量恢复为null,monitor的计数器count自减1,同时该线程进入WaitSet集合中等待被唤醒。
若当前线程执行完毕,也将_owner变量恢复为null,并且会monitor的计数器count自减1,以便其他线程进入获取monitor。如下图所示
12、偏向锁升级为轻量级锁
13、偏向锁、轻量级锁、重量级锁的对比
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁都不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 只有一个线程访问同步块 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间同步块执行速度非常块 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间慢 | 追求吞吐量同步块执行时间较长 |
14、锁的其他优化
1)、适应性自旋(Adaptive Spinning)
从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
2)、锁粗化(Lock Coarsening)
锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。举个例子
public void lockCoarsening() { int i=0; synchronized (this){ i=i+1; } synchronized (this){ i=i+2; } }
上面的两个同步代码块可以变成一个
public void lockCoarsening() { int i=0; synchronized (this){ i=i+1; i=i+2; } }
3)、锁消除(Lock Elimination)
锁消除即删除不必要的加锁操作的代码。比如下面的代码,下面的for循环完全可以移出来,这样可以减少加锁代码的执行过程