线程安全问题

  • 线程不安全:

    • 当多线程并发访问临界资源时(可共享的对象),如果破坏原子操作,可能会造成数据不一致。
    • 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可以保证其正确性。
    • 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。

同步方式(1)

  • 同步代码块:
synchronized(临界资源对象){ //对临界资源加锁
//代码(原子操作)
}

​ 注:

​ 每个对象都有一个互斥锁标记,用来分配给线程的。

​ 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块。

​ 线程退出同步代码块时,会释放响应的互斥锁标记。

线程的状态(阻塞)

同步方式(2)

  • 同步方法:
synchronized 返回值类型 方法名称(形参列表0){ //对当前对象(this)加锁
//代码(原子操作)
}

注:只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步方法中。

​ 线程退出同步方法时,会释放相应的互斥锁标记。

特殊情况1

这种情况下方法由static修饰,因此synchronized锁的对象是类对象.(所谓类对象就是Java虚拟机中的一个模板对象,全局只有一个)

class Number{
public static synchronized void a(){
sleep(1);
System.out.Println("1");
}
public static synchronized void b(){
System.out.Println("2");
}
} public static void main(String[] args){
Number n1 = new Number();
new Thread(() -> {n1.a();}).start();
new Thread(() -> {n1.b();}).start();
}

特殊情况2

这个时候b方法没有被static修饰,因此synchronized锁的对象是 this,也就是创建的实例对象,因此两个线程是同时执行

class Number{
public static synchronized void a(){
sleep(1);
System.out.Println("1");
}
public synchronized void b(){
System.out.Println("2");
}
} public static void main(String[] args){
Number n1 = new Number();
new Thread(() -> {n1.a();}).start();
new Thread(() -> {n1.b();}).start();
}

特殊情况3

与上面情况相似,a方法被static修饰,锁的是类对象,b方法锁的是 this 实例对象,这个时候创建两个实例对象,会同时执行 a,b方法

class Number{
public static synchronized void a(){
sleep(1);
System.out.Println("1");
}
public synchronized void b(){
System.out.Println("2");
}
} public static void main(String[] args){
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {n1.a();}).start();
new Thread(() -> {n2.b();}).start();
}

特殊情况4

这时候 a,b方法都被static修饰,锁的是类对象,因此即使创建了两个实例对象,方法访问的依然是同一个对象即全局唯一的类对象,这时候会先执行a或先执行b

class Number{
public static synchronized void a(){
sleep(1);
System.out.Println("1");
}
public static synchronized void b(){
System.out.Println("2");
}
} public static void main(String[] args){
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {n1.a();}).start();
new Thread(() -> {n2.b();}).start();
}

同步规则

  • 注意:

    • 只有在调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记。
    • 如果调用不包含同步代码块的方法,或者普通方法时,则不需要锁标记,可直接调用。
  • 已知JDK中线程安全的类:
    • StringBuffer
    • Vector
    • Hashtable
    • 以上类中的公开方法,均为synchronize修饰的同步方法。

Java对象头

以32位虚拟机为例

普通对象

  • Klass Word包含了对象的类元信息

数组对象

其中Mark Word结构为

synchronized运行原理

Monitor

下图的Monitor被翻译为监视器或管程(由操作系统提供)

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针(即上图中的Heavyweight Locked指向monitor)

  • 刚开始Monitor中Owner为null
  • 当Thread-2执行到synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
  • 在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED
  • Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争是非公平的
  • 图中WaitSet中的Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程

注意:

  • Synchronized必须是进入同一个对象的monitor才会有上述效果
  • 不加synchronized的对象不会关联监视器,不遵从上述规则

为什么说重量级锁开销大呢

主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。

这就是说为什么重量级线程开销很大的。


synchronized锁优化

轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(无竞争),那么可以使用轻量级锁来优化

轻量级锁对使用者是透明的,即语法仍然是synchronized

例如:

假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();
public static void method1(){
synchronized(obj){
//同步块A
method2();
}
public static void method1(){
synchronized(obj){
//同步块B
}
}
  • 当执行到method1()时,会创建一个锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

MarkWord的结构:

  • 接下来会让锁记录中的Object reference指向锁对象,并尝试用cas替换Object的Mark word, 将 Mark Word的值存入锁记录

  • 如果cas替换成功,对象头中存储了锁记录地址和状态 00,表示由该线程给对象加锁,将来解锁的时候再将Mark word 恢复回去

  • 如果cas失败,有两种情况

    • 如果是其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
    • 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数

发现object已经加锁了,但是锁的地址是自己,那么再添加一条Lock Record作为重入的计数

  • 当退出synchronized代码块(解锁)时,如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数-1

  • 当退出synchronized代码块(解锁)时,锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头

    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

static Object obj = new Object();

public static void method1(){

    synchronized(obj){

        //同步代码块
}
}
  • 当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁

  • 这时Thread-1加轻量级锁失败,进入锁膨胀流程

    • 即为Object对象申请Monitor锁,让Object指向重量级锁地址
    • 然后自己进入Monitor的EntryList BLOCKED

  • 当Thread-0退出同步代码块解锁时,使用cas将Mark Word的值恢复给对象头,失败

    • 这时会进入重量级解锁流程,即按照Monitor地址找到Minitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经推出了同步块,释放了锁),这时当前线程就可以避免阻塞.

自旋重试成功的情况:

注意:

​ 只有多核cpu下,自旋优化才有意义,因为单核下没有多余cpu可以给其他线程用来自旋了

自旋重试失败的情况:

  • 在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋
  • Java7之后不能控制是否开启自旋功能

偏向锁

轻量级锁在没有竞争时,每次重入仍然需要执行CAS操作

Java6中引入了偏向锁来进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归线程所有

例如:

static final Object obj = new Object();
public static void method1(){
synchronized(obj){
//同步块A
method2();
}
public static void method1(){
synchronized(obj){
//同步块B
method3();
}
public static void method3(){
synchronized(obj){
//同步块C
}
}


偏向状态

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,Mark Word值为0x05即最后3位为101,这时它的thread,epoch,age都为0
  • 偏向锁是默认延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数-XX:BiasedLockingStartupDelay=0来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,Mark Word值为0x01即最后3位位001,这时它的hashcode,age都为0,第一次用到hashcode时才会赋值
  • 同时可以通过添加VM参数:-XX:-UseBiasedLocking来禁用偏向锁
  • 如果说在加锁之前调用了对象的hashCode()方法,此时会禁用这个对象的偏向锁
    • 因为哈希码用的时候才产生,默认是0;只有第一次调用对象的hashcode,对象的哈希码才会产生,才在对象头的Mark Word里填充哈希码,只有当Mark Word处于正常状态时才能填充hashCode,因此会禁用掉Biased(偏向锁)
  • 当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏内T2,重偏向会重置对象的Thread ID

当撤销偏向锁阈值超过20次后,JVM会认为自己是不是偏向错了,于是会再给这些对象加锁时重新偏向至加锁线程

批量撤销

当撤销偏向锁阈值超过40次之后,JVM就会觉得自己确实偏向错了,根本就不该偏向.于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

锁清除

我们知道Java程序是一边执行一边编译的,在JVM即时编译过程中会去扫描这些代码,做逃逸分析,看看那些对象是线程安全的,那么对于这些对象的访问就不用做同步操作,减少锁带来的性能损失。

逃逸分析具体有对线程的逃逸分析和对方法的逃逸分析

拿方法逃逸举例,JVM检测某个对象在方法中被定义之后,会不会作为参数被外部方法引用

​ 如果不会,表示该对象是线程安全的,那对象不会逃逸到方法之外,那么对该变量的访问就可以不做同步操作。

事实上JVM就是这么做,在程序中对于一些不存在竞争的代码段,减少同步操作可以提升执行速度,用逃逸分析来得出那些不存在竞争的代码段,然后消除一些锁操作。

逃逸分析可以用参数-XX:+EscapeAnalysis来开启,然后参数-XX:+EliminateLocks来开启锁消除

锁粗化

大部分情况下,我们对一段程序加锁需要尽量缩小范围,因为如果范围太大,执行时间也会增加。但是也有特殊情况,我们看看下面这种情况。

public class LockDemo {
public void fun() {
for (int i = 0; i < 10000; ++i) {
synchronized(LockDemo.class) {
System.out.println("test...");
}
}
}
}

上面的for循环整个过程都在频繁加锁和释放锁,非常耗费性能,因此JIT将会把加锁的过程优化到for循环外面,如下面的代码所示,这就是锁粗化的过程。

public class LockDemo {
public void fun() {
synchronized(LockDemo.class) {
for (int i = 0; i < 10000; ++i) {
System.out.println("test...");
}
}
}
}

synchronized运行原理以及优化的更多相关文章

  1. synchronized实现原理及其优化-(自旋锁,偏向锁,轻量锁,重量锁)

    1.synchronized概述: synchronized修饰的方法或代码块相当于并发中的临界区,即在同一时刻jvm只允许一个线程进入执行.synchronized是通过锁机制实现同一时刻只允许一个 ...

  2. synchronized原理及优化,(自旋锁,锁消除,锁粗化,偏向锁,轻量级锁)

    偏向锁:不占用CPU自旋锁:占用CPU.代码执行成本比较低且线程数少时,可以使用 .不经过OS.内核态,效率偏低 理解Java对象头与Monitor 在JVM中,对象在内存中的布局分为三块区域:对象头 ...

  3. Synchronized用法原理和锁优化升级过程(面试)

    简介 多线程一直是面试中的重点和难点,无论你现在处于啥级别段位,对synchronized关键字的学习避免不了,这是我的心得体会.下面咱们以面试的思维来对synchronized做一个系统的描述,如果 ...

  4. synchronized实现原理

    线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据.因此为了解决这个问题,我们可能需要这样一个方案, ...

  5. jdk源码剖析二: 对象内存布局、synchronized终极原理

    很多人一提到锁,自然第一个想到了synchronized,但一直不懂源码实现,现特地追踪到C++层来剥开synchronized的面纱. 网上的很多描述大都不全,让人看了不够爽,看完本章,你将彻底了解 ...

  6. 深入理解Java并发之synchronized实现原理

    深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoader) 深入 ...

  7. 深入理解并发编程之----synchronized实现原理

    版权声明:本文为博主原创文章,请尊重原创,未经博主允许禁止转载,保留追究权 https://blog.csdn.net/javazejian/article/details/72828483 [版权申 ...

  8. Java并发(五):synchronized实现原理

    一.synchronized用法 Java中的同步块用synchronized标记. 同步块在Java中是同步在某个对象上(监视器对象). 所有同步在一个对象上的同步块在同时只能被一个线程进入并执行操 ...

  9. synchronized底层原理

    synchronized底层语义原理 Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现. 在 Java 语言中,同步用的最多的地方可能是被 syn ...

随机推荐

  1. 在ASP.NET Core中用HttpClient(四)——提高性能和优化内存

    到目前为止,我们一直在使用字符串创建请求体,并读取响应的内容.但是我们可以通过使用流提高性能和优化内存.因此,在本文中,我们将学习如何在请求和响应中使用HttpClient流. 什么是流 流是以文件. ...

  2. Edge 浏览器开发工具新增了 3D 视图,你尝试了吗?

    在使用开发者工具的时候,无意间发现了一个3D面板,如下: 仔细想想,这应该是之前 Firefox 的特性啊,不过后来去掉了,说是太难维护,没想到 Edge 也添加了这个特性. 使用该特性,你可以完成如 ...

  3. 京东 vue3 组件库震撼升级,如约而至!

    京东零售开源项目 NutUI 是一套京东风格的轻量级移动端 Vue 组件库,是开发和服务于移动 Web 界面的企业级产品.经过长时间的开发与打磨,NutUI 3.0 终于和大家见面了!3.0 版本在技 ...

  4. 使用 Android Studio 开发 widget 安卓桌面插件

    •What AppWidget 即桌面小部件,也叫桌面控件,就是能直接显示在Android系统桌面上的小程序: 这么说可能有点抽象,看图: 像这种,桌面上的天气.时钟.搜索框等等,都属于 APP Wi ...

  5. k8s删除节点

    k8s 删除节点 线上环境 # ctl get nodes NAME STATUS ROLES AGE VERSION 10.0.0.123 Ready <none> 104d v1.20 ...

  6. Java高并发测试框架JCStress

    前言 如果要研究高并发,一般会借助高并发工具来进行测试.JCStress(Java Concurrency Stress)它是OpenJDK中的一个高并发测试工具,它可以帮助我们研究在高并发场景下JV ...

  7. 基于Hive进行数仓建设的资源元数据信息统计:Spark篇

    在数据仓库建设中,元数据管理是非常重要的环节之一.根据Kimball的数据仓库理论,可以将元数据分为这三类: 技术元数据,如表的存储结构结构.文件的路径 业务元数据,如血缘关系.业务的归属 过程元数据 ...

  8. [DFS]特殊的质数肋骨

    特殊的质数肋骨 时间限制:1000MS----内存限制:256000KB 题目描述 农民约翰母牛总是产生最好的肋骨. 你能通过农民约翰和美国农业部标记在每根肋骨上的数字认出它们. 农民约翰确定他卖给买 ...

  9. 深入了解Zookeeper核心原理

    之前的文章Zookeeper基础原理&应用场景详解中将Zookeeper的基本原理及其应用场景做了一个详细的介绍,虽然介绍了其底层的存储原理.如何使用Zookeeper来实现分布式锁.但是我认 ...

  10. Java IO流知识总结

    Java IO的原理 I/O是Input和Output的缩写,用来处理设备之间的数据传输.如读/写文件,网络通讯等. Java程序中,对于数据的输入/输出操作以"流(stream)" ...