Synchronized

Synchronized

  1. 聊一聊锁的三大特性
    原子性,有序性,可见性。
  2. Synchronized的使用
  3. Synchronized锁升级
  4. Synchronized-ObjectMonitor

三大特性#

原子性#

类比数据库的事务:ACID

原子性-事务是一个最小的执行的单位,一次事务的多次操作要么都成功,要么都失败。

并发编程的原子性:一个或多个指令在CPU执行过程中不允许中断的。

问题:i++;操作是原子性?

package com.mobini.juc;

public class SyncTest01 {

    int i = 0;

    public void increment() {
        i++;
    }

}

查看方式一

命令行执行

javac SyncTest01.java
javap -v SyncTest01.class

image-20220701230302198

image-20220701230501250

查看方式二

下载插件

image-20220701230731249

image-20220701230648617

getfield:从主内存拉取数据到CPU寄存器

iadd:在寄存器内部对数据进行+1

putfield:将CPU寄存器中的结果甩到主内存中

综上所述,i++;不是原子操作

问题:如何保证i++;是原子性的

锁:synchronized,lock,Atomic(CAS)

使用synchronized

public class SyncTest01 {

    int i = 0;

    public void increment() {
        synchronized (this) {
            i++;
        }
    }

}

image-20220701231411676

使用lock锁也会有类似的概念,也就是在操作i++的三个指令前,先基于AQS成功修改state后才可以操作

使用synchronized和lock锁时,可能会触发将线程挂起的操作,而这种操作会触发内核态和用户态的切换,从而导致消耗资源。


CAS方式就相对synchronized和lock锁的效率更高(也说不定),因为CAS不会触发线程挂起操作!

CAS:compare and swap

线程基于CAS修改数据的方式:先获取主内存数据,在修改之前,先比较数据是否一致,如果一致修改主内存数据,如果不一致,放弃这次修改

CAS就是比较和交换,而比较和交换是一个原子操作

image.png

CAS在Java层面就是Unsafe类中提供的一个native方法,

image-20220702071157690

这个方法只提供了CAS成功返回true,失败返回false,如果需要重试策略,自己实现!

CAS问题:

  • CAS只能对一个变量的修改实现原子性。
  • CAS存在ABA问题
    • A线程修改主内存数据从1~2,卡在了获取1之后。
    • B线程修改主内存数据从1~2,完成。
    • C线程修改主内存数据从2~1,完成。
    • A线程执行CAS操作,发现主内存是1,没问题,直接修改
    • 解决方案:加版本号
  • 在CAS执行次数过多,但是依旧无法实现对数据的修改,CPU会一直调度这个线程,造成对CPU的性能损耗
    • synchronized的实现方式:CAS自旋一定次数后,如果还不成,挂起线程
    • LongAdder的实现方式:当CAS失败后,将操作的值,存储起来,后续一起添加

CAS:在多核情况下,有lock指令保证只有一个线程在执行当前CAS

有序性#

指令在CPU调度执行时,CPU会为了提升执行效率,在不影响结果的前提下,对CPU指令进行重新排序。

如果不希望CPU对指定进行重排序,怎么办?

可以对属性追加volatile修饰,就不会对当前属性的操作进行指令重排序。

什么时候指令重排:满足happens-before原则,即可重排序

单例模式-DCL双重判断。

申请内存,初始化,关联是正常顺序,如果CPU对指令重排,可能会造成

申请内存,关联,初始化,在还没有初始化时,其他线程来获取数据,导致获取到的数据虽然有地址引用,但是内部的数据还没初始化,都是默认值,导致使用时,可能出现与预期不符的结果

可见性#

可见性:前面说过CPU在处理时,需要将主内存数据甩到我的寄存器中再执行指令,指向完指令后,需要将寄存器数据扔回到主内存中。倒是寄存器数据同步到主内存是遵循MESI协议的,说人话就是,

不是每次操作结束就将CPU缓存数据同步到主内存。造成多个线程看到的数据不一样。

解决可见性

volatile每次操作后,立即同步数据到主内存。

synchronized,触发同步数据到主内存。(它只是加锁或是释放锁那一刻)

final,也可以解决可见性问题。

synchronized使用#

  1. synchronized方法
  2. synchronized代码块

类锁和对象锁:

类锁:基础当前类的Class加锁

对象锁:基于this对象加锁

synchronized是互斥锁,每个线程获取synchronized时,基于synchronized绑定的对象去获取锁!

synchronized锁是基于对象实现的!

synchronized是如何基于对象实现的互斥锁,先了解对象再内存中是如何存储的。

image.png

  1. markword
  2. Class Point
    • 比如 new user(); Class Point指向 user.class
  3. 实例数据
    • 比如 int i = 4; i就存放在实例数据种
  4. 补齐填充
    • 为了保证内存大小占用的是 8n

主要展开markword看看

image.png

在Java中查看。

导入依赖:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

查看对象信息

public class SyncTest02 {
    public static void main(String[] args) {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

001 -> 无锁态

image-20220701233427064

初始化的对象是无锁状态

synchronized锁升级#

synchronized在jdk1.6之前,一直是重量级锁:只要线程获取锁资源失败,直接挂起线程(用户-内核)

在jdk1.6之前synchronized效率贼低,再加上Doug Lea推出了ReentrantLock,效率比synchronized快多了,导致JDK团队不得不在jdk1.6将synchronized做优化

核心内容

锁升级,锁消除,锁膨胀

锁升级:#

无锁、偏向锁、轻量级锁和重量级锁这四个状态

  • 无锁状态、匿名偏向状态:没有线程拿锁。
  • 偏向锁状态:没有线程的竞争,只有一个线程再获取锁资源。
    线程竞争锁资源时,发现当前synchronized没有线程占用锁资源,并且锁是偏向锁,使用CAS的方式,设置o的线程ID为当前线程,获取到锁资源,下次当前线程再次获取时,只需要判断是偏向锁,并且线程ID是当前线程ID即可,直接获得到锁资源。
  • 轻量级锁:偏向锁出现竞争时,会升级到轻量级锁(触发偏向锁撤销)。
    轻量级锁的状态下,线程会基于CAS的方式,尝试获取锁资源,CAS的次数是基于自适应自旋锁实现的,JVM会自动的基于上一次获取锁是否成功,来决定这次获取锁资源要CAS多少次。
  • 重量级锁:轻量级锁CAS一段次数后,没有拿到锁资源,升级为重量级锁(其实CAS操作是在重量级锁时执行的)。
    重量级锁就是线程拿不到锁,就挂起。

演示无锁到偏向锁

public class Sync10 {
    public static void main(String[] args) {
        Object o = new Object();
        // 无锁状态
        System.out.println(ClassLayout.parseInstance(o).toPrintable());

        synchronized (o) {
            // 偏向锁
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

image-20220702090549938

问题1:00不是轻量级锁么

偏向锁是延迟开启的,并且在开启偏向锁之后,默认不存在无锁状态,只存在匿名偏向

synchronized因为不存在从重量级锁降级到偏向或者是轻量。
synchronized在偏向锁升级到轻量锁时,会涉及到偏向锁撤销,需要等到一个安全点,stw,才可以撤销,并发偏向锁撤销比较消耗资源
在程序启动时,偏向锁有一个延迟开启的操作,因为项目启动时,ClassLoader会加载.class文件,这里会涉及到synchronized操作,
为了避免启动时,涉及到偏向锁撤销,导致启动效率变慢,所以程序启动时,默认不是开启偏向锁的。

延迟开启(默认4秒,为了看到效果, 设置5秒)

Thread.sleep(5000);

public static void main(String[] args) throws InterruptedException {
    Thread.sleep(5000);
    Object o = new Object();
    // 偏向锁 但是没有偏向任何线程
    System.out.println(ClassLayout.parseInstance(o).toPrintable());

    synchronized (o) {
        // 偏向锁
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

第二种方式

默认是4秒, 改为0后,虚拟机启动后,立刻启动偏向锁

-XX:BiasedLockingStartupDelay=4
-XX:BiasedLockingStartupDelay=0

image-20220702093702765

问题2:都是101无锁状态没了

它其实已经是一个偏向锁了,但是我没有偏向任何线程,所以它是一个匿名偏向。

image-20220702092116659

如果在开启偏向锁的情况下,查看对象,默认对象是匿名偏向。

演示锁升级

// -XX:BiasedLockingStartupDelay=0
public static void main(String[] args) throws InterruptedException {
    Object o = new Object();
    // 匿名偏向
    System.out.println(ClassLayout.parseInstance(o).toPrintable());

    new Thread(() -> {
        synchronized (o) {
            // 1
            System.out.println("T1: " + ClassLayout.parseInstance(o).toPrintable());
            // 2
        }
    }).start();

    synchronized (o) {
        // 3
        System.out.println("Main: " + ClassLayout.parseInstance(o).toPrintable());
        // 4
    }
}

第一种情况

匿名偏向

Main: 101(偏向)

T1: 010(重量)

分析

  1. Main到4这个位置,打印了 Main 101(偏向)
  2. 此时T1开始抢,抢不到 偏向 -> 轻量 -> 重量级锁
  3. Main执行完成,到T1了,T1打印 T1: 010(重量)

image-20220702102000427

第二种情况

匿名偏向

Main: 010(重量)

T1: 010(重量)

分析

  1. Main到3这个位置,刚好时间片结束
  2. 此时T1开始抢,抢不到 偏向 -> 轻量 -> 重量级锁 刚好T1挂起了,
  3. Main 和 T1 都打印 010(重量)

image-20220702102129448

编译器优化的结果,出现了下列效果

锁消除:#

线程在执行一段synchronized代码块时,发现没有共享数据的操作,自动帮你把synchronized去掉。

public class Sync11 {
    public static void main(String[] args) {
        synchronized (Sync11.class) {
            int i = 1;
            i++;
            System.out.println(i);
        }
    }
}

等价于

public class Sync11 {
    public static void main(String[] args) {
        int i = 1;
        i++;
        System.out.println(i);
    }
}

锁膨胀:#

在一个多次循环的操作中频繁的获取和释放锁资源,synchronized在编译时,可能会优化到循环外部。

public class Sync12 {
    static int j = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 99999; i++) {
            synchronized (Sync12.class) {
                j += i;
            }
        }
    }

    // 类似于
    /*public static void main(String[] args) {
        synchronized (Sync12.class) {
            for (int i = 0; i < 99999; i++) {
                j += i;
            }
        }
    }*/
}

类似于

public class Sync12 {
    static int j = 0;

	public static void main(String[] args) {
        synchronized (Sync12.class) {
            for (int i = 0; i < 99999; i++) {
                j += i;
            }
        }
    }
}

synchronized-ObjectMonitor#

涉及ObjectMonitor一般是到达了重量级锁才会涉及到。

在到达重量级锁之后,重量级锁的指针会指向ObjectMonitor对象。

http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime/objectMonitor.hpp

  ObjectMonitor() {
    _header       = NULL;
    _count        = 0;     // 抢占锁资源的线程个数
    _waiters      = 0,     // 调用wait的线程个数。
    _recursions   = 0;     // 可重入锁标记,
    _object       = NULL; 
    _owner        = NULL;  // 持有锁的线程
    _WaitSet      = NULL;  // wait的线程  (双向链表)
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;  // 假定的继承人(锁释放后,被唤醒的线程,有可能拿到锁资源)
    _cxq          = NULL ;  // 挂起线程存放的位置。(双向链表)
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // _cxq会在一定的机制下,将_cxq里的等待线程扔到当前_EntryList里。  (双向链表)
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

偏向锁会降级到无锁状态嘛?怎么降?

会,当偏向锁状态下,获取当前对象的hashcode值,会因为对象头空间无法存储hashcode,导致降级到无锁状态。

posted @   iniwym  阅读(57)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示
主题色彩