带着问题阅读

1、Synchronized如何使用,加锁的粒度分别是什么

2、Synchronized的实现机制是什么

3、Synchronized是公平锁吗

4、Java对Synchronized做了哪些优化

Synchronized介绍

基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案。这意味着在给定时刻只允许一个任务访问共享资源。通常这是通过在代码前面加上一条锁语句来实现的,这就使得在一段时间内只有一个任务可以运行这段代码。因为锁语句产生了一种互相排斥的效果,所以这种机制常常称为互斥量(mutex)。

为防止资源冲突,Java提供了Synchronized用于解决互斥访问的问题。当任务执行被Synchronized修饰的代码时,将先检查锁是否可用,然后获取锁、执行代码,最后释放锁。考虑屋里有一个卫生间,多个人都需要单独使用,为了使用卫生间,每个人都先敲门,看看能否使用,如果没人使用他就进入卫生间并锁上门,当其它人来的时候就会被挡在门外。

Synchronized使用方式

  • 对象锁

synchronized可以用于修饰具体对象,如示例中分别对synObj和this对象加锁,即synObj和this分别作为共享资源被用于互斥访问,其中thread1和thread2同时访问synObj互斥,thread3和thread4同时访问this(demo对象)互斥。

public class Demo {
private Object synObj = new Object(); public void synObj() {
// 对synObj对象加锁
synchronized(synObj) {
// 同步代码
}
} public void synThis() {
// 对当前对象加锁
synchronized(this) {
// 同步代码
}
}
} Demo demo = new Demo();
// 假设以下四个线程同时运行
new Thread(demo::synObj).start(); // thread1
new Thread(demo::synObj).start(); // thread2
new Thread(demo::synThis).start(); // thread3
new Thread(demo::synThis).start(); // thread4
  • 普通方法锁

synchronized也可用于修饰方法,修饰方法时锁的对象即this,因此如果类的多个方法上都添加了synchronized,那么这几个方法在同步执行时也是互斥的。

public class Demo {
public synchronized void test1() {};
public synchronized void test2() {};
} Demo demo = new Demo();
// 以下两个线程同步执行时是互斥的,都需要获取demo对象的锁
new Thread(demo::test1).start();
new Thread(demo::test2).start();
  • 静态方法锁

以上两种应用方式由于锁的粒度都是对象,因此只能在并发调用同一个对象的方法是才会互斥,如果创建了Demo demo1 = new Demo()和Demo demo2 = new Demo()两个对象并分别调用,就不会产生互斥。如要在多实例之间也达成互斥,则可以通过修饰静态方法来达成。

public class StaticDemo {
private static Object obj = new Object();
public void test() {
synchronized(obj) {
// 同步代码
}
} public static synchronized void testStatic() {};
} // 两个线程互斥s
new Thread(StaticDemo::testStatic).start();
new Thread(StaticDemo::testStatic).start();
  • 类锁

通过添加类锁,也可实现多实例之间的互斥。synchronized修饰在静态方法时,也等价于修饰当前类对象。

public class StaticDemo {
public void test() {
synchronized(StaticDemo.class) {
// 同步代码
}
}
}

Synchronized原理分析

不论synchronized用于修饰哪里,本质还是会修饰到具体的对象(实例对象或类对象)上,synchronized的实现机制也是对对象的加锁。Java中每个对象都隐含关联一个监视器ObjectMonitor,监视器通过cpp实现内置在JVM中,监视器地址记录在对象的MarkWord上,synchronized通过ObjectMonitor实现对象的锁操作。

对象头MarkWord简介

JVM在内存中将对象划为三部分:对象头、实例数据和填充数据。对象头分为MarkWord和类型指针两部分,这里只针对锁相关做进一步介绍。MarkWord用于存储对象自身的运行数据,如哈希值、GC分代年龄等,这部分在32位和64位虚拟机中会分别占用32位和64位空间,以下是32位的空间布局(64位布局相同,分的bit数不同),MarkWord会根据对象状态复用存储空间,例如对象未锁定状态下,采用25bit哈希 + 4bitGC年龄 + 1bit固定0 + 2bit标志存储。当标志位为10表示对象处于重量级锁定时,剩余空间就用于存储ObjectMonitor对象的地址。

ObjectMonitor简介

ObjectMonitor() {
...
_count = 0; // 记录个数
_owner = NULL; // 记录持有线程
_cxq = NULL; // 记录锁阻塞线程,与EntryList配合
_WaitSet = NULL; // 记录处于wait状态的线程
_EntryList = NULL; // 记录处于锁阻塞状态的线程
...
}

ObjectMonitor整体内容略去,核心关注以上字段。_owner用于记录持有线程,_count用于记录重入次数,_cxq和_EntryList配合用于记录获取锁失败阻塞后的线程。

线程获取锁失败后会首先被挂载到_cxq队列上并调用park阻塞。当锁被释放时,如_EntryList不为空,则尝试唤醒_EntryList队首元素;如_EntryList为空,默认从_cxq摘取队首元素放入_EntryList并试图获取锁。由于monitor锁机制为非公平锁,因此可能唤醒失败,两个队列都会保存阻塞元素。

详细解析可见参考第二篇文章

Synchronized重量级锁原理

public class Demo {
private Object obj = new Object();
public void test() {
synchronized(obj) {
System.out.println("lock");
}
}
}

编译以上代码,javap -v查看字节码。

...
public void test();
Code:
...
monitorenter // 加锁
...
monitorexit // 释放锁
...
return
...

其余内容略去,关键在于monitorenter和monitorexit两个指令。

当执行monitorenter时,将会尝试获取该对象monitor的所有权。

  • 如果monitor持有数为0即无线程持有,则直接获取monitor并将进入数+1;
  • 如果monitor已被线程占有,检查是否为当前线程,如是当前线程,则将计数器+1;否则阻塞当前线程。

当执行monitorexit时,将monitor计数器-1,如减后为0,则线程释放monitor。

如synchronized修饰在方法上,则会在方法上增加ACC_SYNCHRONIZED的标记,原理与上述相同。

JVM对Synchronized的优化

monitorenter和monitorexit依赖底层操作系统的mutex lock实现,该指令对线程的挂起和唤醒涉及到用户态到内核态的切换,如果同步代码频繁调用,会带来昂贵的切换开销。自jdk1.6起对锁的实现引入了大量优化,下面来介绍一下都做了哪些优化。

锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定一句来源于逃逸分析的数据支持,如果判断一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为是线程私有的,同步加锁无须进行。

public String copyString(String s) {
StringBuffer sb = new StringBuffer();
sb.append(s);
return sb.toString();
}

如示例代码,StringBuffer.append是通过sychronized修饰的线程安全操作,但在该代码块中,sb对象是局部变量,仅会被当前线程访问,不存在线程竞争,因此锁经过编译器检测后可以消除。

锁粗化

原则上编写同步代码时,推荐将同步块的作用范围限制的尽量小,一方面减少同步代码块的执行时间,一方面减少同步竞争次数,以便存在竞争时,等待锁的线程可以尽快获得锁。但是如果一系列连续操作都在对同一对象反复加锁和释放锁,那即使没有线程竞争也会产生很多没必要的开销。

private Object obj = new Object();

public void lock() {
synchronized(obj) {
...
}
// 再次加锁
synchronized(obj) {
...
}
}

如上代码,连续两次对同一对象进行同步,即可将锁粗化合并为一个锁。

锁消除和锁粗化都是依赖JIT即时编译实现,因此通过javac查看编译后的字节码,仍然保留着原始的锁指令。

自旋锁和自适应自旋

前文中我们提到,互斥同步涉及的挂起/唤醒线程都涉及内核态转换,如果频繁产生竞争会带来很大的压力。虚拟机开发团队注意到很多应用对锁的持有只会持续很短的时间,如果可以让竞争锁的线程稍等一下,不放弃处理器,就可以在持有锁的线程执行完毕后获取锁,避免产生空间切换,这就是自旋。

自旋锁在jdk1.4.2中就引入,需要-XX: +UseSpining开启,在jdk6以后就默认开启了。自旋虽然避免了空间切换问题,但如果某个锁竞争很激烈或者锁的持有时间很长,那自旋只能白白占用处理器资源,因此在jdk1.6中引入了自适应自旋。自适应意味着自旋的时间不再固定,如果对一个锁对象自旋等待刚刚成功过,则允许后续自旋等待较长时间;如果自旋很少成功,那就在后续获得锁的过程中直接跳过自旋。

偏向锁

偏向锁也是jdk1.6引入的优化,目的是消除数据在无竞争情况下的同步原语。锁被第一个线程获取后,在接下来的执行过程中,如果一直没有被其他线程获取,则持有偏向锁的线程不在需要同步。

偏向锁加锁流程如下:

  • 检查当前是否为偏向状态。
  • 如果是,检查当前线程ID与Mark Word记录的线程ID是否一致,如一致则进入同步代码,不一致则释放偏向锁
  • 如不是偏向锁,则使用CAS尝试修改线程ID,如修改成功则进入同步代码,失败则释放偏向锁

线程获取偏向锁后,持有锁的线程以后每次进入相应同步块时,都不需要再进行任何同步操作。

偏向锁不会主动释放,只有当其他线程尝试获取锁时,才会检查持有线程是否可以释放锁。如可以释放则替换为新线程ID,不可释放则升级为轻量级锁。

勘误:图中如判断对象头Mark Word记录非当前线程ID,下一步应当为开始偏向锁撤销而非CAS替换。如有不同意见欢迎留言

轻量级锁

轻量级锁在MarkWord标志位中由00表示,轻量级锁首先在当前线程栈帧当中建立一个锁记录Lock Record,用于存储MarkWord的拷贝;然后虚拟机使用CAS操作将Lock Record的地址记录到MarkWord当中,并将标志位改为00,表示对象处于轻量级锁定状态。如果更新失败,则会进入自旋并在自旋达到一定次数后升级为重量级锁。自旋的同时如果有第三个线程尝试获取锁,也会直接升级到重量级锁。

同步代码执行完毕后,轻量级锁同样使用CAS操作将栈帧中的MarkWord拷贝回到对象中,如果操作成功,则释放锁;如果替换失败,则说明有其他线程在竞争锁(意味着升级为重量级锁),则当前膨胀为重量锁转换为重量锁的释放。

重量级锁

重量级锁即上文Synchronized重量级锁原理所述内容,综上synchronized的加锁过程为偏向锁 -> 轻量级锁 -> 重量级锁,这个过程也称为锁膨胀。

图源自网络

总结

最后总结对比一下几种锁实现。

锁类型 运行空间 实现机制 适用范围
偏向锁 用户态 初次CAS加锁,后续如无竞争可直接进入 单线程执行
轻量级锁 用户态 CAS+自旋加锁 锁竞争不激烈
重量级锁 内核态 mutex 内核态操作 锁激烈竞争

参考

Java并发之Synchronized机制详解的更多相关文章

  1. 【转】java的动态代理机制详解

    java的动态代理机制详解   在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一个就是AOP,对于IoC,依赖注入就不用多说了,而对于Spring的核心AOP来说,我们 ...

  2. java的动态代理机制详解-----https://www.cnblogs.com/xiaoluo501395377/p/3383130.html

    java的动态代理机制详解-----https://www.cnblogs.com/xiaoluo501395377/p/3383130.html

  3. java 深拷贝与浅拷贝机制详解

    概要: 在Java中,拷贝分为深拷贝和浅拷贝两种.java在公共超类Object中实现了一种叫做clone的方法,这种方法clone出来的新对象为浅拷贝,而通过自己定义的clone方法为深拷贝. (一 ...

  4. JAVA中的GC机制详解

    优秀Java程序员必须了解的GC工作原理 一个优秀的Java程序员必须了解GC的工作原理.如何优化GC的性能.如何与GC进行有限的交互,因为有一些应用程序对性能要求较高,例如嵌入式系统.实时系统等,只 ...

  5. java的动态代理机制详解

    在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一个就是AOP,对于IoC,依赖注入就不用多说了,而对于Spring的核心AOP来说,我们不但要知道怎么通过AOP来满足的 ...

  6. Java的动态代理机制详解(转)

    在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一个就是AOP,对于IoC,依赖注入就不用多说了,而对于Spring的核心AOP来说,我们不但要知道怎么通过AOP来满足的 ...

  7. (转)java的动态代理机制详解

    原文出自:http://www.cnblogs.com/xiaoluo501395377/p/3383130.html 在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一 ...

  8. [转载] java的动态代理机制详解

    转载自http://www.cnblogs.com/xiaoluo501395377/p/3383130.html 代理模式 代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代 ...

  9. Java虚拟机:类加载机制详解

    版权声明:本文为博主原创文章,转载请注明出处,欢迎交流学习! 大家知道,我们的Java程序被编译器编译成class文件,在class文件中描述的各种信息,最终都需要加载到虚拟机内存才能运行和使用,那么 ...

随机推荐

  1. 微信JSSDK的使用步骤

    步骤一:绑定域名 先登录微信公众平台进入"公众号设置"的"功能设置"里填写"JS接口安全域名".(登录后可在"开发者中心" ...

  2. Windows内核开发-5-(2)-内核模式调试

    Windows内核开发-5-(2)-内核模式调试 普通用户模式的调试,采取的是给进程添加一个线程来挂起断点,作为一个调试器的线程在进程中使用.照这样来类推,对操作系统调试相当于添加一个进程来限制操作系 ...

  3. C# 为什么你应该更喜欢 is 关键字而不是 == 运算符

    前言 在C# 进行开发中,检查参数值是否为null大家都用什么?本文介绍除了传统的方式==运算符,还有一种可以商用is关键字. C# 7.0 中 is 关键字的使用 传统的方式是使用==运算符: if ...

  4. 从理发店小弟到阿里P10大牛,一位高中学渣的“登天”之路

    蚂蚁金服,可能是众多程序猿眼中梦寐以求的归宿,无数拿过数不清奖状的各个高校走出的学子精英都挤破头皮,只为能在蚂蚁占有一席之地. 蚂蚁金服里不乏各种深藏不露的大佬,到了那里才会深刻明白一山还有一山高这句 ...

  5. Notes about BSD

    FreeBSD: mainly for web server; OpenBSD: mainly for security concerned server;

  6. Greenplum数仓监控解决方案(开源版本)

    Greenplum监控解决方案 基于Prometheus+Grafana+greenplum_exporter+node_exporter实现 关联图 一.基本概念 1.Prometheus ​ Pr ...

  7. LiteFlow 2.6.0版本发行注记,项目逻辑解耦的利器

    前言 自从LiteFlow 2.5.X版本发布依赖,陆续经历了10个小版本的迭代.社区群也稳固增长,每天都有很多小伙伴在问我问题. 但是我发现最多人问我的还是:什么时候能支持界面编排? 从LiteFL ...

  8. shell脚本——awk

    目录 一.awk 1.1.awk简介 1.2.基本格式 1.3.工作原理 1.4.常见的内建变量(可直接用) 按字段输出文本 1.5.awk和getline 有重定向符 无重定向符 1.6.指定分隔符 ...

  9. The Apache Tomcat Native library which allows using OpenSSL was not found on the java.library.path 问题解决记录

    1.问题 启动Tomcat之后,在浏览器输入IP后显示503,查看catalina.log发现报错: 2.问题定位:缺少 tomcat-native library 就是说 缺少Tomcat Nati ...

  10. 【Vulnhub】DC-2靶机

    Vulnhub DC-2 靶机 信息搜集 访问web端发现访问不了,可以观察到相应的URL为域名而不是IP,需要在hosts文件种添加一条DNS记录. host位置:C:\Windows\System ...