Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)

博主主页:爪哇贡尘拾Miraitow
创作时间:2022年2月18日 15:41
内容介绍: Synchronized详解
参考资料:黑马程序员JUC
⏳简言以励:列位看官,且将新火试新茶,诗酒趁年华
内容较多有问题希望能够不吝赐教
欢迎点赞 收藏 ⭐留言

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第1张图片


Synchronized详解

    • synchronized的作用
    • Java 对象头
    • Monitor 原理(Synchronized底层实现-重量级锁)
    • synchronized原理
    • Synchronized 原理进阶
    • 1、轻量级锁
    • 2、轻量级锁解锁流程
    • 3、锁膨胀
    • 4、自旋优化(优化重量级锁竞争)
    • 5、偏向锁
      • 5.1 偏向状态
      • 5.2 撤销偏向锁-hashcode方法
      • 5.3 撤销偏向锁-其它线程使用对象
      • 5.4 撤销 - 调用 wait/notify
      • 5.5 批量重偏向
    • 6、同步省略 (锁消除)

synchronized的作用

(1)原子性:所谓原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。被synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放。
(2)可见性:可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。 synchronizedvolatile(后面文章会讲到)都具有可见性,其中synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到共享内存当中(也就是主内存,而不是线程的副本),保证资源变量的可见性。
(3)有序性:有序性值程序执行的顺序按照代码先后执行。 synchronizedvolatile都具有有序性,Java允许编译器和处理器对指令进行重排(JIT),但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。

补充
volatile可以保证可见性和顺序性,这些都很好理解,那么它为什么不能保证原子性呢?

首先需要了解的是,Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j =i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了。 所以,如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性了。

Java 对象头

以 32 位虚拟机为例,普通对象的对象头结构如下,其中的Klass Word为指针,指向对应的Class对象

类对象:

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第2张图片

数组对象

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第3张图片

其中 Mark Word 结构为

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第4张图片
对象头包含两部分:运行时元数据(Mark Word)和类型指针 (Klass Word)

1、运行时元数据:

  • 哈希值(HashCode),可以看作是堆中对象的地址
  • GC分代年龄(年龄计数器) (用于新生代from/to区晋升老年代的标准, 阈值为15,之所以为15是因为占用四个字节,最大为15)
  • 锁状态标志(用于JDK1.6对synchronized的优化 -> 轻量级锁(00))
  • 线程持有的锁用Thread.holdsLock(lockObj) 获取判断是否当前线程持有锁
  • 偏向线程ID (用于JDK1.6对synchronized的优化 -> 偏向锁)
  • 偏向时间戳
  • 类型指针

2. 指向类元数据InstanceKlass,确定该对象所属的类型。指向的其实是方法区中存放的类元信息

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第5张图片
对象的存储布局
Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第6张图片

Monitor 原理(Synchronized底层实现-重量级锁)

多线程同时访问临界区: 使用重量级锁

JDK6对Synchronized的优先状态:偏向锁–>轻量级锁–>重量级锁

Monitor被翻译为监视器或者说管程

每个java对象都可以关联一个Monitor,如果使用synchronized给对象上锁(重量级),该对象头的Mark Word中就被设置为指向Monitor对象的指针

每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第7张图片

简单描述:

  • 刚开始时Monitor中的Ownernull
  • 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 中的条件不满足进入 WAITING 状态的线程,后面讲wait-notify 时会分析

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

详细描述:

下图原理解释:
Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第8张图片

Thread2访问到synchronized(obj)中的共享资源的时候

  • 首先会将synchronized中的锁对象中对象头的MarkWord去尝试指向操作系统的Monitor对象. 让锁对象中的MarkWord和Monitor对象相关联. 如果关联成功, 将obj对象头中的MarkWord的对象状态从01(无锁)改为10(重量级)
  • 因为Monitor没有和其他的obj的MarkWord相关联, 所以Thread2就成为了该Monitor的Owner(所有者)
  • 又来了个Thread1执行synchronized(obj)代码, 它首先会看看能不能执行该临界区的代码; 它会检查obj是否关联了Montior, 此时已经有关联了, 它就会去看看该Montior有没有所有者(Owner), 发现有所有者了(Thread2); Thread1也会和该Monitor关联, 该线程就会进入到它的EntryList(阻塞队列);
  • Thread2执行完临界区代码后, Monitor的Owner(所有者)就空出来了. 此时就会通知Monitor中的EntryList阻塞队列中的线程, 这些线程通过竞争, 成为新的所有者

synchronized原理

    static final Object lock=new Object();
    static int counter = 0;
    public static void main(String[] args) {
        synchronized (lock) {
            counter++;
        }
    }

反编译后的部分字节码

 0 getstatic #2 
 # 取得lock的引用(synchronized开始了)
 3 dup    
 # 复制操作数栈栈顶的值放入栈顶,即复制了一份lock的引用
 4 astore_1
 # 操作数栈栈顶的值弹出,即将lock的引用存到局部变量表中
 5 monitorenter
 # 将lock对象的Mark Word置为指向Monitor指针
 6 getstatic #3 
 9 iconst_1
10 iadd
11 putstatic #3 
14 aload_1
# 从局部变量表中取得lock的引用,放入操作数栈栈顶
15 monitorexit
# 将lock对象的Mark Word重置,唤醒EntryList
16 goto 24 (+8)
# 下面是异常处理指令,可以看到,如果出现异常,也能自动地释放锁
19 astore_2
20 aload_1
21 monitorexit
22 aload_2
23 athrow
24 return

注意:方法级别的 synchronized 不会在字节码指令中有所体现

具体原理可以看我之前记录的文章
synchronized底层原理

Synchronized 原理进阶

在JDK1.6以后引入偏向锁和轻量级锁在JVM层面实现加锁的逻辑,不依赖底层的操作系统,就不会产生切换的消耗(之前的直接加入重量级锁,就会导致后来的线程进入阻塞,所以会导致上下文切换,涉及到操作系统从用户态转到核心态),所以,Markword对的状态记录有四种分别是:无锁偏向锁轻量级锁重量级锁

1、轻量级锁

通过锁记录的方式, 场景 : 多个线程交替进入临界区

轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。轻量级锁对使用者是透明的,即语法仍然是 synchronized

eg: 线程A来操作临界区的资源, 给资源加锁,到执行完临界区代码,释放锁的过程, 没有线程来竞争, 此时就可以使用轻量级锁; 如果这期间有线程来竞争的话, 就会升级为重量级锁(synchronized)

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

static final Object obj = new Object();
public static void method1() {
     synchronized( obj ) {
         // 同步块 A
         method2();
     }
}
public static void method2() {
     synchronized( obj ) {
         // 同步块 B
     }
}
  1. 每次指向到synchronized代码块时,都会创建锁记录(Lock Record)对象,每个线程都会包括一个锁记录的结构,锁记录内部可以储存对象的Mark Word和对象引用reference

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第9张图片2. 让锁记录中的Object reference指向对象,并且尝试用cas(compare and sweep)替换Object对象的Mark Word ,将Mark Word 的值存入锁记录

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第10张图片

① 如果cas替换成功,那么对象的对象头储存的就是锁记录的地址和状态00,如下所示

  • 线程中锁记录, 记录了锁对象的锁状态标志; 锁对象的对象头存储了锁记录的地址和状态, 标志哪个线程获得了锁
  • 此时栈帧中存储了对象的对象头中的锁状态标志,年龄计数器,哈希值等; 对象的对象头中就存储了栈帧中锁记录的地址和状态00, 这样的话对象就知道了是哪个线程锁住自己。

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第11张图片

②如果cas失败,有两种情况(①锁膨胀,②重入锁失败)

  • ①锁膨胀:如果是其它线程已经持有了该Object的轻量级锁,那么表示有竞争,将进入锁膨胀阶段,此时对象Object对象头中已经存储了别的线程的锁记录地址 00,指向了其他线程;
  • ②重入锁失败:如果是自己的线程已经执行了synchronized进行加锁,那么那么再添加一条 Lock Record作为重入的计数,在上面代码中,临界区中又调用了method2, method2中又进行了一次synchronized加锁操作, 此时就会在虚拟机栈中再开辟一个method2方法对应的栈帧(栈顶), 该栈帧中又会存在一个独立的Lock Record, 此时它发现对象的对象头中指向的就是自己线程中栈帧的锁记录; 加锁也就失败了.这种现象就叫做 锁重入; 线程中有多少个锁记录, 就能表明该线程对这个对象加了几次锁 (锁重入计数)

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第12张图片

2、轻量级锁解锁流程

当线程退出synchronized代码块的时候,如果获取的是取值为null的锁记录 ,表示有锁重入,这时重置锁记录,表示重入计数减一

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第13张图片

当线程退出synchronized代码块的时候,如果获取的锁记录取值不为 null,那么使用casMark Word的值恢复给对象

  • 成功则解锁成功

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第14张图片

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

3、锁膨胀

如果在尝试加轻量级锁的过程中,cas操作无法成功,这是有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行锁膨胀,将轻量级锁变成重量级锁

  1. 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第15张图片

  1. 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程,轻量级锁没有阻塞队列的概念, 即为对象申请Monitor锁,让Object指向重量级锁地址01),然后自己进入Monitor 的EntryList 变成BLOCKED状态

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第16张图片

  1. 当Thread-0 推出synchronized同步块时,使用cas将Mark Word的值恢复给对象头,失败(因为对象的对象头中存储的是重量级锁的地址,状态变为10了之前的是00, 肯定恢复失败),那么会进入重量级锁的解锁过程,即按照Monitor的地址找到Monitor对象,将Owner设置为null唤醒EntryList 中的Thread-1线程

4、自旋优化(优化重量级锁竞争)

重量级锁竞争的时候,还可以使用自旋来进行优化(不立即加入Monitor的阻塞队列EntryList中,先自旋,我们可以想一下,如果立马到阻塞状态,务必会照成上下文切换,会耗费资源,倒不如我先试着自旋以下,如果成功,我们也顶多占用一点cpu),如果当前线程自旋成功(即在自旋的时候持锁的线程释放了锁),那么当前线程就可以不用进行上下文切换就获得了锁

  1. 自旋重试成功的情况

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第17张图片
2. 自旋重试失败的情况,自旋了一定次数还是没有等到持锁的线程释放锁

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第18张图片

  • 自旋会占用 CPU 时间单核 CPU自旋就是浪费多核 CPU 自旋才能发挥优势
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。Java 7 之后不能控制是否开启自旋功能

5、偏向锁

轻量级的锁中,我们可以发现,如果同一个线程对同一个对象进行重入锁时,也需要执行CAS操作,这是有点耗时,那么java6开始引入了偏向锁,只有第一次使用CAS时将对象的Mark Word头设置为入锁线程ID,之后这个入锁线程再进行重入锁时,发现线程ID是自己的,那么就不用再进行CAS了

举个栗子:
Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第19张图片

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第20张图片

5.1 偏向状态

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第21张图片

一个对象的创建过程

  1. 如果开启了偏向锁(默认是开启的),那么对象刚创建之后,Mark Word 最后三位的值101,并且这是它的Thread,epoch,age都是0,在加锁的时候进行设置这些的值.

  2. 偏向锁默认是延迟的,不会在程序启动的时候立刻生效,如果想避免延迟,可以添加虚拟机参数来禁用延迟:-XX:BiasedLockingStartupDelay=0来禁用延迟

  3. 注意:处于偏向锁的对象解锁后,线程id 仍存储于对象头

  4. 实验Test18.java,加上虚拟机参数XX:BiasedLockingStartupDelay=0进行测试

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第22张图片

输出结果如下,三次输出的状态码都为101

  biasedLockFlag (1bit): 1
  	LockFlag (2bit): 01
  biasedLockFlag (1bit): 1
  	LockFlag (2bit): 01
  biasedLockFlag (1bit): 1
  	LockFlag (2bit): 01

测试禁用:如果没有开启偏向锁,那么对象创建后最后三位的值为001,这时候它的hashcode,age都为0,hashcode是第一次用到hashcode时才赋值的。在上面测试代码运行时在添加 VM 参数-XX:-UseBiasedLocking禁用偏向锁(禁用偏向锁则优先使用轻量级锁),退出synchronized状态变回001

  1. 测试代码Test2.java 虚拟机参数-XX:-UseBiasedLocking

Synchronized详解(Monitor,轻量级锁,偏向锁,锁膨胀,锁消除,自旋)_第23张图片

  1. 输出结果如下,最开始状态为001,然后加轻量级锁变成00,最后恢复成001

    biasedLockFlag (1bit): 0
    	LockFlag (2bit): 01
    LockFlag (2bit): 00
    biasedLockFlag (1bit): 0
    	LockFlag (2bit): 01
    

5.2 撤销偏向锁-hashcode方法

  • 测试 hashCode:当调用对象的hashcode方法的时候就会撤销这个对象的偏向锁,因为使用偏向锁时没有位置存hashcode的值了

  • 测试代码时,使用虚拟机参数 -XX:BiasedLockingStartupDelay=0 确保我们的程序最开始使用了偏向锁!但是结果显示程序还是使用了轻量级锁。

5.3 撤销偏向锁-其它线程使用对象

这里我们是偏向锁撤销变成轻量级锁的过程,那么就得满足轻量级锁的使用条件,就是没有线程对同一个对象进行锁竞争,我们使用waitnotify 来辅助实现

5.4 撤销 - 调用 wait/notify

会使对象的锁变成重量级锁,因为wait/notify方法之后重量级锁才支持

5.5 批量重偏向

如果对象被多个线程访问,但是没有竞争,这时候偏向了线程一的对象又有机会重新偏向线程二,即可以不用升级为轻量级锁,其实要实现重新偏向是要有条件的,就是超过20对象对同一个线程如线程一撤销偏向时,那么第20个及以后的对象才可以将撤销对线程一的偏向这个动作变为将第20个及以后的对象偏向线程二。

6、同步省略 (锁消除)

  • 线程同步的代价是相当高的,同步的后果是降低并发性性能
  • 在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。
  • 如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。

例如下面的代码,根本起不到锁的作用:

public void f() {
    Object gql = new Object();
    synchronized(gql) {
        System.out.println(gql);
    }
}

代码中对hellis这个对象加锁,但是hellis对象的生命周期只在f()方法中(在栈中,不共享),并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉,优化成:

public void f() {
    Object hellis = new Object();
	System.out.println(gql);
}

字节码文件中并没有进行优化,加锁和释放锁的操作依然存在,同步省略操作是在解释运行时发生的

你可能感兴趣的:(JUC,轻量锁,偏向锁,synchronized,锁膨胀,Monitor)