Java性能之synchronized锁的优化

synchronized / Lock

1.JDK 1.5之前,Java通过synchronized关键字来实现锁功能

  • synchronized是JVM实现的内置锁,锁的获取和释放都是由JVM隐式实现的

2.JDK 1.5,并发包中新增了Lock接口来实现锁功能

  • 提供了与synchronized类似的同步功能,但需要显式获取和释放锁

3. Lock同步锁是基于Java实现的,而synchronized是基于底层操作系统的Mutex Lock实现的

  • 每次获取和释放锁都会带来用户态和内核态的切换,从而增加系统的性能开销
  • 在锁竞争激烈的情况下,synchronized同步锁的性能很糟糕
  • 在JDK 1.5,在单线程重复申请锁的情况下,synchronized锁性能要比Lock的性能差很多

4.JDK 1.6,Java对synchronized同步锁做了充分的优化,甚至在某些场景下,它的性能已经超越了Lock同步锁

实现原理

public class SyncTest { public synchronized void method1() { } public void method2() { Object o = new Object(); synchronized (o) { } }}$ javac -encoding UTF-8 SyncTest.java$ javap -v SyncTest

修饰方法

public synchronized void method1(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=0, locals=1, args_size=1 0: return
  1. JVM使用ACC_SYNCHRONIZED访问标识来区分一个方法是否为同步方法
  2. 在方法调用时,会检查方法是否被设置了ACC_SYNCHRONIZED访问标识
  • 如果是,执行线程会将先尝试持有Monitor对象,再执行方法,方法执行完成后,最后释放Monitor对象

修饰代码块

public void method2(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=1 0: new #2 // class java/lang/Object 3: dup 4: invokespecial #1 // Method java/lang/Object."<init>":()V 7: astore_1 8: aload_1 9: dup 10: astore_2 11: monitorenter 12: aload_2 13: monitorexit 14: goto 22 17: astore_3 18: aload_2 19: monitorexit 20: aload_3 21: athrow 22: return
  1. synchronized修饰同步代码块时,由monitorenter和monitorexit指令来实现同步
  2. 进入monitorenter指令后,线程将持有该Monitor对象,进入monitorexit指令,线程将释放该Monitor对象

管程模型

1.JVM中的同步是基于进入和退出管程(Monitor)对象实现的

2.每个Java对象实例都会有一个Monitor,Monitor可以和Java对象实例一起被创建和销毁

3.Monitor是由ObjectMonitor实现的,对应ObjectMonitor.hpp

4.当多个线程同时访问一段同步代码时,会先被放在EntryList中

5.当线程获取到Java对象的Monitor时(Monitor是依靠底层操作系统的Mutex Lock来实现互斥的)

  • 线程申请Mutex成功,则持有该Mutex,其它线程将无法获取到该Mutex

6.进入WaitSet

  • 竞争锁失败的线程会进入WaitSet
  • 竞争锁成功的线程如果调用wait方法,就会释放当前持有的Mutex,并且该线程会进入WaitSet
  • 进入WaitSet的进程会等待下一次唤醒,然后进入EntryList重新排队

7.如果当前线程顺利执行完方法,也会释放Mutex

8.Monitor依赖于底层操作系统的实现,存在用户态和内核态之间的切换,所以增加了性能开销

ObjectMonitor() { _header = NULL; _count = 0; // 记录个数 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; // 持有该Monitor的线程 _WaitSet = NULL; // 处于wait状态的线程,会被加入 _WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 多个线程访问同步块或同步方法,会首先被加入 _EntryList _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0;}

锁升级优化

  1. 为了提升性能,在JDK 1.6引入偏向锁、轻量级锁、重量级锁,用来减少锁竞争带来的上下文切换
  2. 借助JDK 1.6新增的Java对象头,实现了锁升级功能

Java对象头

  1. 在JDK 1.6的JVM中,对象实例在堆内存中被分为三部分:对象头、实例数据、对齐填充
  2. 对象头的组成部分:Mark Word、指向类的指针、数组长度(可选,数组类型时才有)
  3. Mark Word记录了对象和锁有关的信息,在64位的JVM中,Mark Word为64 bit
  4. 锁升级功能主要依赖于Mark Word中锁标志位和是否偏向锁标志位
  5. synchronized同步锁的升级优化路径:偏向锁 -> 轻量级锁 -> 重量级锁

偏向锁

  1. 偏向锁主要用来优化同一线程多次申请同一个锁的竞争,在某些情况下,大部分时间都是同一个线程竞争锁资源
  2. 偏向锁的作用
  • 当一个线程再次访问同一个同步代码时,该线程只需对该对象头的Mark Word中去判断是否有偏向锁指向它
  • 无需再进入Monitor去竞争对象(避免用户态和内核态的切换)
  1. 当对象被当做同步锁,并有一个线程抢到锁时
  • 锁标志位还是01,是否偏向锁标志位设置为1,并且记录抢到锁的线程ID,进入偏向锁状态
  1. 偏向锁不会主动释放锁
  • 当线程1再次获取锁时,会比较当前线程的ID与锁对象头部的线程ID是否一致,如果一致,无需CAS来抢占锁
  • 如果不一致,需要查看锁对象头部记录的线程是否存活
  • 如果没有存活,那么锁对象被重置为无锁状态(也是一种撤销),然后重新偏向线程2
  • 如果存活,查找线程1的栈帧信息
  • 如果线程1还是需要继续持有该锁对象,那么暂停线程1(STW),撤销偏向锁,升级为轻量级锁
  • 如果线程1不再使用该锁对象,那么将该锁对象设为无锁状态(也是一种撤销),然后重新偏向线程2
  1. 一旦出现其他线程竞争锁资源时,偏向锁就会被撤销
  • 偏向锁的撤销可能需要等待全局安全点,暂停持有该锁的线程,同时检查该线程是否还在执行该方法
  • 如果还没有执行完,说明此刻有多个线程竞争,升级为轻量级锁;如果已经执行完毕,唤醒其他线程继续CAS抢占
  1. 在高并发场景下,当大量线程同时竞争同一个锁资源时,偏向锁会被撤销,发生STW,加大了性能开销
  • 默认配置
  • -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=4000
  • 默认开启偏向锁,并且延迟生效,因为JVM刚启动时竞争非常激烈
  • 关闭偏向锁
  • -XX:-UseBiasedLocking
  • 直接设置为重量级锁
  • -XX:+UseHeavyMonitors

红线流程部分:偏向锁的获取和撤销

轻量级锁

  1. 当有另外一个线程竞争锁时,由于该锁处于偏向锁状态
  2. 发现对象头Mark Word中的线程ID不是自己的线程ID,该线程就会执行CAS操作获取锁
  • 如果获取成功,直接替换Mark Word中的线程ID为自己的线程ID,该锁会保持偏向锁状态
  • 如果获取失败,说明当前锁有一定的竞争,将偏向锁升级为轻量级锁
  1. 线程获取轻量级锁时会有两步
  • 先把锁对象的Mark Word复制一份到线程的栈帧中(DisplacedMarkWord),主要为了保留现场!!
  • 然后使用CAS,把对象头中的内容替换为线程栈帧中DisplacedMarkWord的地址
  1. 场景
  • 在线程1复制对象头Mark Word的同时(CAS之前),线程2也准备获取锁,也复制了对象头Mark Word
  • 在线程2进行CAS时,发现线程1已经把对象头换了,线程2的CAS失败,线程2会尝试使用自旋锁来等待线程1释放锁
  1. 轻量级锁的适用场景:线程交替执行同步块,绝大部分的锁在整个同步周期内都不存在长时间的竞争

红线流程部分:升级轻量级锁

自旋锁 / 重量级锁

  1. 轻量级锁CAS抢占失败,线程将会被挂起进入阻塞状态
  • 如果正在持有锁的线程在很短的时间内释放锁资源,那么进入阻塞状态的线程被唤醒后又要重新抢占锁资源
  1. JVM提供了自旋锁,可以通过自旋的方式不断尝试获取锁,从而避免线程被挂起阻塞
  2. 从JDK 1.7开始,自旋锁默认启用,自旋次数不建议设置过大(意味着长时间占用CPU)
  • -XX:+UseSpinning -XX:PreBlockSpin=10
  1. 自旋锁重试之后如果依然抢锁失败,同步锁会升级至重量级锁,锁标志位为10
  • 在这个状态下,未抢到锁的线程都会进入Monitor,之后会被阻塞在WaitSet中
  1. 在锁竞争不激烈且锁占用时间非常短的场景下,自旋锁可以提高系统性能
  • 一旦锁竞争激烈或者锁占用的时间过长,自旋锁将会导致大量的线程一直处于CAS重试状态,占用CPU资源
  1. 在高并发的场景下,可以通过关闭自旋锁来优化系统性能
  • -XX:-UseSpinning
  • 关闭自旋锁优化
  • -XX:PreBlockSpin
  • 默认的自旋次数,在JDK 1.7后,由JVM控制

小结

1.JVM在JDK 1.6中引入了分级锁机制来优化synchronized

2.当一个线程获取锁时,首先对象锁成为一个偏向锁

  • 这是为了避免在同一线程重复获取同一把锁时,用户态和内核态频繁切换

3.如果有多个线程竞争锁资源,锁将会升级为轻量级锁

  • 这适用于在短时间内持有锁,且分锁交替切换的场景
  • 轻量级锁还结合了自旋锁来避免线程用户态与内核态的频繁切换

4.如果锁竞争太激烈(自旋锁失败),同步锁会升级为重量级锁

5.优化synchronized同步锁的关键:减少锁竞争

  • 应该尽量使synchronized同步锁处于轻量级锁或偏向锁,这样才能提高synchronized同步锁的性能
  • 常用手段
  • 减少锁粒度:降低锁竞争
  • 减少锁的持有时间,提高synchronized同步锁在自旋时获取锁资源的成功率,避免升级为重量级锁

6.在锁竞争激烈时,可以考虑禁用偏向锁和禁用自旋锁

我是小架,我们

下篇文章见!

原文地址:https://www.cnblogs.com/sevencutekk/p/11563367.html

时间: 2024-10-08 10:19:26

Java性能之synchronized锁的优化的相关文章

002-多线程-锁-同步锁-synchronized几种加锁方式、Java对象头和Monitor、Mutex Lock、JDK1.6对synchronized锁的优化实现

一.synchronized概述基本使用 为确保共享变量不会出现并发问题,通常会对修改共享变量的代码块用synchronized加锁,确保同一时刻只有一个线程在修改共享变量,从而避免并发问题. synchronized结论: 1.java5.0之前,协调线程间对共享对象的访问的机制只有synchronized和volatile,但是内置锁在功能上存在一些局限性,jdk5增加了Lock以及ReentrantLock. 2.java5.0,增加了一种新的机制:显式锁ReentrantLock,注意它

java 多线程8 : synchronized锁机制 之 方法锁

脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量或者全局静态变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过的.注意这里 局部变量是不存在脏读的情况 多线程线程实例变量非线程安全 看一段代码: public class ThreadDomain13 { private int num = 0; public void addNum(String userName) { try { if ("

java 多线程9 : synchronized锁机制 之 代码块锁

synchronized同步代码块 用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个较长时间的任务,那么B线程必须等待比较长的时间.这种情况下可以尝试使用synchronized同步语句块来解决问题.看一下例子: 下面例子是优化后的例子 使用代码块锁,原先例子是方法锁,就是同步 必须要执行2个for  public class ThreadDomain18 { public void doLongTimeTask() throws Exception

推荐:Java性能优化系列集锦

Java性能问题一直困扰着广大程序员,由于平台复杂性,要定位问题,找出其根源确实很难.随着10多年Java平台的改进以及新出现的多核多处理器,Java软件的性能和扩展性已经今非昔比了.现代JVM持续演进,内建了更为成熟的优化技术.运行时技术和垃圾收集器.与此同时,底层的硬件平台和操作系统也在演化. 目录: 一.Java性能优化系列之一--设计优化 二.Java性能优化系列之二--程序优化 三.Java性能优化系列之三--并发程序设计详解 四.Java性能优化系列之四--Java内存管理与垃圾回收

Java Synchronized 锁的实现原理详解及偏向锁-轻量锁-重量锁

Synchronize是重量级锁吗?是互斥锁吗? 它的实现原理? 前言 线程安全是并发编程中的重要关注点,造成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多个线程共同操作共享数据.因此为了解决这个问题,我们可能需要这样一个方案,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只

Synchronized锁性能优化偏向锁轻量级锁升级 多线程中篇(五)

不止一次的提到过,synchronized是Java内置的机制,是JVM层面的,而Lock则是接口,是JDK层面的 尽管最初synchronized的性能效率比较差,但是随着版本的升级,synchronized已经变得原来越强大了 这也是为什么官方建议使用synchronized的原因 毕竟,他是一个关键字啊,这才是亲儿子,Lock,终归差了一点 简单看下,synchronized大致都经过了哪些重要的变革 重量级锁 对于最原始的synchronized关键字,锁被称之为重量级锁 因为底层依赖监

java线程安全和锁优化

面向对象的编程思想是站在现实世界的角度去抽象和解决问题,他把数据和行为都看作是对象的一部分,这样可以让程序员能以符合现实世界的思维方式来编写和组织程序. 线程安全的一个恰当的定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的 . 按照线程安全的安全程度由强至弱来排序,可以将java语言中各种操作共享的数据分为以下5类:不可变.绝对线程安全 相对

JAVA性能优化:35个小细节让你提升java代码的运行效率

代码优化,一个很重要的课题.可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用,但是,吃的小虾米一多之后,鲸鱼就被喂饱了. 代码优化也是一样,如果项目着眼于尽快无BUG上线,那么此时可以抓大放小,代码的细节可以不精打细磨:但是如果有足够的时间开发.维护代码,这时候就必须考虑每个可以优化的细节了,一个一个细小的优化点累积起来,对于代码的运行效率绝对是有提升的. 代码优化的目标是 减小代

Java Synchronized 锁的实现原理与应用 (偏向锁,轻量锁,重量锁)

简介 在Java SE 1.6之前,Synchronized被称为重量级锁.在SE 1.6之后进行了各种优化,就出现了偏向锁,轻量锁,目的是为了减少获得锁和释放锁带来的性能消耗. Synchroized的使用(三种形式)(1) 对于普通同步方法,锁是当前实例对象.如下代码示例:解释:对于set和get方法来说,都是在方法上使用了同步关键字,所以他们是同步方法,锁的就是当前的实例对象,怎么理解了,看下面的main方法,就是这个new的实例对象.所以他们的锁对象都是synchronizedMetho