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
- JVM使用ACC_SYNCHRONIZED访问标识来区分一个方法是否为同步方法
- 在方法调用时,会检查方法是否被设置了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
- synchronized修饰同步代码块时,由monitorenter和monitorexit指令来实现同步
- 进入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;
}
锁升级优化
- 为了提升性能,在JDK 1.6引入偏向锁、轻量级锁、重量级锁,用来减少锁竞争带来的上下文切换
- 借助JDK 1.6新增的Java对象头,实现了锁升级功能
Java对象头
- 在JDK 1.6的JVM中,对象实例在堆内存中被分为三部分:对象头、实例数据、对齐填充
- 对象头的组成部分:Mark Word、指向类的指针、数组长度(可选,数组类型时才有)
- Mark Word记录了对象和锁有关的信息,在64位的JVM中,Mark Word为64 bit
- 锁升级功能主要依赖于Mark Word中锁标志位和是否偏向锁标志位
- synchronized同步锁的升级优化路径:偏向锁 -> 轻量级锁 -> 重量级锁
偏向锁
- 偏向锁主要用来优化同一线程多次申请同一个锁的竞争,在某些情况下,大部分时间都是同一个线程竞争锁资源
- 偏向锁的作用
- 当一个线程再次访问同一个同步代码时,该线程只需对该对象头的Mark Word中去判断是否有偏向锁指向它
- 无需再进入Monitor去竞争对象(避免用户态和内核态的切换)
- 当对象被当做同步锁,并有一个线程抢到锁时
- 锁标志位还是01,是否偏向锁标志位设置为1,并且记录抢到锁的线程ID,进入偏向锁状态
- 偏向锁不会主动释放锁
- 当线程1再次获取锁时,会比较当前线程的ID与锁对象头部的线程ID是否一致,如果一致,无需CAS来抢占锁
- 如果不一致,需要查看锁对象头部记录的线程是否存活
- 如果没有存活,那么锁对象被重置为无锁状态(也是一种撤销),然后重新偏向线程2
- 如果存活,查找线程1的栈帧信息
- 如果线程1还是需要继续持有该锁对象,那么暂停线程1(STW),撤销偏向锁,升级为轻量级锁
- 如果线程1不再使用该锁对象,那么将该锁对象设为无锁状态(也是一种撤销),然后重新偏向线程2
- 一旦出现其他线程竞争锁资源时,偏向锁就会被撤销
- 偏向锁的撤销可能需要等待全局安全点,暂停持有该锁的线程,同时检查该线程是否还在执行该方法
- 如果还没有执行完,说明此刻有多个线程竞争,升级为轻量级锁;如果已经执行完毕,唤醒其他线程继续CAS抢占
- 在高并发场景下,当大量线程同时竞争同一个锁资源时,偏向锁会被撤销,发生STW,加大了性能开销
- 默认配置
- -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=4000
- 默认开启偏向锁,并且延迟生效,因为JVM刚启动时竞争非常激烈
- 关闭偏向锁
- -XX:-UseBiasedLocking
- 直接设置为重量级锁
- -XX:+UseHeavyMonitors
红线流程部分:偏向锁的获取和撤销
轻量级锁
- 当有另外一个线程竞争锁时,由于该锁处于偏向锁状态
- 发现对象头Mark Word中的线程ID不是自己的线程ID,该线程就会执行CAS操作获取锁
- 如果获取成功,直接替换Mark Word中的线程ID为自己的线程ID,该锁会保持偏向锁状态
- 如果获取失败,说明当前锁有一定的竞争,将偏向锁升级为轻量级锁
- 线程获取轻量级锁时会有两步
- 先把锁对象的Mark Word复制一份到线程的栈帧中(DisplacedMarkWord),主要为了保留现场!!
- 然后使用CAS,把对象头中的内容替换为线程栈帧中DisplacedMarkWord的地址
- 场景
- 在线程1复制对象头Mark Word的同时(CAS之前),线程2也准备获取锁,也复制了对象头Mark Word
- 在线程2进行CAS时,发现线程1已经把对象头换了,线程2的CAS失败,线程2会尝试使用自旋锁来等待线程1释放锁
- 轻量级锁的适用场景:线程交替执行同步块,绝大部分的锁在整个同步周期内都不存在长时间的竞争
红线流程部分:升级轻量级锁
自旋锁 / 重量级锁
- 轻量级锁CAS抢占失败,线程将会被挂起进入阻塞状态
- 如果正在持有锁的线程在很短的时间内释放锁资源,那么进入阻塞状态的线程被唤醒后又要重新抢占锁资源
- JVM提供了自旋锁,可以通过自旋的方式不断尝试获取锁,从而避免线程被挂起阻塞
- 从JDK 1.7开始,自旋锁默认启用,自旋次数不建议设置过大(意味着长时间占用CPU)
- -XX:+UseSpinning -XX:PreBlockSpin=10
- 自旋锁重试之后如果依然抢锁失败,同步锁会升级至重量级锁,锁标志位为10
- 在这个状态下,未抢到锁的线程都会进入Monitor,之后会被阻塞在WaitSet中
- 在锁竞争不激烈且锁占用时间非常短的场景下,自旋锁可以提高系统性能
- 一旦锁竞争激烈或者锁占用的时间过长,自旋锁将会导致大量的线程一直处于CAS重试状态,占用CPU资源
- 在高并发的场景下,可以通过关闭自旋锁来优化系统性能
- -XX:-UseSpinning
- 关闭自旋锁优化
- -XX:PreBlockSpin
- 默认的自旋次数,在JDK 1.7后,由JVM控制
小结
1.JVM在JDK 1.6中引入了分级锁机制来优化synchronized
2.当一个线程获取锁时,首先对象锁成为一个偏向锁
- 这是为了避免在同一线程重复获取同一把锁时,用户态和内核态频繁切换
3.如果有多个线程竞争锁资源,锁将会升级为轻量级锁
- 这适用于在短时间内持有锁,且分锁交替切换的场景
- 轻量级锁还结合了自旋锁来避免线程用户态与内核态的频繁切换
4.如果锁竞争太激烈(自旋锁失败),同步锁会升级为重量级锁
5.优化synchronized同步锁的关键:减少锁竞争
- 应该尽量使synchronized同步锁处于轻量级锁或偏向锁,这样才能提高synchronized同步锁的性能
- 常用手段
- 减少锁粒度:降低锁竞争
- 减少锁的持有时间,提高synchronized同步锁在自旋时获取锁资源的成功率,避免升级为重量级锁
6.在锁竞争激烈时,可以考虑禁用偏向锁和禁用自旋锁
我是小架,我们
下篇文章见!
Java性能之synchronized锁的优化的更多相关文章
- 015-线程同步-synchronized几种加锁方式、Java对象头和Monitor、Mutex Lock、JDK1.6对synchronized锁的优化实现
一.synchronized概述基本使用 为确保共享变量不会出现并发问题,通常会对修改共享变量的代码块用synchronized加锁,确保同一时刻只有一个线程在修改共享变量,从而避免并发问题. syn ...
- Java性能 -- CAS乐观锁
synchronized / Lock / CAS synchronized和Lock实现的同步锁机制,都属于悲观锁,而CAS属于乐观锁 悲观锁在高并发的场景下,激烈的锁竞争会造成线程阻塞,而大量阻塞 ...
- java 多线程8 : synchronized锁机制 之 方法锁
脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量或者全局静态变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数 ...
- 深入理解Java中的synchronized锁重入
问题导入:如果一个线程调用了一个对象的同步方法,那么他还能不能在调用这个对象的另外一个同步方法呢? 这里就是synchronized锁重入问题. 一.synchronized锁重入 来看下面的代码: ...
- java 多线程9 : synchronized锁机制 之 代码块锁
synchronized同步代码块 用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个较长时间的任务,那么B线程必须等待比较长的时间.这种情况下可以尝试使用 ...
- Java多线程学习——synchronized锁机制
Java在多线程中使用同步锁机制时,一定要注意锁对对象,下面的例子就是没锁对对象(每个线程使用一个被锁住的对象时,得先看该对象的被锁住部分是否有人在使用) 例子:两个人操作同一个银行账户,丈夫在ATM ...
- Java并发,synchronized锁住的内容
synchronized用在方法上锁住的是什么? 锁住的是当前对象的当前方法,会使得其他线程访问该对象的synchronized方法或者代码块阻塞,但并不会阻塞非synchronized方法. 脏读 ...
- synchronized原理及优化,(自旋锁,锁消除,锁粗化,偏向锁,轻量级锁)
偏向锁:不占用CPU自旋锁:占用CPU.代码执行成本比较低且线程数少时,可以使用 .不经过OS.内核态,效率偏低 理解Java对象头与Monitor 在JVM中,对象在内存中的布局分为三块区域:对象头 ...
- 深入理解Java并发之synchronized实现原理
深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoader) 深入 ...
随机推荐
- 手机分辨率DPI怎么计算
长度方向像素数平方加宽度方向像素平方然后开根号,最后除以屏幕大小(英寸)
- Python爬虫,你是否真的了解它?
程序员有时候很难和外行人讲明白自己的工作是什么,甚至有些时候,跟同行的人讲清楚“你是干什么的”也很困难.比如我自己,就对Daivd在搞的语义网一头雾水.所以我打算写一篇博客,讲一下“爬虫工程师”的工作 ...
- 剑指offer笔记面试题9----用两个栈实现队列
题目:用两个栈实现一个队列.队列的声明如下,请实现它的两个函数appendTail和deleteHead,分别完成在尾部插入节点和在队列头部删除节点的功能. 测试用例: 往空的队列里添加.删除元素. ...
- PHP fnmatch 文件系统函数
定义和用法 fnmatch - 用模式匹配文件名 目前该函数无法在 Windows 或其它非 POSIX 兼容的系统上使用. 版本支持 PHP4 PHP5 PHP7 4.3.0(含)+支持 支持 支持 ...
- ABP入门教程3 - 解决方案
点这里进入ABP入门教程目录 创建项目 点这里进入ABP启动模板 如图操作,我们先生成一个基于.NET Core的MPA(多页面应用).点击"Create my project!" ...
- “强大”的MapPPP
写在前面 因为要给用户发送通知提醒,项目中有个短信模板/微信模板/钉钉模板/邮件模板的占位符替换的class.其中一段代码的逻辑是根据入参(model/json)来定义要替换的占位符集合,使用的是Ma ...
- Python入门基础学习(时间模块,随机模块)
Python基础学习笔记(六) time模块: 时间的三种表示方法: 1.格式化字符串 2.时间戳 用来表示和1970年的时间间隔,单位为s 3.元组 struct_time 9个元素 time的st ...
- MATLAB实例:Munkres指派算法
MATLAB实例:Munkres指派算法 作者:凯鲁嘎吉 - 博客园 http://www.cnblogs.com/kailugaji/ 1. 指派问题陈述 指派问题涉及将机器分配给任务,将工人分配给 ...
- Deepin 15.9系统直接运行exe运行程序
以下为你介绍在深度Deepin 15.9 Linux操作系统下直接运行exe文件的方法,此方法基于deepin-wine实现,经测试,一些exe文件是可以正常打开的,但部分可能会出现无法使用的情况,但 ...
- mysql导出数据的几种形式-待更新
1.导出某个数据库的某张表,添加where条件 mysqldump -u [用户名] -p -h [ip地址] --default-character-set=utf8 [数据库名] [表名] - ...