关于Java锁性能提高(锁升级)机制的总结

目录
  • Java锁性能提高机制
    • 锁偏向
    • 轻量级锁
    • 自旋锁
    • 重量级锁
  • Java锁升级简述
    • 对象头结构
    • synchronized关键字
    • monitor
    • 锁的四种状态

Java锁性能提高机制

锁的使用很难避免,如何尽量提高锁的性能就显得比较重要了

锁偏向

所谓的偏向锁是指在对象实例的Mark Word(说白了就是对象内存中的开头几个字节保留的信息,如果把一个对象序列化后明显可以看见开头的这些信息),为了在线程竞争不激烈的情况下,减少加锁及解锁的性能损耗(轻量级锁涉及多次CAS操作)在Mark Word中有保存这上次使用这个对象锁的线程ID信息,如果这个线程再次请求这个对象锁,那么只需要读取该对象上的Mark Word的偏向锁信息(也就是线程id)跟线程本身的id进行对比,如果是同一个id就直接认为该id获得锁成功,而不需要在进行真正的加解锁操作。

其实说白了就是你上次来过了,这次又来,并且在这两次之间没有其他人来过,对于这个线程来说,锁对象的资源随便用都是安全的。这次用缓存来换取性能的做法,不过偏向锁在锁竞争不激烈的情景下使用才能获取比较高的效率。当使用CAS竞争偏向锁失败,表示竞争比较激烈,偏向锁升级为轻量级锁。

轻量级锁

所谓轻量级锁是比偏向锁更耗资源的锁,实现机制是,线程在竞争轻量级锁前,在线程的栈内存中分配一段空间作为锁记录空间(就是轻量级锁对应的对象的对象头的字段的拷贝),拷贝好后,线程通过CAS去竞争这个对象锁,试图把对象的对象头子段改成指向所记录空间,如果成功则说明获取轻量级锁成功,如果失败,则进入自旋(说白了就是循环)取试着获取锁。如果自旋到一定次数还是不能获取到锁,则进入重量级锁。

自旋锁

说白了就是获取锁失败后,为了避免直接让线程进入阻塞状态而采取的循环一定次数去试着获取锁的行为。(线程进入阻塞状态和退出阻塞状态是涉及到操作系统管理层面的,需要从用户态进入内核态,非常消耗系统资源),为什么能这样做呢,是因为试验证明,锁的持有时间一般是非常短的,所以一般多次尝试就能竞争到锁。

重量级锁

所谓的重量级锁,其实就是最原始和最开始java实现的阻塞锁。在JVM中又叫对象监视器。

这时的锁对象的对象头字段指向的是一个互斥量,所有线程竞争重量级锁,竞争失败的线程进入阻塞状态(操作系统层面),并且在锁对象的一个等待池中等待被唤醒,被唤醒后的线程再次去竞争锁资源。

小结:

所谓的锁升级,其实就是从偏向锁à轻量级锁(自旋锁)à重量级锁,之前一直被这几个概念困扰,网上的 文章解释的又不通俗易懂,其实说白了,一切一切的开始源于java对synchronized同步机制的性能优化,最原始的synchronized同步机制是直接跳过前几个步骤,直接进入重量级锁的,而重量级锁因为需要线程进入阻塞状态(从用户态进入内核态)这种操作系统层面的操作非常消耗资源,这样的话,synchronized同步机制就显得很笨重,效率不高。

那么为了解决这个问题,java才引入了偏向锁,轻量级锁,自旋锁这几个概念。

拿这几个锁有何优化呢?网上也没有通俗易懂的解释,其实说白了就是,偏向锁是为了避免CAS操作,尽量在对比对象头就把加锁问题解决掉,只有冲突的情况下才指向一次CAS操作,而轻量级锁和自旋锁呢,其实两个是一体使用的,为的是尽量避免线程进入内核的阻塞状态,这对性能非常不利,试图用CAS操作和循环把加锁问题解决掉,而重量级锁是最终的无奈解决方案,说白了就是能通过内存读取判断解决加速问题优于〉通过CAS操作和空循环优于〉CPU阻塞,唤醒线程。

Java锁升级简述

什么是锁:在并发环境下,多线程针对同一个资源进行争抢,可能会导致数据出现异常,为了解决这个问题,就引入了锁机制。通过锁对资源进行锁定。

在JVM当中,寄存器、虚拟机栈、本地方法栈是线程独享(安全)的。但是Java堆(保存所有的Java对象和数组)、方法区、运行时常量池是线程共享的,就有可能出现线程安全问题。每个object对象都有一把锁,这个锁在对象头中,记录着这个对象在被哪个线程占用着。

对象头结构

Java的对象分为三个部分:对象头、实例数据、对齐填充字节。

对象头存放了对象本身的运行时信息。对象头包含了两部分:Mark Word和ClassPointer。相对于实际数据,对象头属于一些额外的内存开销。因此被设计的极小以提升效率。其中,Mark Word中,就包含了锁信息。

锁状态 25bit 4bit 1bit 2bit
23bit 2bit 是否偏向锁 锁标志位
无锁 对象的hashCode 分代年龄 0 01
偏向锁 线程id Epoch 分代年龄 1 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向重量级锁的指针 10
GC标记 11

Mark Word(32位为例)

对象内存存在一把锁,锁信息放在了对象头的Mark Word当中。在最后两位中,代表了锁标志位:无锁、偏向锁、轻量级锁、重量级锁。

synchronized关键字

synchronized关键字可以用来同步线程,synchronized被编译后,会生成monitorenter,monitorexit两个字节码指令。JVM以此来进行线程同步。

monitor

monitor即为同步监视器,一个线程进入了monitor,其他线程只能够等待,当且只有这个线程退出,其他线程才有机会竞争到所资源进行执行。

Entry Set中聚集了一些想要进入Monitor的线程,它们处于waiting状态,假设某个名为A线程成功进入了Monitor,那么它就处于active状态。假设此时A线程执行途中,遇到一个判断条件,需要它暂时让出执行权,那么它将进入wait set,状态也被标记为waiting。这时entry set中的线程就有机会进入monitor,假设一个线程B成功进入并且顺利完成,那么它可以通过notify的形式来唤醒wait set中的线程A,让线程A再次进入Monitor,执行完成后便退出。

这就是synchronized的同步机制,但是synchronized可能存在性能问题,因为monitor是依赖于操作系统的Mutex Lock来实现的,Java线程事实上是对操作系统线程的映射,所以每当挂起或唤醒一个线程都要切换到操作系统的内核态,这个操作是比较重量级的。在一些情况下,甚至切换时间本身就会超出线程执行任务的时间,这样的话,使用synchronized将会对程序的性能产生影响。

从Java6开始,synchronized关键字就进行了优化,引入了“偏向锁”,“轻量级锁”,所以锁共有四种状态,分别为:无锁、偏向锁、轻量级锁、重量级锁。

锁的四种状态

无锁

  • 无锁顾名思义就是没有对资源进行锁定,所有线程都能够访问同一资源。在出现资源竞争的情况下,不想对资源进行锁定,但是还是想通过某种手段进行多线程的控制。假如有一个累计值,我们不想通过锁定资源的方式进行限制,但是想控制只有一个线程能修改成功的话,就可以通过CAS操作(可以看AutomaticInteger中对unsafe类的操作)。但是当出现循环的时候,某个时间的cpu占用率会变得很高,而且可能出现ABA问题(通过版本号解决)。

偏向锁

  • 当我们开始为对象进行加锁,假如某对象被加锁了,但在实际运行的时候,只有一条线程会获取这个对锁,那么我们最理想的方式,是不要通过系统状态切换,也不通过CAS获取锁,因为那样或多或少还是消耗了一些资源。我们设想的是最好对象能够认识这个线程,只要是这个线程过来,那么对象就直接把锁交出去。我们可以认为这个对象偏爱这个线程,所以被称为“偏向锁”。
  • 那么偏向锁是怎么实现的呢?在Mark Word中,当锁标志为是01,那么判断倒数第三个bit是否为1,如果是1,那么代表当前对象的锁状态为偏向锁,于是再去读Mark Word的前23个bit,这23个bit就是线程ID,通过线程ID来确认想要获得对象锁的线程是不是之前所记录的线程ID。

轻量级锁

  • 假如情况发生了变化,对象发现目前不只有一个线程,而是有多个线程正在竞争锁,那么偏向锁将会升级为轻量级锁。Mark
  • Word发生变化,指向栈中Lock Record的指针。

重量级锁

  • 倘若在轻量级锁的情况下,线程自旋次数过多,这个时候就会从轻量级锁转换成为重量级锁。Mark Word再次发生变化,指向重量级锁的指针。这个时候,其他线程试图获取锁时都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • java 中锁的性能提高办法

    java 中锁的性能提高办法 我们努力为自己的产品所遇到的问题思考解决办法,但在这篇文章中我将给大家分享几种常用的技术,包括分离锁.并行数据结构.保护数据而非代码.缩小锁的作用范围,这几种技术可以使我们不使用任何工具来检测死锁. 锁不是问题的根源,锁之间的竞争才是 通常在多线程的代码中遇到性能方面的问题时,一般都会抱怨是锁的问题.毕竟锁会降低程序的运行速度和其较低的扩展性是众所周知的.因此,如果带着这种"常识"开始优化代码,其结果很有可能是在之后会出现讨人厌的并发问题. 因此,明白竞争

  • java锁升级过程过程详解

    目录 1.说到锁升级的过程,我们就得说一下对象头 对象头 对象头的存在形式 接下来让我们看看锁升级的过程 专业版解释 我通过马士兵老师讲的带味道的栗子大致懂了这个过程(菜鸟版理解) 总结 1.说到锁升级的过程,我们就得说一下对象头 对象头 java对象保存在内存中,由3个部分组成: 1. 对象头 2. 实例数据 3. 对齐填充字节 4. 如果是数组还包含数组长度 对象头的存在形式 让我们先看看图,主要来说一下 Mark Word markword 8bytes class pointer - 指

  • 详解java中各类锁的机制

    目录 前言 1. 乐观锁与悲观锁 2. 公平锁与非公平锁 3. 可重入锁 4. 读写锁(共享锁与独占锁) 6. 自旋锁 7. 无锁 / 偏向锁 / 轻量级锁 / 重量级锁 前言 总结java常见的锁 区分各个锁机制以及如何使用 使用方法 锁名 考察线程是否要锁住同步资源 乐观锁和悲观锁 锁住同步资源后,要不要阻塞 不阻塞可以使用自旋锁 一个线程多个流程获取同一把锁 可重入锁 多个线程公用一把锁 读写锁(写的共享锁) 多个线程竞争要不要排队 公平锁与非公平锁 1. 乐观锁与悲观锁 悲观锁:不能同时

  • java synchronized 锁机制原理详解

    目录 前言: 1.synchronized 的作用: 2.synchronized 底层语义原理: 3. synchronized 的显式同步与隐式同步: 3.1.synchronized 代码块底层原理: 3.2.synchronized 方法底层原理: 4.JVM 对 synchronized 锁的优化: 4.1.锁升级:偏向锁->轻量级锁->自旋锁->重量级锁 4.1.1.synchronized 的 Mark word 标志位: 4.1.2.锁升级过程: 4.2.锁消除: 4.3

  • 关于Java锁性能提高(锁升级)机制的总结

    目录 Java锁性能提高机制 锁偏向 轻量级锁 自旋锁 重量级锁 Java锁升级简述 对象头结构 synchronized关键字 monitor 锁的四种状态 Java锁性能提高机制 锁的使用很难避免,如何尽量提高锁的性能就显得比较重要了 锁偏向 所谓的偏向锁是指在对象实例的Mark Word(说白了就是对象内存中的开头几个字节保留的信息,如果把一个对象序列化后明显可以看见开头的这些信息),为了在线程竞争不激烈的情况下,减少加锁及解锁的性能损耗(轻量级锁涉及多次CAS操作)在Mark Word中

  • 详解java中的互斥锁信号量和多线程等待机制

    互斥锁和信号量都是操作系统中为并发编程设计基本概念,互斥锁和信号量的概念上的不同在于,对于同一个资源,互斥锁只有0和1 的概念,而信号量不止于此.也就是说,信号量可以使资源同时被多个线程访问,而互斥锁同时只能被一个线程访问 互斥锁在java中的实现就是 ReetranLock , 在访问一个同步资源时,它的对象需要通过方法 tryLock() 获得这个锁,如果失败,返回 false,成功返回true.根据返回的信息来判断是否要访问这个被同步的资源.看下面的例子 public class Reen

  • JAVA对象分析之偏向锁、轻量级锁、重量级锁升级过程

    在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分: 对象头(Header) 实例数据(Instance Data) 对齐填充(Padding). 对象头 HotSpot虚拟机(后面没有说明的话默认是这个虚拟机)对象头包括三部分: Mark Word 指向类的指针 数组长度(只有数组对象才有) 对象头之Mark Word Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关. Mar

  • Redis上实现分布式锁以提高性能的方案研究

    背景: 在很多互联网产品应用中,有些场景需要加锁处理,比如:秒杀,全局递增ID,楼层生成等等.大部分是解决方案基于DB实现的,Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系. 项目实践 任务队列用到分布式锁的情况比较多,在将业务逻辑中可以异步处理的操作放入队列,在其他线程中处理后出队,此时队列中使用了分布式锁,保证入队和出队的一致性.关于redis队列这块的逻辑分析,我将在下一次对其进行总结,此处先略过. 接下来对redis实现的分

  • 详解Java中的悲观锁与乐观锁

    一.悲观锁 悲观锁顾名思义是从悲观的角度去思考问题,解决问题.它总是会假设当前情况是最坏的情况,在每次去拿数据的时候,都会认为数据会被别人改变,因此在每次进行拿数据操作的时候都会加锁,如此一来,如果此时有别人也来拿这个数据的时候就会阻塞知道它拿到锁.在Java中,Synchronized和ReentrantLock等独占锁的实现机制就是基于悲观锁思想.在数据库中也经常用到这种锁机制,如行锁,表锁,读写锁等,都是在操作之前先上锁,保证共享资源只能给一个操作(一个线程)使用. 由于悲观锁的频繁加锁,

  • 一起聊聊Java中13种锁的实现方式

    目录 1.悲观锁 2.乐观锁 3.分布式锁 加锁 4.可重入锁 5.自旋锁 6.独享锁 7.共享锁 8.读锁/写锁 9.公平锁/非公平锁 10.可中断锁/不可中断锁 11.分段锁 12.锁升级(无锁|偏向锁|轻量级锁|重量级锁) 无锁 偏向锁 轻量级锁 重量级锁 13.锁优化技术(锁粗化.锁消除) 最近有很多小伙伴给我留言,分布式系统时代,线程并发,资源抢占,"锁" 慢慢变得很重要.那么常见的锁都有哪些? 今天Tom哥就和大家简单聊聊这个话题. 1.悲观锁 正如其名,它是指对数据修改时

  • Java并发问题之乐观锁与悲观锁

    首先介绍一些乐观锁与悲观锁: 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁.传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁.再比如Java里面的同步原语synchronized关键字的实现也是悲观锁. 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版

  • Java线程安全和锁Synchronized知识点详解

    一.进程与线程的概念 (1)在传统的操作系统中,程序并不能独立运行,作为资源分配和独立运行的基本单位都是进程. 在未配置 OS 的系统中,程序的执行方式是顺序执行,即必须在一个程序执行完后,才允许另一个程序执行:在多道程序环境下,则允许多个程序并发执行.程序的这两种执行方式间有着显著的不同.也正是程序并发执行时的这种特征,才导致了在操作系统中引入进程的概念. 自从在 20 世纪 60 年代人们提出了进程的概念后,在 OS 中一直都是以进程作为能拥有资源和独立运行的基本单位的.直到 20 世纪 8

  • Java多线程中Lock锁的使用总结

    多核时代 摩尔定律告诉我们:当价格不变时,集成电路上可容纳的晶体管数目,约每隔18个月便会增加一倍,性能也将提升一倍.换言之,每一美元所能买到的电脑性能,将每隔18个月翻两倍以上.然而最近摩尔定律似乎遇到了麻烦,目前微处理器的集成度似乎到了极限,在目前的制造工艺和体系架构下很难再提高单个处理器的速度了,否则它就被烧坏了.所以现在的芯片制造商改变了策略,转而在一个电路板上集成更多的处理器,也就是我们现在常见的多核处理器. 这就给软件行业带来麻烦(也可以说带来机会,比如说就业机会,呵呵).原来的情况

随机推荐