Java 中 synchronized 的优化操作:锁升级、锁消除、锁粗化

这篇具有很好参考价值的文章主要介绍了Java 中 synchronized 的优化操作:锁升级、锁消除、锁粗化。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

由 并发编程中常见的锁策略 总结可知,synchronized 具有以下几个特性:

  1. 开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁。
  2. 开始是轻量级锁实现,如果锁被持有的时间较长,就转换成重量级锁。
  3. 实现轻量级锁时,大概率用自旋锁策略。
  4. 是一种不公平锁。
  5. 是一种可重入锁。
  6. 不是读写锁。

本文介绍synchronized的几种优化操作,包括锁升级、锁消除和锁粗化。

一、锁升级

JVM 将 synchronized 锁分为无锁、偏向锁、轻量级锁、重量级锁这四种状态。在加锁过程中,会根据实际情况,依次进行升级。(**目前主流的 JVM 的实现,只能锁升级,不能锁降级!**不是无法实现,只不过可能是因为存在一些代价,使得这样做的收益和代价不成比例,因此就没有实现。)

整体的加锁过程(锁升级过程):刚开始加锁,是偏向锁状态;遇到锁竞争后,升级成自旋锁(轻量级锁);当竞争更激烈时,就会变成重量级锁(交给内核阻塞等待)。

Java 中 synchronized 的优化操作:锁升级、锁消除、锁粗化,# JavaWeb,java,开发语言,并发编程,多线程,synchronized

1、偏向锁(Biased Locking)

第一个尝试加锁的线程优先进入偏向锁状态。偏向锁是Java虚拟机(JVM)中用于提高线程同步性能的一种优化技术。在多线程环境中,对共享资源进行同步操作,需要使用锁(synchronized)来保证线程的互斥访问。传统的锁机制存在竞争和上下文切换的开销,对性能会有一定的影响。而偏向锁则是为了减少无竞争情况下的锁操作开销而引入的。

偏向锁不是真的“加锁”,只是先让线程针对锁对象有个标记,记录某个锁属于哪个线程。

它的基本思想是,当一个线程获取锁并访问同步代码块时,如果没有竞争,那么下次该线程再次进入同步块时,无需再次获取锁。这是因为在无竞争的情况下,假设一个线程反复访问同步代码块,无需每次都去竞争锁,只需判断锁是否处于偏向状态;如果是,那么直接进入同步代码块即可。

通俗来说就是,如果后续没有其他线程再来竞争该锁,那么就不用真的加锁了,从而避免了加锁解锁的开销。 但一旦还有其他线程来尝试竞争这个锁,偏向锁就立即升级成真的锁(轻量级锁),此时别的线程就只能等待了。这样做既保证了效率,也保证了线程安全。

如何判定有没有别的线程来竞争该锁?

注意,偏向锁是synchronized内部做的工作。synchronized会针对某个对象进行加锁,这个所谓的“偏向锁”正是在这个对象里头做一个标记。

由于一开始已经在锁对象中记录了当前锁属于哪个线程,因此很容易识别当前申请锁的线程是否是一开始就记录了的线程。

如果另一个线程正在尝试对同一个对象进行加锁,也会先尝试做标记,但结果却发现已经有标记了。于是JVM就会通知先来的线程,让它赶快把锁升级一下。

偏向锁本质上是“延迟加锁”,即能不加锁就不加锁,尽量避免不必要的加锁开销;但是该做的标记还是得做的,否则就无法区分何时需要真正加锁。

举个栗子理解偏向锁

假设男主是一个锁,女主是一个线程。如果只有女主和男主暧昧(即只有这一个线程来使用这个锁),那么即使男主和女主不领证结婚(避免了高成本操作),也可以一直生活下去。

但是如果此时有女配出现,也尝试竞争男主,想和男主搞暧昧,那么此时女主就必须当机立断,不管领证结婚这个操作成本多高,也势必要把这个动作完成(即真正加锁),让女配死心。

所以说,偏向锁 = 搞暧昧~~

2、自旋锁

**什么是自旋锁?**在锁策略的文章中提到:

自旋锁是一种典型的轻量级锁的实现方式,它通常是纯用户态的,不需要经过内核态。按之前的方式,线程在抢锁失败后即进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但实际上,在大部分情况下虽然当前抢锁失败,但过不了很久锁就会被释放,没必要就放弃 CPU。这个时候就可以使用自旋锁来处理这样的问题。

自旋锁是一种忙等待锁的机制。当一个线程需要获取自旋锁时,它会反复地检查锁是否可用,而不是立即被阻塞。如果获取锁失败(锁已经被其他线程占用),当前线程会立即再尝试获取锁,不断自旋(空转)等待锁的释放,直到获取到锁为止。第一次获取锁失败,第二次的尝试会在极短的时间内到来。这样能保证一旦锁被其他线程释放,当前线程能第一时间获取到锁。

优点:没有放弃 CPU,不涉及线程阻塞和调度。一旦锁被释放就能第一时间获取到锁。
缺点:如果锁被其他线程持有的时间比较久,那么就会持续的消耗 CPU 资源(忙等),而挂起等待的时候是不消耗 CPU 的。

自旋锁适用于保护临界区较小、锁占用时间短的情况,因为自旋会消耗CPU资源。自旋锁通常使用原子操作或特殊的硬件指令来实现。

随着其他线程进入锁竞争,偏向锁状态会被消除,进入轻量级锁状态,即自适应的自旋锁。

此处的轻量级锁是通过 CAS 来实现。通过 CAS 检查并更新一块内存 (比如比较 null 与该线程引用是否相等),如果更新成功,则认为加锁成功;如果更新失败,则认为锁被占用,继续自旋式的等待,期间并不放弃 CPU 资源。

(见 详解CAS算法)

Java 中 synchronized 的优化操作:锁升级、锁消除、锁粗化,# JavaWeb,java,开发语言,并发编程,多线程,synchronized

由于自旋操作是一直让 CPU 空转,比较浪费 CPU 资源,因此此处的自旋不会一直持续进行,而是达到一定的时间或重试次数就不再自旋了。这也就是所谓的 “自适应”。

3、重量级锁

**什么是重量级锁 ?**在锁策略的文章中提到:

简单来说,轻量级锁是加锁解锁的过程更快更高效的锁策略,而重量级锁是加锁解锁的过程更慢更低效的锁策略。重量级锁中加锁机制重度依赖 OS 提供的 mutex(互斥量)。

  • 大量的内核态用户态切换。
  • 很容易引发线程的调度。

这两个操作的成本都比较高,而且一旦涉及到用户态和内核态的切换,效率就低了。

如果竞争进一步激烈,自旋不能快速获取到锁状态。就会膨胀为重量级锁。

自旋锁虽然能最快获取到锁,但是要消耗大量 CPU(因为自旋的时候CPU是快速空转的)。如果当前锁竞争非常激烈,比如 50 个线程竞争一个锁,1 个争上,另外 49 个等待。这么多线程都在自旋空转,CPU的消耗就非常大。既然如此,就更改锁策略,升级成重量级锁,让其它的线程都在内核里进行阻塞等待(这意味着线程要暂时放弃 CPU 资源,由内核进行后续调度)。

(PS:目前的主流操作系统如 windows,Linux,调度的开销都是很大的。系统不承诺能在 xx 时间内一定能完成指定的调度,极端情况下调度的开销可能非常大。

但还存在另外一种实时操作系统(例如 vxworks),它能够以更低的成本完成任务调度,但牺牲了更多的其他功能。在如火箭发射这种对时间精度比较高的特殊领域就会用到。)

如果竞争进一步激烈,自旋不能快速获取到锁状态。就会膨胀为重量级锁。

此处的重量级锁就是指内核提供的 mutex 。

  1. 某线程执行加锁操作,先进入内核态。
  2. 在内核态判定当前锁是否已经被别的线程占用 。
  3. 如果该锁没有占用,则加锁成功,并切换回用户态。
  4. 如果该锁被占用,则加锁失败。此时线程进入锁的等待队列并挂起,等待被操作系统唤醒。
  5. 经历了一系列的“沧海桑田”,这个锁终于被其他线程释放了,此时操作系统也想起了这个被挂起的线程,于是唤醒这个线程,并让它尝试重新获取锁。

二、锁消除

锁消除也是“非必要,不加锁”的一种体现。与锁升级不同,锁升级是程序在运行阶段 JVM 做出的优化手段。而锁消除是在程序编译阶段的优化手段。编译器和 JVM 会检测当前代码是否是多线程执行或是否有必要加锁。如果无必要,但又把锁给写了,那么在编译的过程中就会自动把锁去掉。

有些应用程序代码中可能会用到没有必要用到的 synchronized。例如 StringBuffer 就是线程安全的,它的每一个关键方法都加了synchronized关键字:

Java 中 synchronized 的优化操作:锁升级、锁消除、锁粗化,# JavaWeb,java,开发语言,并发编程,多线程,synchronized

但这里就有一个问题:如果是在单线程中使用StringBuffer,是不涉及线程安全问题的。这个时候其实就没必要加锁。那么这时编译器就会出手,发现synchronized是没必要加的,就会在编译阶段把synchronized去掉,相当于加锁操作没有真正被编译。

StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");

此时,每个 append 的调用都会涉及加锁和解锁。但如果只是在单线程中执行这段代码,那么其中的这些加锁解锁操作是没有必要的,白白浪费了一些资源开销。

锁消除整体来说是一个比较保守的优化手段,毕竟编译器肯定得保证消除的操作是靠谱的。所以只有十拿九稳的时候才会实施锁消除,否则仍然会上锁,这时就会交给其它的操作策略来对锁进行优化(比如上面的锁升级)。

三、锁粗化

锁的粒度指的是 synchronized 代码块中包含代码的多少。代码越多,粒度越大;代码越少,粒度越小。

一般我们在写代码时,多数情况下是希望锁的粒度更小一点。(锁的粒度小就意味着串行执行的代码更少,并发执行的代码更多)。如果某个场景需要频繁地加锁解锁,此时编译器就可能把这个操作优化成个粒度更粗的锁,即锁的粗化。

Java 中 synchronized 的优化操作:锁升级、锁消除、锁粗化,# JavaWeb,java,开发语言,并发编程,多线程,synchronized

实际开发过程中使用细粒度锁,是期望释放锁的时候其他线程能使用锁。但是实际中可能并没有其他线程来抢占这个锁。这种情况 JVM 就会自动把锁粗化,避免频繁申请释放锁造成不必要的开销。

举个栗子理解锁粗化

上班时要向领导汇报工作。你的领导给你安排了三个工作:A、B、C。
汇报方式有:

  1. 先打个电话,汇报工作 A 的进展,挂了电话;再打个电话,汇报工作 B 的进展,挂了电话;再打个电话,汇报工作C的进展,挂了电话。(你给领导打电话,领导接你的电话,领导就干不了别的;别人要给领导打电话,就只能阻塞等待。每次锁竞争都可能引入一定的等待开销,此时整体的效率可能反而更低。)
  2. 打个电话,一口气汇报 工作 A,工作B,工作 C,挂了电话。

显然第二种方式是更加高效的。

可见,synchronized 的策略是比较复杂的,它是一个很“智能”的锁。文章来源地址https://www.toymoban.com/news/detail-596739.html

到了这里,关于Java 中 synchronized 的优化操作:锁升级、锁消除、锁粗化的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Synchronized与锁升级

    目录 1.先从阿里及其他大厂面试题说起 2.路线总纲 3.Synchronized的性能变化  4.Synchronized锁种类及升级步骤 4.1多线程访问情况:3种 4.2升级流程 4.3无锁 4.4偏向锁 4.5轻量级锁 4.6重量级锁 4.7总结 5.锁消除和锁粗化 1.谈谈你对Synchronized的理解 2.Synchronized的锁升级 3.线程池几个参数说

    2024年02月01日
    浏览(29)
  • 并发编程11:Synchronized与锁升级

    谈谈你对Synchronized的理解 Sychronized的锁升级你聊聊 Synchronized实现原理,monitor对象什么时候生成的?知道monitor的monitorenter和monitorexit这两个是怎么保证同步的嘛?或者说这两个操作计算机底层是如何执行的 偏向锁和轻量级锁有什么区别 Java5以前,只有Synchronized,这个是操作系

    2024年02月04日
    浏览(31)
  • 【JVM】synchronized锁升级的过程

    目录 如何从无锁状态到偏向锁状态: 偏向锁升级为轻量级锁: 轻量级锁到自旋锁的状态: 自旋锁升级为重量级锁: 下面是自旋锁升级到重量级锁的过程: 重量级锁的特点如下: synchronized锁分为三种状态——偏向锁、轻量级锁、重量级锁         当一个线程访问被syn

    2024年02月09日
    浏览(22)
  • 多线程JUC 第2季 synchronized锁升级过程

    用锁能够实现数据的安全,但是会带来性能下降。Synchronized是一个重量级锁,锁的升级过程: 无锁-偏向锁-轻量级锁-重量级锁。 高并发时,同步调用应尽量考虑锁的性能损耗,能用无锁数据结构,就不要用锁,能用区块不要用锁住整个方法体;能有对象锁,就不要用类锁。

    2024年02月09日
    浏览(26)
  • 【Java练习题汇总】《第一行代码JAVA》多线程篇,汇总Java练习题——线程及多线程概念、Thread 类及 Runnable 接口、线程状态、synchronized同步操作...

    一、填空题 Java 多线程可以依靠________ 、________ 和________ 三种方式实现。 多个线程操作同一资源的时候需要注意________,依靠________ 实现,实现手段是:________ 和________,过多的使用,则会出现________ 问题。 Java 程序运行时,至少启动________ 个线程,分别是________ 和_

    2024年02月16日
    浏览(41)
  • Golang编译优化——公共子表达式消除

    公共子表达式消除(Common Subexpression Elimination,CSE)也有书上称为冗余表达式消除,旨在减少程序中重复计算相同表达式的次数,从而提高程序的执行效率。 在程序中,如果同一个表达式在不同的地方多次出现并且具有相同的输入,则这个表达式就是一个公共子表达式。公共

    2024年04月28日
    浏览(19)
  • Synchronized同步锁的优化方法 待完工

    在并发编程中,多个线程访问同一个共享资源时,我们必须考虑如何维护数据的原子性 。在 JDK1.5 之前,Java 是依靠 Synchronized 实现锁功能来做到这点的。Synchronized 是 JVM 实现的一种内置锁,锁的获取和释放是由 JVM 隐式实现。 到了 JDK1.5 版本,并发包中新增了 Lock 接口

    2024年02月14日
    浏览(33)
  • 【操作系统】死锁问题---死锁的消除方法

    本文章将主要解释死锁的消除方法 一、死锁的概念         这是《操作系统》对于死锁的定义: 有并发进程P1,P2,…Pn,它们共享资源R1,R2,…Rm (n0,m0, n=m)。其中,每个Pi(1≤i≤n)拥有资源Rj(1≤j ≤m),直到不再有剩余资源。同时,各Pi又在不释放Rj的前提下要求Rk(k≠j,1≤k ≤m),

    2024年02月15日
    浏览(30)
  • 【JavaEE初阶】多线程进阶(五)常见锁策略 CAS synchronized优化原理

    乐观锁:预测锁竞争不是很激烈。 悲观锁:预测锁竞争会很激烈。 以上定义并不是绝对的,具体看预测锁竞争激烈程度的结论。 轻量级锁加锁解锁开销比较小,效率更高。 重量级锁加锁解锁开销比较大,效率更低。 多数情况下,乐观锁也是一个轻量级锁。 多数情况下,悲

    2024年02月03日
    浏览(34)
  • java操作rabbitmq实现简单的消息发送(socket编程的升级)

    准备: 1.下载rabbitmq并搭建环境(和python那篇一样:http://www.cnblogs.com/g177w/p/8176797.html) 2.下载支持的jar包(http://repo1.maven.org/maven2/com/rabbitmq/amqp-client) 生产者方(Productor.java): View Code 消费者方(Consummer.java):

    2023年04月08日
    浏览(33)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包