目录

一、概述

二、CAS概述和作用

三、synchronized锁升级过程

四、偏向锁

五、轻量级锁

六、重量级锁

七、锁的优缺点对比

八、总结


一、概述

在JDK1.6之前,synchronized的性能比较差,在JDK1.6中,JDK官方对synchronized进行了一些优化,比如锁升级、锁消除等等。这些优化很多都涉及到CAS操作,了解了CAS,有利于我们理解synchronized底层锁升级的过程,话不多说,先看下什么是CAS?

二、CAS概述和作用

  • CAS的全称是: Compare And Swap(比较并交换)。CAS操作涉及到3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧的预估值X等于内存中的值V,就将新的值B保存到内存中。
  • CAS的作用:CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证,CAS可以保证共享变量赋值时的原子操作。

下面我们通过一个示例来看看CAS怎么实现无锁并发:

定义一个共享变量num,然后启动多个线程,同时执行1000次num++.

import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;public class CasDemo {public static void main(String[] args) throws InterruptedException {//定义一个共享变量AtomicInteger numAtomicInteger = new AtomicInteger();ArrayList<Thread> threads = new ArrayList<>();//启动五个线程同时进行num++for (int i = 0; i < 5; i++) {Thread t = new Thread(() -> {for (int j = 0; j < 1000; j++) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//保证numAtomicInteger++操作的原子性numAtomicInteger.incrementAndGet();}});t.start();threads.add(t);}//使用join()等待所有线程都执行完成for (Thread t : threads) {t.join();}System.out.println("numAtomicInteger = " + numAtomicInteger.get());}
}

我们看到,这里我们使用到了AtomicInteger原子整形类来替换直接num++操作,运行程序,观察控制台输出:

可见,AtomicInteger能够实现共享变量的原子操作。接着我们看一下AtomicInteger的incrementAndGet()是怎么保证原子性的。

//java.util.concurrent.atomic.AtomicInteger#unsafe
private static final Unsafe unsafe = Unsafe.getUnsafe();//java.util.concurrent.atomic.AtomicInteger#incrementAndGet
public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}//sun.misc.Unsafe#getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4) {int var5;do {//找到这个对象的某个值var5 = this.getIntVolatile(var1, var2);//var2: 内存值//var5: 预估值//var5+var4: 更新值   } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));return var5;
}

通过源码我们可以看到,AtomicInteger底层使用到了Unsafe类,它提供了原子操作。Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。Unsafe对象不能直接调用,只能通过反射获得。

Unsafe的compareAndSwapInt()方法其实就是CAS方法,它会自旋判断当前内存的值与预估值是否相等, 如果相等就会把内存值用更新值替换,如果不相等则不操作,以此来保证原子性。

下面我们介绍一下乐观锁和悲观锁。

  • 悲观锁

从悲观的角度出发,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞。因此synchronized我们也将其称之为悲观锁。JDK中的ReentrantLock也是一种悲观锁,性能较差。

  • 乐观锁

从乐观的角度出发,总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,就算改了也没关系,再重试即可。所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去修改这个数据,如何没有人修改则更新,如果有人修改则重试。CAS这种机制我们也可以将其称之为乐观锁,综合性能较好。

下面总结一下CAS算法的优缺点:

CAS优点:

  • 可以避免优先级倒置和死锁等问题;
  • 允许更高程度的并行机制;

缺点:

CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题:

  • 循环时间长开销很大

如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

  • 只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

  • ABA问题

如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?

如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

CAS获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。结合CAS和volatile可以实现无锁并发,CAS只适用于竞争不激烈、多核 CPU 的场景下。

三、synchronized锁升级过程

高效并发是从JDK 5到JDK 6的一个重要改进,HotSpot虛拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,包括偏向锁( Biased Locking )、轻量级锁( Lightweight Locking )和适应性自旋(Adaptive Spinning)、锁消除( Lock Elimination)、锁粗化( Lock Coarsening )等,这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。

无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁

前面提到很多种锁的状态,那么锁的状态到底存在哪里以及怎么区分当前属于哪一种锁呢?

其实,锁的状态保存在对象头结构的mark word标记字中,如果小伙伴还不熟悉Java对象内存布局,可以参考笔者另外一篇文章:https://weishihuai.blog.csdn.net/article/details/116401467【Object对象的内存布局学习总结】。

以上是Java对象处于5种不同状态时,Mark Word中64个位的表现形式,上面每一行代表对象处于某种状态时的样子。其中跟锁状态相关部分的含义如下:

  • lock:表示锁状态的标记位,占用2个二进制位;
  • biased_lock:表示对象是否启用偏向锁标记,占用1个二进制位。biased_lock为1时表示对象启用偏向锁,为0时表示对象没有偏向锁;

通过倒数三位数,即【biased_lock + lock】,我们可以判断出锁的类型,表达的锁状态含义如下表所示:

偏向锁标识(biased_lock)

锁标识(lock)

锁的类型

0

01

无锁

1

01

偏向锁

0

00

轻量级锁

0

10

重量级锁

0

11

GC标志

下面我们分别对各种锁状态进行详细的分析。

四、偏向锁

【a】偏向锁概念

首先来看一下什么是偏向锁?

HotSpot作者经过研究实践发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。

偏向锁的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。

不过一旦出现多个线程竞争时必须撤销偏向锁,所以撤销偏向锁消耗的性能必须小于之前节省下来的CAS原子操作的性能消耗,不然就得不偿失了。

【b】案例演示偏向锁

首先在项目中添加内存布局相关的maven依赖:

<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.9</version>
</dependency>
import org.openjdk.jol.info.ClassLayout;public class BiasedLockDemo {public static void main(String[] args) {MyRunnable myRunnable = new MyRunnable();Thread thread = new Thread(myRunnable);thread.start();}
}class MyRunnable implements Runnable {static Object object = new Object();@Overridepublic void run() {synchronized (object) {//打印锁对象的内存布局System.out.println(ClassLayout.parseInstance(object).toPrintable());}}
}

启动程序,观察控制台输出:

java.lang.Object object internals:OFFSET  SIZE   TYPE DESCRIPTION                               VALUE0     4        (object header)                           38 f5 75 19 (00111000 11110101 01110101 00011001) (427160888)4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

如上我们看到,markword的倒数三位是000,根据前面的图,000表示的是轻量级锁,此时只有一个线程访问,为什么输出来的不是偏向锁标识101呢?

原因其实是偏向锁在Java 6之后是默认启用的,但在应用程序启动几秒钟之后才激活,可以使用 -XX:BiasedLockingStartupDelay=0 参数关闭延迟。

配置完启动参数之后再次启动程序,观察内存布局

java.lang.Object object internals:OFFSET  SIZE   TYPE DESCRIPTION                               VALUE0     4        (object header)                           05 08 ea 18 (00000101 00001000 11101010 00011000) (417990661)4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到,这一次倒数三位就是偏向锁标识101了。不知道怎么查看锁标识的可以参考下图:

【c】偏向锁的原理

当线程第一次访问同步块并获取锁时,偏向锁处理流程如下:

  • 1. 虚拟机将会把对象头中的标志位设为“01”,即偏向模式;
  • 2. 同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中 ,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高;

【d】偏向锁的撤销

什么时候会触发撤销偏向锁呢?一旦有多个线程同时过来争抢资源的时候,可能就会发生偏向锁的撤销:

  • 1. 偏向锁的撤销动作必须等待全局安全点;
  • 2. 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态;
  • 3. 撤销偏向锁,恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态;

如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过 XX:-UseBiasedLocking=false 参数关闭偏向锁。

【e】偏向锁的好处

偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问比如线程池,那偏向模式就是多余的,反而会影响效率。

五、轻量级锁

【a】什么是轻量级锁

轻量级锁是JDK 6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用monitor的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的。

【b】为什么引入轻量级锁?

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的场景。因为阻塞线程需要CPU从用户态转到内核态,代价比较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁的释放。

【b】轻量级锁的原理

当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:

  • 1. 判断当前对象是否处于无锁状态(hashcode、0、01),如果是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方称为Displaced Mark Word),将对象的Mark Word复制到栈帧中的锁记录Lock Record中,将Lock Reocrd中的owner指向当前对象。
  • 2. JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作。
  • 3. 如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态。

【c】轻量级锁的释放

轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

  • 1、取出在获取轻量级锁保存在Displaced Mark Word中的数据;
  • 2、用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功;
  • 3、 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要将轻量级锁膨胀升级为重量级锁;

对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。

【d】轻量级锁优缺点

  • 优点:在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗;
  • 缺点:如果长时间自旋后还没竞争到锁,将会过度耗费CPU;

六、重量级锁

当轻量级锁遇到多个线程竞争锁资源的时候, 这时候轻量级锁可能会膨胀成重量级锁。

public class HeavyweightLockDemo {//锁对象private static Object obj = new Object();public static void main(String[] args) {Thread thread1 = new Thread(() -> {synchronized (obj) {try {Thread.sleep(20000);} catch (InterruptedException e) {e.printStackTrace();}}}, "t1");Thread thread2 = new Thread(() -> {synchronized (obj) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}, "t2");thread1.start();thread2.start();System.out.println(ClassLayout.parseInstance(obj).toPrintable());}
}

启动程序,观察锁对象的内存布局:

java.lang.Object object internals:OFFSET  SIZE   TYPE DESCRIPTION                               VALUE0     4        (object header)                           aa de 12 03 (10101010 11011110 00010010 00000011) (51568298)4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)12     4        (loss due to the next object alignment)
Instance size: 16 bytes

我们看到,当多个线程去争抢同一把锁对象obj的时候,锁对象的markword倒数三位是010,所以这是一把轻量级锁。

七、锁的优缺点对比

下表是对各种状态的锁的对比:

锁的类型

优点

缺点

适用场景

偏向锁

加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距

如果线程间存在锁竞争,会带来额外的锁撤销的消耗

适用于只有一个线程访问同步块场景

轻量级锁

竞争的线程不会阻塞,提高了程序的响应速度

如果始终得不到锁竞争的线程,使用自旋会消耗CPU

追求响应时间

同步块执行速度非常快

重量级锁

线程竞争不适用自旋,不会消耗CPU

线程阻塞,响应时间缓慢

追求吞吐量

同步块执行时间较长

八、总结

本文首先介绍了CAS原子算法,然后详细分析了synchronized锁升级的过程【无锁 > 偏向锁 > 轻量级锁 > 重量级锁】。

下面总结一下锁升级的大体过程:

【a】偏向锁升级成轻量级锁

大体过程如下图:

文字描述如下【摘自https://blog.csdn.net/tongdanping/article/details/79647337】:

当线程1访问代码块并获取锁对象时,会在锁对象的对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和锁对象的对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

【b】轻量级锁升级成重量级锁

大体过程如下图:

文字描述如下【摘自https://blog.csdn.net/tongdanping/article/details/79647337】:

  • 1、线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为Displaced MarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;
  • 2、如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁;
  • 3、如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转;

参考学习文章:

https://blog.csdn.net/sspudding/article/details/89563462

https://www.cnblogs.com/linghu-java/p/8944784.html

https://www.jianshu.com/p/36eedeb3f912

https://blog.csdn.net/tongdanping/article/details/79647337

JDK6中synchronized优化之锁升级相关推荐

  1. Synchronized关键字和锁升级

    一.Synchronized 对于多线程不安全(当数据共享(临界资源),而多线程同时访问并改变该数据时,就会不安全),JAVA提供的锁有两个,一个是synchronized关键字,另外一个就是lock ...

  2. Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/tongdanping/article/ ...

  3. java 锁升级_Java并发 锁优化和锁升级

    前言 本篇文章介绍Java Synchronized锁优化. 锁是存在哪里的,怎么标识是什么锁 Monitor机制在Java中怎么表现的 锁优化 锁升级 1. 锁存在哪里 对象在内存中的布局分为三块区 ...

  4. 如何升级jdk_简述面试常见问题的锁升级与锁优化

    上一篇讲了锁与线程安全的关系,讲到jdk经历了几个版本对锁进行了优化,这里简单梳理一下锁优化. 锁升级过程就是锁优化 在JDK最开始的时候synchronized属于重量级的锁,每次加锁都是通过操作系 ...

  5. 第十二章:synchronized与锁升级

    相关面试题 锁优化背景 Synchronized 锁性能变化 jdk5 以前 复习:为什么任意一个对象都能成为锁? jdk6 之后 synchronized的种类以及锁升级流程 锁升级流程 无锁 偏向 ...

  6. 小白也能看懂的锁升级过程和锁状态

    一.前言 锁的状态总共有四种,级别由低到高依次为:无锁.偏向锁.轻量级锁.重量级锁,这四种锁状态分别代表什么,为什么会有锁升级?其实在 JDK 1.6之前,synchronized 还是一个重量级锁, ...

  7. 【Java】多线程SynchronizedVolatile、锁升级过程 - 预习+第一天笔记

    预习 1.什么是线程 基本概念 我们先从线程的基本概念开始,给大家复习一下,不知道有多少同学是基础不太好,说什么是线程都不知道的,如果这样的话,花时间去补初级内容的课. 什么是叫一个进程? 什么叫一个 ...

  8. SQL Server 锁升级

    锁升级(Lock Escalations)是 SQL Server 使用的优化技术,用来控制在 SQL Server 锁管理里把持锁的数量.锁升级是将许多细粒度锁 ((如行或页) 锁)转换为表锁的过程 ...

  9. java中synchronized锁的升级(偏向锁、轻量级锁及重量级锁)

    java同步锁前置知识点 编码中如果使用锁可以使用synchronized关键字,对方法.代码块进行同步加锁 Synchronized同步锁是jvm内置的隐式锁(相对Lock,隐式加锁与释放) Syn ...

  10. Synchronized锁升级:无锁-> 偏向锁 -> 轻量级锁 -> 重量级锁

    一. 概述 1. Synchronized锁升级的原因 用锁能够实现数据的安全性,但是会带来性能下降.无锁能够基于线程并行提升程序性能,但是会带来安全性下降. 2. Synchronized锁升级的过 ...

最新文章

  1. TikTok时代细分需求 牛逼亚马逊运营团队打造新爆款
  2. mysql集群会备份数据吗_mysql集群即双机备份与主从复制
  3. spring boot 切换 oracle 和 mysql_spring-boot多数据源动态切换
  4. 唐骏给李开复泼冷水:创业不可复制
  5. 【自动化测试】在做自动化测试之前你需要知道的
  6. 手机游戏再造创业神话 80后月入过百万
  7. Artlantis studio 2021 for Mac(三维渲染工具)
  8. Atitit 艾提拉总结的操作硬件解决方案 目录 1.1. Atitit 列出wifi热点以及连接 1 1.2. 剪贴板 1 1.3. 屏幕 触摸与截屏 1 1.4. 性能 cpu 内存 硬盘 资源
  9. 可视化排班管理_呼叫中心外包之管理要点与数据分析对策
  10. android wine教程_如何在 Android 上借助 Wine 来运行 Apps
  11. 一次失败的鼠标修理经验
  12. 横版过关游戏开发-碰撞检测
  13. html js不触发_「万字整理 」这里有一份Node.js入门指南和实践,请注意查收 ??
  14. 【InterFace】I2C 总线详述
  15. 分水岭:知识的深度拓展
  16. 心脏出血(Heartbleed)漏洞浅析、复现
  17. java廖雪峰_廖雪峰Java教程学习笔记(一)——Java程序基础
  18. mysql取消自动登录_腾讯QQ怎么取消QQ宠物自动登录?,你需要学习了
  19. 电影票房排名查询易语言代码
  20. android系统软件卸载_adb配置使用

热门文章

  1. java 性能优化分析工具_【java】JVM性能调优监控工具、可视化在线内存分析工具...
  2. 自动驾驶 9-6: EKF 的替代方案 - 无迹卡尔曼滤波器
  3. 偏微分方程的引入及概述
  4. tableau示例超市数据在哪儿_Tableau | 超市销售数据可视化分析
  5. 2021-09-14基于用 户 行为 序列建模的推荐算法研究
  6. 2021-09-1017. 电话号码的字母组合
  7. Lattice - 规划模块 1.采样轨迹 2.计算轨迹cost 3 循环检测筛选轨迹
  8. 矩阵论作业1,2,3讲
  9. 机器学习专项练习笔记(持续更新)
  10. Python集合set与frozenset的区别