【Java】锁机制
参考
https://blog.csdn.net/varyall/article/details/79698145
《深入理解Java虚拟机》
锁状态:无锁、偏向锁、轻量级锁、重量级锁(具体是何种状态,取决于竞争情况)
这三个只是代表锁的状态。而非动作。
首先了解为什么锁的是对象
-
对象头(分为两部分)
-
第一部分用于存储对象自身的运行时数据,如哈希吗,GC分代年龄(又称为Mark Word)
-
第二部分用于存储指向方法区对象类型数据的指针。即是对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据。
-
-
实例数据(真正存储有效信息)
-
对齐填充(非必须,仅起着占位符的作用)
HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,即对象大小必须是8字节的整数倍。而对象头正好是8字节的整数倍,而对象实例数据部分没有对齐,需要通过对齐填充来补全。
虚拟机通过Mark Word的信息来区分锁的状态:
Epoch:偏向时间戳
轻量级锁、重量级锁、偏向锁
这四种状态都不是Java语言中的锁,而是Jvm为了提高锁的获取与释放效率而做的优化
四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。
-
轻量级锁:本意是在没有多线程竞争的前提下,通过CAS操作而避免了使用互斥量的开销。
在只有两个线程的情况下,提高性能。
-
偏向锁:大多数情况下,锁不存在多线程竞争,而是总是由同一线程多次获得时,为了使线程获得锁的代价更低而引入了偏向锁。提高了带有同步,但无竞争的程序性能。
在只有一个线程的情况下,进一步提高性能。
轻量级锁的原理
-
虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间。
-
将还没有被锁定的同步对象的对象头中的Mark Word拷贝到锁记录空间中
-
拷贝成功后,使用CAS操作将对象头中的Mark Word修改为一个指针(指向此线程的锁记录空间)
-
如果CAS操作成功,这个线程就拥有了该对象锁,并将锁标志“01”改为“00”,即表示此对象处于轻量级锁的状态。此时,如果有另一个线程竞争轻量级锁,则会发生自旋。
-
如果失败了,说明这个Mark Word已经被别的线程拿走了,如果存在两条以上的线程争夺同一个锁,那么轻量级锁就会膨胀为重量级锁。锁标志则被修改为“10”。膨胀之后,不可逆转。变成重量级锁之后,其他竞争失败的线程不会自旋,而是阻塞。(大量线程自旋,极大浪费CPU)
解锁过程:
-
依然是CAS操作,将上面被替换的Mark Word再替换回对象头中。
-
替换失败,说明轻量级锁已经膨胀成了重量级锁,所以CAS操作替换失败,此时,一定有其他线程在抢夺锁的过程中被阻塞,这时候,释放锁的同事,唤醒被挂起的线程。
偏向锁
轻量级锁的获取和释放都依赖于CAS原子操作,而偏向锁是在无竞争的情况下,连CAS操作都不做了。只有在需要置换ThreadID的时候,执行一次CAS原子操作。在只有一个线程执行同步块的情况下,进一步提高性能。
偏向锁的获取过程:
-
在对象第一次被线程获取的时候,查看Mark Word中偏向锁标志是否为1,锁标志是否为01。是否已经确定Thread ID,如果已经确定,则不需要CAS操作,直接执行同步代码块。
-
如果没有确定Thread ID,则通过CAS操作竞争锁,成功:则将Mark Word中的Thread ID设置为当前线程ID,然后执行同步代码块
-
如果竞争失败:则说明有线程一同竞争,偏向锁会升级为轻量级锁。竞争失败的线程自旋。
偏向锁的释放:
如果没有竞争,线程不会主动释放偏向锁,只有在有竞争的情况下,偏向锁会被撤销,提升为轻量级锁。
三种锁的转化
三种锁对比
自旋锁
线程的阻塞,挂起,唤醒,都会给系统性能带来压力。
有时候线程只需要等待很短的时间,这个时间将线程挂机,就很不值得。
所以自旋技术:让线程执行一个忙循环(自旋),稍等一下。这就是自旋锁。
-XX:+UseSpinning开启自旋锁。JDK1.6之后,默认开启。
自旋锁的效果:避免了线程切换的开销,但是在自旋过程中要占用CPU。如果锁被占用很短,自旋时间很短,自旋等待的效果就会很好,反之,如果锁占用时间长,自旋就会很浪费资源。
JDK1.6之前自旋默认循环次数为10次。可以使用参数-XX:PreBlockSpin来修改。
JDK1.6之后,次数不在固定,可以自适应,由同一个锁的前一个自旋时间来决定。比如:上一个等待此锁的线程自旋了多久,这次系统就会让此线程自旋多久。
锁消除
对于堆内存中永远不会逃逸,不会被其他线程访问到的变量,就认为是线程私有的,无需同步加锁。
虚拟机即时编译器在运行时,如果检测到不可能存在共享数据竞争的锁,说明这个锁无意义,就会自动进行消除这个锁。这就是锁消除。
// 看起来没有同步的方法 public String concatString(String str1,String str2,String str3){ return str1 + str2 + str3; } // 在JDK1.5之前,会自动转化为StringBuffer进行append()操作 public String concatString(String str1,String str2,String str3){ StringBuffer sb = new StringBuffer(); sb.append(str1); sb.append(str2); sb.append(str3); return sb.toString(); }
锁粗化
原则上,将同步块的作用范围限制的尽量小,在多线程中的互斥同步的操作数量就会尽可能的小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。
大部分情况下,这是正确的。但是,如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作在循环体中,即使没有竞争,频繁互斥同步也会导致不必要的性能损耗。
锁粗化:如果虚拟机检测到有这样一连串零碎操作(如上面的代码)都对同一个对象加锁,将会把加锁同步的范围扩大到整个操作序列的外部。
原文地址:https://www.cnblogs.com/mussessein/p/11651148.html
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释
- 太厉害了,这款开源类库可以帮你简化每一行代码
- Linux ps和pstree命令知识点总结
- CentOS7上以rpm方式安装JDK8
- linux DMA接口知识点详解
- Linux中使用crond工具创建定时任务的方法
- Linux which命令的具体使用
- Linux安装Python3.8.1的教程详解
- linux压缩文件命令zip的实例用法
- centos下samba文件夹共享服务器配置详解
- Centos7安装FFmpeg音/视频工具简易文档
- Linux 进程通信之FIFO的实现
- Linux nl命令的使用方法
- Linux gcc命令的具体使用
- Linux dirname命令的具体使用
- Linux 相对路径和绝对路径的使用