Java内置锁synchronized的实现原理及应用(三)
简述
Java中每个对象都可以用来实现一个同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。
具体表现形式如下:
1、普通同步方法,锁的是当前实例对象
2、静态同步方法,锁的是当前Class对象
3、对于同步代码块,锁的是Synchronized括号中的代码块
线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时自动释放锁,无论是通过正常路径退出,还是通过代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步方法或代码块。
从 JVM 规范 中 可以 看到 Synchonized 在 JVM 里 的 实现 原理, JVM 基于 进入 和 退出 Monitor 对象 来 实现 方法 同步 和 代码 块 同步, 但 两者 的 实现 细节 不一样。 代码 块 同步 是 使用 monitorenter 和 monitorexit 指令 实现 的, 而 方法 同步 是 使用 另外 一种 方式 实现 的, 细节 在 JVM 规范 里 并没有 详细 说明。 但是, 方法 的 同步 同样 可以 使用 这 两个 指令 来 实现。
monitorenter 指令 是在 编译 后 插入 到 同步 代码 块 的 开始 位置, 而 monitorexit 是 插入 到 方法 结束 处 和 异常 处, JVM 要 保证 每个 monitorenter 必须 有 对应 的 monitorexit 与之 配对。 任何 对象 都有 一个 monitor 与之 关联, 当 且 一个 monitor 被 持有 后, 它将 处于 锁定 状态。 线程 执行 到 monitorenter 指令时, 将会 尝试 获取 对象 所 对应 的 monitor 的 所有权, 即 尝试 获得 对象 的 锁。
对象头
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Heather)、实例数据(Instance Data)和对齐填充(Padding)。
synchronized 用的 锁 是 存在 对象头 中。
如果 对象 是 数组 类型, 则 虚拟 机 用 3 个 字 宽( Word) 存储 对 象头, 如果 对象 是非 数组 类型, 则用 2 字 宽 存储 对 象头。 在 32 位 虚拟 机中, 1 字 宽 等于 4 字节, 即 32bit。
Java 对象 头里 的 Mark Word 里 默认 存储 对象 的 HashCode、 分 代 年龄 和 锁 标记 位。
锁升级
Java SE 1. 6 为了 减少 获得 锁 和 释放 锁 带来 的 性能 消耗, 引入 了“ 偏向 锁” 和“ 轻量级 锁”, 在 Java SE 1. 6 中, 锁 一 共有 4 种 状态, 级别 从低 到 高 依次 是: 无 锁 状态、 偏向 锁 状态、 轻量级 锁 状态 和 重量级 锁 状态, 这 几个 状态 会 随着 竞争 情况 逐渐 升级。 锁 可以 升级 但不能 降级, 意味着 偏向 锁 升级 成 轻量级 锁 后 不能 降级 成 偏向 锁。 这种 锁 升级 却不 能 降级 的 策略, 目的 是 为了 提高 获得 锁 和 释放 锁 的 效率。
偏向锁
HotSpot的作者经过研究发现,大多情况下,锁不仅不存在多线程的竞争,而且总是由同一个线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
当一个线程访问同步块并获得锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,当该线程再次进入和退出同步块时,不需要再进行CAS操作加锁和解锁,只需要判断一下对象头中(Mark Word)是否存储有指向当前线程的偏向锁。
如果存在指向当前线程的偏向锁,说明该线程已经获得了锁。
如果不存在,则需要判断一下Mark Word中的偏向锁标识是否为1,如果不为1,就使用CAS竞争锁;如果标识为1,则尝试使用CAS将对象头中的偏向锁指向当前线程。
1、偏向锁撤销
偏向锁使用了一种等到锁竞争出现才释放锁的机制,当其它线程竞争偏向锁时,当前持有偏向锁的线程才会释放锁。
偏向锁的撤销,需要等待全局安全点(在这个时间点没有正在执行的字节码)。此时会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否存活,如果线程处于非活动状态,则设置对象头锁标识为无锁状态;如果线程依然存活,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word 要么重新偏向于其它线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
2、关闭偏向锁
偏向锁在Java6和Java7中是默认启用的,它在应用程序启动几秒钟之后才激活,如有需要可以使用JVM参数来关闭延迟或关闭偏向锁:
-XX:BiasedLockingStartupDelay=0 关闭偏向锁激活延迟
-XX:-UseBiasedLocking=false 关闭偏向锁(程序默认进入轻量级锁)
轻量级锁
1、轻量级锁加锁
线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word 复制到锁记录中,官方称为 Displaced Mark Word。然后线程舱室使用CAS将对象头中的Mark Word 替换为指向锁记录的指针。如果替换成功当前线程获得锁,如果失败,表示其他线程竞争 锁,当前线程就会尝试使用自旋锁来获取锁。
2、轻量级锁解锁
轻量级锁解锁时,会使用原子的CAS操作,将Displaced Mark Word 替换回到对象头,如果替换成功,表示没有竞争发生;如果替换失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程,阻塞了),一旦锁升级成重量级锁,就不会再降级到轻量级锁了。此时,其它线程试图获取锁时,都会被阻塞,当持有锁的线程释放锁之后会唤醒这些阻塞的线程,被唤醒的线程就会进行新一轮的竞争锁。
锁的优缺点对比
synchronized 锁重入
关键字 synchronized 拥有锁重入的功能,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也说明了,在一个synchronized 方法或代码块的内部调用当前对象的其它 synchronized 方法或代码块时,始终时可以得到锁的。
public class Class1 {
synchronized public void method1() {
System.out.println(" method1");
method2();
}
synchronized public void method2() {
System.out.println(" method2");
method3();
}
synchronized public void method3() {
System.out.println(" method3");
}
}
public class MyThread extends Thread {
private int i = 0;
@Override
public void run() {
Class1 class1=new Class1();
class1.method1();
}
}
public static void main(String[] args) {
MyThread myThread=new MyThread();
myThread.start();
}
输出结果:
method1
method2
method3
由此结果可以得出以下结论:一个线程获得了某个对象的锁后,当该线程持有这个对象的锁还没有释放的情况下,该线程再次调用当前对象中另外一个同步方法时,这个对象的锁还是可以获取的。如果锁不可重入的话,就会造成死锁。
synchronized 锁,出现异常自动释放
当 一个 线程 执行 的 代码 出现 异常 时, 其所 持 有的 锁 会 自动 释放。
public class Class1 {
synchronized public void method1() {
System.out.println(" method1");
method2();
}
synchronized public void method2() {
System.out.println(" method2");
method3();
}
synchronized public void method3() {
System.out.println(" method3");
}
synchronized public void testMethod() {
if (" a".equals(Thread.currentThread().getName())) {
while (true) {
System.out.println(" ThreadName=" + Thread.currentThread().getName() + " 执行开始时间=" + System.currentTimeMillis());
System.out.println(" ThreadName=" + Thread.currentThread().getName() + " 出现异常" + System.currentTimeMillis());
Integer.parseInt(" a");
}
} else {
System.out.println(" Thread B 执行开始时间=" + System.currentTimeMillis());
}
}
}
public class ThreadA extends Thread {
private Class1 class1;
public ThreadA(Class1 class1) {
this.class1 = class1;
}
@Override
public void run() {
class1.testMethod();
}
}
public class ThreadB extends Thread {
private Class1 class1;
public ThreadB(Class1 class1) {
this.class1 = class1;
}
@Override
public void run() {
class1.testMethod();
}
}
public static void main(String[] args) {
Class1 class1 = new Class1();
ThreadA a = new ThreadA(class1);
a.setName(" a");
a.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
ThreadB b = new ThreadB(class1);
b.setName(" b");
b.start();
}
执行结果:
ThreadName= a 执行开始时间=1535899339871
ThreadName= a 出现异常1535899339871
Exception in thread " a" java.lang.NumberFormatException: For input string: " a"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:569)
at java.lang.Integer.parseInt(Integer.java:615)
at com.lkf.Class1.testMethod(Class1.java:29)
at com.lkf.ThreadA.run(ThreadA.java:20)
Thread B 执行开始时间=1535899340375
线程A出现异常并释放锁,线程B进入方法,正常执行,结论就是出现异常锁会自动释放
synchronized 锁,同步不具有继承性
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public void method() { }
}
synchronized 关键字修饰的方法被重写后默认不再是 synchronized 的;虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定义的一部分,所以如果在父类中的某个方法使用了 synchronized 关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,必须显式地在子类的这个方法中加上 synchronized 关键字才可以。
总结
1、synchronized 关键字主要用来解决多线程并发同步问题,可以用来修饰类的实例方法、静态方法、代码块;
2、synchronized 实例方法实际保护的是同一个对象的方法调用,当为不同对象时多线程是可以同时访问同一个 synchronized 方法的;
3、synchronized 静态方法和 synchronized 实例方法保护的是不同对象,不同的两个线程可以同时执行 synchronized 静态方法,另一个执行 synchronized 实例方法,因为 synchronized 静态方法保护的是 class 类对象,synchronized 实例方法保护的是 this 实例对象;
4、synchronized 代码块同步的可以是任何对象,因为任何对象都有一个锁和等待队列。
5、synchronized 具备可重入性,对同一个线程在获得锁之后在调用其他需要同样锁的代码时可以直接调用,其可重入性是通过记录锁的持有线程和持有数量来实现的,调用 synchronized 代码时检查对象是否已经被锁,是则检查是否被当前线程锁定,是则计数加一,不是则加入等待队列,释放时计数减一直到为零释放锁。
6、synchronized 还具备内存可见性,除了实现原子操作避免竞态以外对于明显是原子操作的方法(譬如一个 boolean 状态变量 state 的 get 和 set 方法)也可以通过 synchronized 来保证并发的可见性,在释放锁时所有写入都会写回内存,而获得锁后都会从内存读取最新数据;不过对于已经是原子性的操作为了保证内存可见性而使用 synchronized 的成本会比较高,轻量级的选择应该是使用 volatile 修饰,一旦修饰 java 就会在操作对应变量时插入特殊指令保证可见性。
7、synchronized 是重量级锁,其语义底层是通过一个 monitor 监视器对象来完成,其实 wait、notify 等方法也依赖于 monitor 对象,所以这就是为什么只有在同步的块或者方法中才能调用 wait、notify 等方法,否则会抛出 IllegalMonitorStateException 异常的原因,监视器锁(monitor)的本质依赖于底层操作系统的互斥锁(Mutex Lock)实现,而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,所以这就是为什么 synchronized 效率低且重量级的原因(Java 1.6 进行了优化,但是相比其他锁机制还是略显偏重)。
8、synchronized 在发生异常时会自动释放线程占用的锁资源,Lock 需要在异常时主动释放,synchronized 在锁等待状态下无法响应中断而 Lock 可以。
Java内置锁synchronized的实现原理及应用(三)的更多相关文章
- Java内置锁synchronized的实现原理
简述Java中每个对象都可以用来实现一个同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock). 具体表现形式如下: 1.普通同步方法,锁的是当前实例对象 ...
- 深入理解java内置锁(synchronized)和显式锁(ReentrantLock)
多线程编程中,当代码需要同步时我们会用到锁.Java为我们提供了内置锁(synchronized)和显式锁(ReentrantLock)两种同步方式.显式锁是JDK1.5引入的,这两种锁有什么异同呢? ...
- Java内置锁synchronized的可重入性
学习自 https://blog.csdn.net/aigoogle/article/details/29893667 对我很有帮助 感谢作者
- 深入理解Java内置锁和显式锁
synchronized and Reentrantlock 多线程编程中,当代码需要同步时我们会用到锁.Java为我们提供了内置锁(synchronized)和显式锁(ReentrantLock)两 ...
- jvm内置锁synchronized不能被中断
很久没看技术书籍了,今天看了一下<七周七并发模型>前面两章讲的java,写的还是有深度的.看到了一个有demo,说jvm内置锁synchronized是不能被中断的.照着书上写了个demo ...
- Java内置锁和简单用法
一.简单的锁知识 关于内置锁 Java具有通过synchronized关键字实现的内置锁,内置锁获得锁和释放锁是隐式的,进入synchronized修饰的代码就获得锁,走出相应的代码就释放锁. jav ...
- Java 并发:内置锁 Synchronized
摘要: 在多线程编程中,线程安全问题是一个最为关键的问题,其核心概念就在于正确性,即当多个线程訪问某一共享.可变数据时,始终都不会导致数据破坏以及其它不该出现的结果. 而全部的并发模式在解决问题时,採 ...
- Java 内置锁 重入问题
阅读<Java并发编程实战>,遇到了一个问题 代码如下 /** * @auther draymonder */ public class Widget { public synchroni ...
- Java内置锁的简单认识
多线程开发离不开锁机制,现在的Java语言中,提供了2种锁,一种是语言特性提供的内置锁,还有一种是 java.util.concurrent.locks 包中的锁,这篇文章简单整理一下内置锁的知识点. ...
随机推荐
- POJ 1789 Prim
给定N个字符串,某个字符串转为另一个字符串的花费为他们每一位不相同的字符数. 求最小花费Q. Input 多组输入,以0结束. 保证N不超过2000. Output 每组输出"The hig ...
- c#将电脑时间同步到网络时间
最近遇到个项目,需要控制软件使用时间,由于电脑不联网可以修改时间,故需要联网使电脑同步网络时间 网上寻找了很多解决方案,代码如下: //Forproc_Win32.cs//对常用Win32 API函数 ...
- SIP中的SDP offer/answer交换初探
1.引言 SDP的offer/answer模型本身独立与于利用它的高层协议.SIP是使用offer/answer模型的应用之一.RFC 3264 定义了offer/answer模型,但没有规定使用哪个 ...
- 面试之HTML5 Web存储
前几天面试遇到了一个题是问localStorage和sessionStorage的区别,当时的回答不是很全面,今天就针对这个问题做一下整理(概念,用法,区别) HTML5 Web存储,一个比 cook ...
- 使用cnpm淘宝镜像
选装cnpm 1.说明:因为npm安装插件是从国外服务器下载,受网络影响大,可能出现异常,如果npm的服务器在中国就好了,所以我们乐于分享的淘宝团队干了这事. 2.官方网址:http://npm.ta ...
- K2 BPM_K2受邀出席SAP研讨会:共话“智能+”时代下的赋能与转型_全业务流程管理专家
3月5日,第十三届全国人大二次会议在北京召开.政府工作报告首次出现“智能+”,并明确指出2019年,要打造工业互联网平台,拓展“智能+”,为制造业转型升级赋能.从政府工作报告中不难看出,“智能+” ...
- SpringCloud之Hystrix容错保护原理及配置
1 什么是灾难性雪崩效应? 如下图的过程所示,灾难性雪崩形成原因就大致如此: 造成灾难性雪崩效应的原因,可以简单归结为下述三种: 服务提供者不可用.如:硬件故障.程序BUG.缓存击穿.并发请求量过大等 ...
- mysql主备搭建
mysql主备搭建参考文档https://www.cnblogs.com/clsn/p/8150036.html前提条件:系统:Ubuntu 16.04.6 LTSMySQL版本:5.7.24主库IP ...
- 在VS2017添加MVC项目
以前一直是用vs2012 MVC直接可以找到 在公司用的是vs2017 今天想创建一个MVC,和vs不同啊!!! 居然找不到MVC 然后找找找 ,突然想到创建asp.net 项目的时候发现了MV ...
- idou老师教你学Istio 04:Istio性能及扩展性介绍
Istio的性能问题一直是国内外相关厂商关注的重点,Istio对于数据面应用请求时延的影响更是备受关注,而以现在Istio官方与相关厂商的性能测试结果来看,四位数的qps显然远远不能满足应用于生产的要 ...