Java关键字synchronized原理与锁的状态详解
作者:凌波漫步& 发布时间:2021-11-16 05:30:29
一、Java中锁的概念
自旋锁:是指当一个线程获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能被成功获取,直到获取到锁才会退出循环。
乐观锁:假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,重试修改。
悲观锁:假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。
独享锁(写):给资源加上写锁,线程可以修改资源,其它线程不能再加锁(单写)。
共享锁(读):给资源加上读锁后只能读不能修改,其它线程也只能加读锁,不能加写锁(多度)。看成Semaphore(信号量)理解即可。
可重入锁&不可重入锁:线程拿到一把锁之后,可以自由进入同一把锁所同步的其它代码。
公平锁&非公平锁:争抢锁的顺序,如果是按先来后到,则为公平。即能保证抢锁的顺序和抢到锁的顺序一致则为公平锁。
二、同步关键字synchronized特性
特性:可重入、独享、悲观锁。
锁相关的优化:
锁消除 :开启锁消除的参数有
-XX:+DoEscapeAnalysis
、-XX:+EliminateLocks
。锁粗化:JDK做了锁粗化的优化,但我们自己可从代码层面优化。
1、锁消除示例
/**
* 锁消除示例,JIT即时编译,进行了锁消除
* @author 刘亚楼
* @date 2020/1/16
*/
public class LockEliminationExample {
/**
* StringBuilder线程不安全,StringBuffer用了synchronized关键字,是线程安全的
* 针对下面这种单线程加锁、解锁操作,JIT会进行优化,进行锁消除
*/
public static void eliminateLock() {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("a");
stringBuffer.append("b");
stringBuffer.append("c");
stringBuffer.append("a");
stringBuffer.append("b");
stringBuffer.append("c");
stringBuffer.append("a");
stringBuffer.append("b");
stringBuffer.append("c");
}
}
2、锁粗化示例
/**
* 锁粗化示例
* @author 刘亚楼
* @date 2020/1/16
*/
public class LockCoarseningExample {
/**
* 针对下面这种无意义的加锁操作,JIT会进行优化,对变量i的所有操作放到一个同步代码块里
*/
public static void lockCoarsening() {
int i = 0;
synchronized (LockCoarseningExample.class) {
i++;
}
synchronized (LockCoarseningExample.class) {
i--;
}
synchronized (LockCoarseningExample.class) {
i++;
}
synchronized (LockCoarseningExample.class) {
i++;
i--;
i++;
}
}
}
备注:锁消除和锁粗化的区别在于锁消除是针对单个线程重复加解锁做的优化,最终没有锁的存在。而锁粗化不只是针对单线程,且最终还是有锁的存在。
三、synchronized关键字原理
1、关于Mark Word
首先,对象在堆中由对象头、实例数据和对齐填充组成。
对象头包含两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向锁id等,这部分数据官方称为"Mark Word"。
对象头的另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
synchronized实现的锁是通过改变对象头的"Mark Word"来实现的。
"Mard Word"在32位和64位的虚拟机(未开启压缩指针)中分别为32位和64位。32位虚拟机"Mark Word"如下:
2、锁的状态变化
(1) 无锁 → 轻量级锁
无锁变成轻量级锁时,多个线程会读取对象的对象头的无锁状态mark word内容,然后进行cas
操作进行修改,预期值是无锁状态mark word内容,新值是轻量级锁状态mark word内容,若修改成功,Lock record address
指向成功获取锁的线程的Lock Record
。
演示流程如下:
(2) 轻量级锁 → 重量级锁
由于未成功获取锁的线程会自旋,长时间自旋会消耗CPU资源,因此自旋到一定次数会进行锁升级,由轻量级锁转变为重量级锁。
重量级锁是通过object monitor(对象监视器)实现的,对象监视器包括entryList(锁池)、owner(持锁者)、waitSet(等待集合)等。
升级为重量级锁时对象头mark word的内容是monitor address(对象监视器地址),指向对象监视器。
演示流程如下:
备注:抢锁失败线程会进入entryList(锁池),在调用wait方法后,线程会进入waitSet(等待集合),waitSet中的线程被唤醒后会重新进入entryList。
(3) 关于偏向锁
加锁之后不解锁,针对单线程
所谓偏向就是偏心,单线程加锁之后就不再解锁,减少了加锁→业务处理→释放锁→加锁操作流程。
在JDK6以后,默认已经开启了偏向锁这个优化,通过JVM参数-XX:-UseBiasedLocking
来禁用偏向锁,若偏向锁开启,只有一个线程抢锁,可获取到偏向锁。
关于偏向锁Mark Word内容如下:
偏向标记第一次有用,出现过争用后就没用了。
偏向锁本质就是无锁,如果没有发生过任何多线程争抢锁的情况,JVM认为就是单线程,无需做同步。
备注:JVM为了少干活,同步在JVM底层是有很多操作来实现的,如果没有争用,就不需要去做同步操作。
(4) 完整的锁升级过程
如果未开启偏向锁,无锁状态会先升级为轻量级锁,轻量级锁自选到一定程度升级为重量级锁。
如果开启了偏向锁,有两种情况:
当锁未被占用时,会升级为无锁,无锁再升级为轻量级锁,再由轻量级锁升级为重量级锁。
当锁被占用时,会升级为轻量级锁,再由轻量级锁升级到重量级锁。
来源:https://blog.csdn.net/lingbomanbu_lyl/article/details/125401484


猜你喜欢
- Android中SQLite 使用方法详解现在的主流移动设备像android、iPhone等都使用SQLite作为复杂数据的存储引擎,在我们
- springboot 高版本后不支持log4j了,很多人还是喜欢log4j风格的日志,我们自己来加载log4j,其实
- //==============================================// &n
- 有段时间没有写博客了,也在努力的从传统单机开发向分布式系统过度,所以再次做一些笔记,以方便日后查看。直接进入正题吧,今天记录spring-b
- 文件上传是网站非常常用的功能,直接使用Servlet获取上传文件还得解析请求参数,比较麻烦,所以一般选择采用apache的开源工具,comm
- 我们深知在操作Java流对象后要将流关闭,但往往事情不尽人意,大致有以下几种不能一定将流关闭的写法:1.在try中关流,而没在finally
- 本文实例为大家分享了android实现蓝牙app的具体代码,供大家参考,具体内容如下private BluetoothGatt blueto
- 上一章节回顾:Netty分布式源码分析监听读事件概述pipeline, 顾名思义, 就是管道的意思, 在net
- .NET 中的正则表达式是基于 Perl 5 的正则表达式。超时从 .NET Framework 4.5 开始,正则表达式支持在匹配操作中指
- 看了Android版QQ的自定义头像功能,决定自己实现,随便熟悉下android绘制和图片处理这一块的知识。先看看效果:思路分析:这个效果可
- 下面我通过代码为大家分享下C#模拟鼠标,具体内容如下:想必有很多人在项目开发中可能遇见需要做模拟鼠标点击的小功能,很多人会在百度过后采用mo
- 什么是数组数组是相同类型数据的有序集合数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成。其中,每一个数据称作一个数组元素,每
- 如何在原有日期时间上加几个月或几天在原有的时间上添加几个月SimpleDateFormat df = new SimpleDateForma
- 搭建个SSM框架居然花费了我好长时间!特此记录!需要准备的环境:idea 2017.1jdk1.8Maven 3.3.9请提前将idea与M
- 本篇文章介绍selenium 操作浏览器阅读目录浏览器最大化 前进,后退, 刷新截图操作模拟鼠标操作杀掉Windows浏览器进程浏览器最大化
- 动态数组ArrayList类在System.Collecions的命名空间下,所以使用时要加入System.Collecions命名空间,而
- 前面有写到Spring+SpringMVC+MyBatis深入学习及搭建(一)——MyBatis的基础知识。MybatisFirst中存在大
- 原则:1、函数指针,实际上是函数编码后的指令在内存中的首地址,在C++/C中,这个地址可以用函数名直接使用一个函数调用另一个函数的时候,就可
- 获取接口调用凭据①接口说明access_token是公众号的全局唯一票据,公众号调用各接口时都需使用access_token。开发者需要进行
- 1.标识符①用于给变量、类和方法命名(类名首字母大写,变量和方法名首字母小写并遵循驼峰原则)②标识符的命名规范: ■ 标识符必须