synchronized悲观锁

synchronized悲观锁

小龙 684 2020-03-19

0

synchronized是java中解决并发问题最简单的一种常用方法,也是最简单的一种方法。synchronized“同步”,在开始学习并发编程的时候,synchronized也算百试不爽的良药,但是随着学习的深入,知道了synchronized在java未优化之前是一个重量级锁,相对于Lock而言,它显得很笨重,以至于开始觉得它的性能并不高效而慢慢摒弃它。但是在JDK 1.6 Java多synchronized进行了一些列的优化之后,synchronized显得并不那么笨重了。以下是对synchronized的一些学习过程
先了解一下线程同步:

  • 线程同步:当有一个线程对内存进行操作时,其他的线程不能对这块内存地址进行操作,其他的线程处于等待状态,直到该线程操作完成,其他的线程才能操作该内存
  • 有两种方式可以管理并发控制:乐观锁悲观锁

|- 乐观锁:很乐观的认为不会存在严重的并发情况,所以每次取数据的时候不进行上锁,但是在更新数据的时候会判断一下在此期间有没有其他线程也修改了同一个数据;如:CAS
|- 悲观锁:悲观的认为一定会存在验证的并发情况,所以每次操作数据都会进行上锁,无论是读取还是更新,其他的线程要想操作数据就会进入阻塞,直到拿到锁;synchronized就是悲观锁机制。

一、基本使用

synchronized的作用主要有三个:

  • 原子性:确保线程互斥的访问同步代码
  • 可见性:保证共享变量的修改可以及时可见,其实是通过Java内存模型中的“对一个变量unlock操作之前,必须同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量之前,,需要重新从主内存中load操作或assign操作初始化变量值”来保证的
  • 有序性:有效的解决重排序问题,既一个unlock操作先行发送(happen-before)于后面对同一个锁的lock操作
    从语法上来讲,synchronized可以将任意非null对象作为“锁”,在HsotSpot JVM实现中,锁有个专门的名字:对象监视器(Object Monitor)。

synchronized也有三种用法

  • 当synchronized作用在实例方法时:监视器锁(monitor)便是对象实例(this)
  • 当synchronized作用在静态方法时:监视器锁(monitor)便是对象Class实例,因为Class数据存在永久代,因此静态方法锁相当于该类的一个全局锁
  • 当synchronized作用在某一个对象实例时:监视器锁(montor)便是括号括起来的对象实例
    注意synchronized内置锁是一种对象锁
    (锁的是对象而非引用变量),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。可重入的做大作用是避免死锁,如:子类同步方法调用了父类的同步方法,如果没有重入特性,则会发送死锁

二、同步原理

synchronized的同步是在软件层面依赖JVM,而j.u.c.Lock是在硬件层面依赖特色的CPU指令。
当一个线程访问同步代码块是,首先是需要获得锁,才能继续执行的同步代码,当退出或抛出异常时必须释放锁,观察下面代码

public class TestDemo {
    public void method(){
        synchronized (this){
            System.err.println("Method 1 start");
        }
    }
}

查看反编译后的结果
syn1.png
从反编译后的代码而已看出几点:
1、monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用是就会处于锁定状态,线程执行到monitorenter指令时就会尝试或monitor的所有权,具体过程如下

1、如果monitor的进入数为0,则该线程进入monito,然后将进入数设置为1,该线程即为monitor的所有者
2、如果线程已经占有了monitor,只是重新进入,则进入monitor的进入数 +1
3、如果其他线程占用了monitor,则该线程必须进入到阻塞状态,知道monitor的进入数为0,再重新尝试获取monitor的所有权。

2、monitorexit:执行monitorexit的线程必须是objectre所对应的monitor的所有者。指令执行时,monitor的进入数 -1 ,如果 -1后的进入数为0,那么线程退出monitor,不在是这个monitor的所有者。其它被这个monitor阻塞的线程可以去尝试获取这个monitor的所有权

从反编译结果可以看出“monitorexit”指令出现了两次,第一次为同步正常退出释放锁,第二次为发生异常退出释放锁;

现在应该很清除的看出synchronized的实现原理,synchronized的底层是通过一个monitor的对象来完成的,其实wait/notify等方法也是依赖于monitor对象,这就是为什么只能在同步代码块或同步方法中才能调用“wait或notify”等方法,否则会抛出“java.lang.IllegalMonitorStateException”异常

观察同步方法

public class TestDemo {
    public synchronized void method(){
            System.err.println("Method 1 start");
    }
}

反编译后的结果

syn2.png

通过反编译结果可以看到,在同步方法上面,并没有通过“monitorenetr”和“monitorexit”指令完成上锁和释放锁(理论上也是通过这两条指令来完成的),不过相对于普通的方法,其常量池里面多了ACC_SYNCHRONIZED标识符。JVM就是根据该标识符来实现方法的同步的

当方法调用时,调用指令将会检查方法的“ACC_SYNCHRONIZED”访问标志是否被设置,如果设置了,线程执行时会先获取monitor,获取成功之后才能执行方法体,方法体执行完后释放monitor。在方法执行期间其他任何线程都无法再获得同一个monitor对象

同步代码块和同步方法这两种方式本质上是没有区别的,只是方法的同步是一种隐式的方式来获取的,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起,等待重新调度,会导致“用户态”和“内核态”两个状态之间来回切换,对性能的影响较大。

3、同步概念

3.1 Java对象头

在JVM中,对象在内存中的布局分为三块:对象头,实例数据和对齐填充,如下图所示

syn4.png

1、数据实例:存放类的属性信息,包括父类的属性信息
2、 对齐填充:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了对字节对齐
3、 对象头:java对象头一般只占两个机器码(在32位虚拟机中,一个机器码等于四个字节,也就是32bit,在64位虚拟机中,一个机器码对应八个字节,也就是64bit),但是如果对象是数组类型的,则需要设置三个机器码,因为JVM虚拟机可以通过java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组大小,多出来的一个机器码用来记录数组的长度。

synchronized用的锁就是存在对象头里的;什么是对Java象头呢?Hotspot虚拟机的对象头主要包括两部分数据组成:Mark Word(标记字段)Class Pointer(类型指针)。其中Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象是那个类的实例,Mark Word用于存储对象自身的运行时数据,他是实现轻量级锁和偏向锁的关键。Java对象头具体结构描述如下表

长度内容说明
32/64bitMark Word存储对象的hashCode或锁信息等
32/64bitClass Metadata Address存储到对象类型数据的指针
32/64bitArray Length数组长度(如果当前对象是数组)

Make Word用于存储对象自身的运行时数据,如:哈希码(HashCode)、GC分代年龄、锁的状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。观察无锁状态下Mark Word部分的存储结构

25bit4bit1bit是否偏向锁2bit锁标志位
无锁状态对象的HashCode对象分代年龄001

对象头信息是对象自身定义的与数据无关的额外存储成本,但是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的存储空间中存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发送改变,可能变化为以下4种数据

syn5png.png

在64位虚拟机下,Make Word是64bit大小的,其存储结构如下
syn8.png

对象头的最后两位存储了锁的标志,01是初始状态,未加锁,其对象头里存储的是对象本身的哈希码,随着锁级别的不同,对象头里会存储不同的内容。偏向锁存储的是当前占用此对象的线程ID;而轻量级则存储的指向线程栈中锁记录的指针。我们可以观察出“锁”这个东西可能是个锁记录+对象头里的引用指针(判断线程是否拥有锁时将线程的锁记录地址和对象头里的指针地址比较),也有可能是对象头里的线程ID(判断线程是否拥有锁时将线程的ID和对象头里存储的线程ID比较)。
syn9.png

3.2 对象头中Mark Word与线程中Lock Record

在线程进入同步代码块的时候,如果此同步对象没有被锁定,及它的锁标志位是01,虚拟机首先在当前线程的栈中创建我们称之为“锁记录(Lock Record)”的空间,用于存储锁对象的Mark Word的拷贝,官方把这个拷贝记录称为“Displaced Mark Word”
整个Mark Word及其拷贝至关重要。
Lock Record是线程私有的数据结构,每一个线程都有一个可以Lock Record列表,同时还有一个全局可以列表。每一个被锁住对象的Mark Word都会和一个Lock Record关联(对象头的Mark Word中的Lock Word指向Lock Record的起始地址),同时Lock Record中有一个Owner字段存放拥有该锁的线程唯一标识,(或者object mark word),表示该锁被这个线程占用,如下表所示为Lock Record的内部结构

LockRecord描述
Owner初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL
EntryQ关联一个系统互斥锁(semaphore),阻塞所有视图锁住monitor record失败的线程
RCThis表示blocked或waiting在monitor record上的所有线程个数
Nest用来实现重入锁的计数
HashCode保存从对象头拷贝过来的HashCode值(可能还包括GC age)
Candidata用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程都要唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后又因为竞争锁失败又被阻塞)从而导致性能严重的下降,Candidata只有两种可能的值: 0:表示没有需要唤醒的线程; 1:表示要唤醒一个继任线程来竞争锁

3.3监视器(Monitor)

任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。synchronized在JVM
里的实现都是
基于进入和退出Monitor对象来实现方法同步和代码块同步的
,虽然具体的实现细节不一样,但都是可以通过成对的“monitorenter”,“monitorexit”指令来实现的。

1、monitorenter指令:插入在同步代码块的开始位置,当代码执行到该指令时,将尝试获取该对象Monitor的所有权,级尝试获得该对象的锁。
2、 MonitorExit指令;插入在方法结束处和异常处,JVM保证每个MonitorEnter都必须有对应的MonitorExit

Monitor可以理解为一个同步工具,也可以描述成一种同步机制,它通常被描述为一个对象。
与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都为成Monitor的潜力,因为在Java的设计中,每一个Java对象自打创建就带了一把看不见的锁,它叫做内部锁或者Monitor锁。
也就是synchronized的对象锁,Mark Word锁标识为10,其中指针指向的是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的(ObjectMonitor是C++编写),其主要结构如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor中有两个队列,_WaitSet和_EntryList,用来保存ObjectMonitor对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段代码时。

1、首先会进入_EntryList集合,当线程获取到对象的monitor后,进入_Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count +1;
2、若线程调用wait()方法,将释放当前持有的monitor,owner回复为null,count -1,同时该线程进入WaitSet集合中等待被唤醒
3、若当前线程执行完毕,也将释放monitor(锁)并复位count值,以便其他线程进入后去monitor

同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针指向),Synchronized锁便是通过这种方式获取锁的,也就是为什么Java中任意对象可以作为锁的原因,同时notiify/notifyAll/wait等方法会使用到monitor锁对象,所以必须在同步代码块中使用。
监视器monitor有两种方式:互斥协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程访问。
什么时候需要协作

一个线程向缓冲区写数据,另一个线程从缓冲区读数据,如果读线程发现缓冲区空就会等待,当写线程向缓冲区写入数据,就会唤醒读线程,这里的读线程和写线程就是一个合作关系。JVM通过Object类的wwait方法来使自己等待,在调用wait方法之后,该线程会释放它持有的监视器,这个等待的线程并不会马上执行,而是要通知线程释放监视器后,它重新获取监视器才有执行机会。如果刚好唤醒的这个线程需要的监视器被其他线程占用,那么这个线程会继续等待,Object类中的notifyAll方法可以解决这个问题,它可以唤醒所有等待的线程,总有一个线程执行。
syn10.png
如上图所示,一个线程通过1号门进入Entry Set(入口区),如果在入口区没有线程等待,那么这个线程就会获取监视器成为监视器的Owner,然后执行监视区域的代码。如果在入口区中有其它线程在等待,那么新来的线程也会和这些线程一起等待。线程在持有监视器的过程中,有两个选择,一是正常执行监视区域的代码,释放监视器,通过5号门退出监视器;二是可能等待某个条件出现,于是它会通3号门到Wait Set(等待区)休息,直到相应的条件满足后在通过4号门进入重新获取监视器再执行。

注意

当一个线程释放监视器时,在入口区和等待区等待的线程都回去竞争监视器,如果入口区的线程赢了,会从2号门进入;如果等待区的线程赢了会从4号门进入。只有通过三号门才能即进入等待区,在等待区的线程只有通过4号门才能退出等待区,也就是说一个线程只有在持有监视器才能执行wait操作,处于等待的线程只有再次获得监视器才能退出等待状态。

4 锁的优化

从JDK1.5引入了现代操作系统新增了CAS原子操作(JDK1.5中并没有对synchronized关键字做优化,而是体现在J.U.C中,所以在该版本中concurrent包有更好的性能),从JDK1.6开始,java就对synchronized的实现机制进行了较大的调整,包括使用JDK1.5引进的CAS自旋,除此之外还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。基于这些优化Synchronized关键字的性能得到极大的提高,同时语义清晰 ,操作简单,无需手动关闭,因此在允许的情况下尽量使用此关键字。

锁主要存在有四种状态:无状态、偏向锁状态、轻量级状态锁、重量级状态锁,锁可以从偏向锁升级到轻量级锁,从轻量级锁升级到重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级

在JDK1.6中默认是开启偏向锁和轻量级锁的,可以通过-XX:UseBiaseLocking参数来禁用偏向锁

4.1 自旋锁

线程的阻塞和唤醒是需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力,。同时我们发现在许多的应用上面,对象的锁状态只会持续很短一段时间,为了这一段很短的时间频繁的阻塞和唤醒线程是非常不值得的,为了优化引入了自旋锁
什么是自旋锁?

所谓的自旋锁是指当一个线程尝试获取某个对象的锁时,发现该对象的锁已经被其他线程占用了,那么此时该线程就一直循环检测这个对象的锁是否被释放了,而不是进入挂起状态或休眠状态。

自旋锁适用于锁保护的临界区很小的情况,临界区很小的化,锁占用的时间就会很短。自旋锁并不代替阻塞,虽然它可以避免线程状态切换带来的性能损耗,但是它占用了CPU处理的时间,如果持有对象锁的线程很快的就释放了锁,那么自旋锁的效率就非常的高,但是如果长时间不释放锁,那么线程就会一直自旋等待,这就会白白的浪费CPU资源,而且在自旋期间它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。所有自旋锁的自旋等待时间(自旋的次数)必须 有一个限度,如果自旋超出了规定的时间还没有获取到锁,则应该被挂起

自旋锁是在JDK 1.4.2中引入的,默认是关闭的,但可以使-XX:+UseSpinning开启,JDK1.6就默认开启了,同时其默认自旋的次数为10次,可以通过参数-XX:PreBlockSpin来跳转自旋次数

但是如果通过参数-XX:PreBlockSpin来调整自旋次数,会带来诸多不便,线程对锁的占用时间长短不一,如果设置的次数太大也会造成资源的浪费。于是在JDK1.6引入了自适应的自旋锁,让虚拟机变得越来越聪明

4.2适应性自旋锁

从JDK1.6开始引入更加聪明的自旋锁,即自适应自旋锁,所谓的自适应就意味着自旋的次数不在固定,它是由前一次在同一个锁上线程的自旋时间和锁拥有者的状态来决定的。如何进行适应性自旋呢?

线程如果自旋成功了,那么下一次自旋的次数就会加多,因为虚拟机认为上一次既然自旋成功了,那么此次自旋也很有可能会成功,那么它就会允许自旋等待的次数更多,反之
对于某个锁,很少有能自旋成功的,那么在以后获取这个锁的时候自旋的次数会减少,甚至省略掉自选你过程,以避免处理器资源浪费

有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对锁的情况预测会越来越准确,虚拟机将会变得越来越聪明。

4.3 锁消除

为了保证数据的完整性,在进行操作的时需要对这部分操作进行同步控制,**但是在某些情况下,JVM检测到不可能存在共享数据竞争,这时JVM就会对这些同步锁进行消除。

锁消除的依据是逃逸分析的数据支持*

如果不存在竞争,为什么还要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据分析来确定,但是对于程序员来说还不清楚吗?在明明知道不会存在竞争的代码块前加上同步?但是程序有时候并不我们想的那样?虽然没有显示使用锁,但是在一些JDK内置API是,入StringBuffer、Vector、HashTable等,这时候会存在隐式的加锁操作。比如StringBuffer的append()方法,Vector的add()方法。

public class TestDemo{
    public static void main(String[] args) {
        Vector<String> vector = new Vector<>();
        for (int x = 0 ;x<10;x++) {
            vector.add("synchronized"  + x);
        }
        System.err.println(vector);
    }
}
// add源码
public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
}

通过源码可以看出,在add()方法上使用了Synchronized关键字,在运行上面代码的时候,**JVM可以明显的检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以放心大胆地将vector内部的加锁操作消除、

4.4 锁粗化

在使用同步锁的时候,需要让同步代码块的作用范围尽可能小 —— 仅在共享数据的实际作用域中才进行同步,这样作的目的是为了使需要同步的操作数量尽可能缩小,如果存在竞争锁,那么等待锁的线程也能尽快拿到锁

锁的粗化比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。

就如上面的代码示例

vector在每次add()的时候都需要加锁操作,JVM检测到同一个对象(Vector)连续加锁、解锁操作,就会合并成一个范围更大的加锁、解锁操作,既加锁操作解锁操作会移到for循环之外。

4.5 偏向锁

偏向锁是JDK1.6重要的引进,因为JVM虚拟机的作者经过研究实践发现,在打多数的情况下,锁不仅不存在多线程竞争中,而且总是由同一线程多次获得,为了让线程后的锁的代价更低,引进了偏向锁。

偏向锁是在单线程执行代码块使使用的机制,如果在多线程并发的环境下(既线程A尚未执行万同步代码块,线程B就发起了申请锁的请求),则一定会转化为轻量级或重量级锁。

在JDK1.5中偏向锁默认是关闭的,但是到了JDK1.6偏向锁已经默认开启了。如果并发数较大同事同步代码快执行时间比较长的,则被多个线程同时访问的几率就很大,就可以通过参数-XX:UseBiaseLocking来禁止偏向锁(但这是个JVM参数,不能针对某一个对象锁来单独设置)。

引入偏向锁主要的目的是:为了在没有多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。因为轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令的,而偏向锁只需要置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗也必须小于节省下来的CAS原子指令的性能消耗)

轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁是在只有一个线程执行同步块时进一步提高性的。

探究一下偏向锁如何减少不必要的CAS操作?首先来看一下无竞争下锁存在什么问题

现在几乎所有的锁都是可重入的,既已获得锁的线程可以多次锁住/解锁监视对象,按照之前的JVM虚拟机设计,每次加锁解锁都会涉及到一些CAS操作(比如对等待队列的CAS操作),CAS操作会延迟本地调用,因此偏向锁的想法是,一旦线程第一次获得了对象的锁,之后便让这个对象偏向这个线程,之后的多次调用则可以避免CAS操作,说白了就是设置个变量,如果发现为true则无需再走各种加锁/解锁流程

**CAS为什么会引入本地延迟?这需要从SMP(对称多处理器)架构说起
syna.png

其意思就是**所有的CPU会共享一条消息总线(BUS),靠此总线连接主存,每个核都有自己的一级缓存,各核相对于BUS对称分,因此这种架构称为“对称多处理器”

而CAS的全称是Compare-And-Swap,是一条CPU的原子指令,其作用是让CPU比较后原子的更新某个位置的值,其实现方式是基于硬件平台的汇编指令,也就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,哪些AtomicInteger原子操作类便是使用了这些封装后的接口。

例如:Core1和Care2可能会同时把主存中某个位置的值Load自己的L1 cache中,**当Core1在自己的L1 cache中修改了这个值,就会通过消息总线(BUS)是Core2中L1 cache对应的值“失效”,而 Core2一旦发现自己L1 cache中的值失效(称Cache命中缺失)则会通过总线从内存中加载该地址的最新值,大家通过总线的来回通信成为“Cache一致性流量”,因为总线被设计为固定的“通信能力”,如果Cache一致性流量过大,总线将成为瓶颈。而当Core1和Core2中的值再次一致时,成为“cache一致性”,从这个层面来说,锁设计的终极目的便是减少Cache一致性流量

而CAS恰好会导致Cache一致性流量,如果很多线程共享同一个对象,当某个Core CAS成功是必然会引起总线风暴,这就是所谓的本地延迟,本质上偏向锁是为了消除CAS,降低Cache一致性流量

所以当一个线程访问同步代码快获取锁的时候,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程进入和退出同步代码块时不需要花费CAS操作争夺锁的资源,只需要检查是否为偏向锁,锁的标识以及ThreadID即可,处理流程如下:

1、检测Mark Word是否为可偏向状态,既是否为偏向锁:1,锁的标识符:01
2、若为可偏向状态,则检测线程ID是否为当前线程ID,如果是者执行步骤(5),否则执行步骤(2)
3、如果测试线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的先ID替换为当前线程ID,否则执行步骤(4)
4、通过CAS竞争锁失败,证明当前存在多线程竞争情况,当达到全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,随后被阻塞在安全点的线程继续往下执行同步代码块
5、执行同步代码块

偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程不会主动去释放偏向锁,需要等待其他线程来竞争锁。偏向锁的撤销需要等待全局安全点(这个时间点上没有正则执行的代码)。其步骤如下:

1、暂停拥有偏向锁的线程
2、判断锁对象是否还被处于锁定状态,否,则恢复到无锁状态(01),运行其余线程及竞争锁。是,则挂起持有锁的当前线程,并将指向当前线程的锁记录地址的指针放入对象头的Mark Wor,升级为轻量级锁(00),然后恢复持有锁的当前线程,进入轻量级锁的竞争模式。
注意:此处将当前线程挂起再恢复的过程中并没有发生锁的转移,任然在当前线程手中,只是穿插了该“将对象头的线程ID更变为执行锁记录地址的指针”这么个事