深入理解JUC之Synchronized原理

1、理解Synchronized

Synchronized是JDK为我们提供的锁,主要用于处理线程安全问题,线程安全问题的根本原因是线程上下文切换,导致字节码指令的交错,我们通过Synchronized保护共享资源,在并发场景下,采用上锁的方式使得资源只能某个时刻只能被一个线程操作,这样防止多个线程同时操作共享资源导致的错误问题,保证同一时刻有且只有一个线程在操作共享数据。也就是说,当某个线程操作Synchronized锁保护的资源时,其他线程只能处于阻塞状态,只有释放了锁,其他的线程中某个线程才有机会获得锁对象处理资源。Synchronized相比于Volatile不仅保证了可见性和有序性,也保证了原子性,适用于解决多个线程操作共享资源所带来的线程安全问题

2、理解Java对象头

在了解Synchronized之前,需要先明白对象头和Monitor管程,先来说说对象头,对象头是java实例对象的一部分,一个Java对象主要包括三个部分:对象头 + 对象体 + 对齐字节,结构如下:
在这里插入图片描述

在多线程并发访问统一资源时,需要依赖对象头中的MarkWord指向Monitor管程(后面会谈到),MarkWord中主要记录了对象的hashCode、锁信息、分代年龄、GC标志等信息。KlassWord主要用于指向该对象的Class类对象(Synchronized主要利用MarkWord,而不是KlassWord),我们使用Synchronized修饰该Java对象来上锁,防止并发问题时,多个线程抢夺锁资源,MarkWord存储的内容会和该对象的Monitor管程的引用地址进行交换,所以Synchronized最终其实是依赖锁对象指向的Monitor管程来控制线程并发访问问题!

3、Monitor管程(监视器)

每个 Java 对象都可以关联一个对应的 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的MarkWord就指向Monitor对象(monitorenter指令),Monitor主要分为三个部分:Owner + EntryList + WaitSet
在这里插入图片描述
但是真正需要Monitor的时机仅仅在锁类型为重量级锁时,才会需要Monitor的参入来把控并发的安全性问题!起初还没有线程得到锁资源时,Owner为null,只要某个线程获取到了锁对象进入到同步代码块中,Owner就会指向当前得到所资源的线程,例如图中的Thread2,但是其他没有竞争到锁资源的线程都会阻塞,在EntryList中等待(线程处于BLOCKED状态),当Thread2释放锁时,后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的(不会按照排队顺序执行线程,而是随机竞争),下一个抢到资源的线程就会被Owner引用,其他线程又进入到EntryList中阻塞。

当Synchronized代码块中的线程主动调用wait()方法,此时当前运行的线程会释放锁资源,直接进入到Monitor的Waiting中(此时线程处于WAITING状态),只有下一个得到锁的线程主动调用notity()或notifyAll()时,WAITING状态的线程才有机会唤醒,被唤醒后线程会进入到EntryList中阻塞等待抢夺CPU,此过程中WaitSet中的线程状态从WAITING变成BLOCKED状态。

值得注意的是,只有得到锁资源的线程才有资格调用wait()和notify()方法,并且notify()方法唤醒线程也只是随机唤醒,并不能指定唤醒某个线程,Monitor只有在重量级锁情况下才会使用,其他锁状态下并不会使用Monitor管程,接下来介绍一下Synchronized不同类型的锁场景!

4、Synchronized进阶原理

不同锁状态,MarkWord格式如下,依次是:无锁、偏向锁、轻量级锁、重量级锁
在这里插入图片描述
这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁!

4.1、偏向锁

假设当前场景只有一个线程,并没有线程竞争关系,不仅不存在多线程竞争,而且总是由同一线程多次获得,那么此时这个线程如果获取到锁资源,锁状态就是偏向锁,此时Synchronized修饰的锁对象会把当前线程的ID存入到MarkWord中,以后只要不发生竞争,这个对象就归该线程所有。偏向锁相比于轻量级锁最大的好处就是不需要每次获取锁都需要进行进行CAS操作。开启了偏向锁,MarkWord最后3位为101,这时它的 hashcode、分代年龄age 都为 0,第一次调用hashCode()时才会进行赋值(原因是MarkWord只有64位,而线程ID就要占用54位),同理当调用对象的hashCode()方法后,偏向锁状态也会被撤销,因为MarkWord大小有限!偏向锁存在的最大意义就是相比于轻量级锁,不需要每次获取锁都要进行CAS操作,节省了很多性能消耗!

4.2、轻量级锁

如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。偏向锁升级就会变成轻量级锁,轻量级锁和偏向锁使用场景不同,偏向锁是仅仅只有一个线程获得锁资源,没有其他线程和该线程共用锁场景下才会使用。但是轻量级锁是多个线程共同获取一个锁对象,但是不存在竞争关系,也就是说这些线程虽然都会访问Synchronized中的共享资源,但是他们获取锁的时间是岔开的,也就是不存在同一时刻两个线程抢夺同一锁资源的情况!

轻量级锁对象需要和想要获取锁资源线程的 LockRecord(锁记录)进行CAS操作,具体是与锁记录中的地址值进行CAS交换,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的MarkWord,当线程获得到锁资源,也就是CAS交换成功,会把锁记录的地址和锁对象的MarkWord进行交换,通过比较锁状态号00 和 01,如果CAS比较交换成功就是上锁成功了,释放锁时同样还要进行一次CAS交换,恢复成最初的状态,等待下次的CAS获取轻量级锁
在这里插入图片描述
当线程重复获取当前锁,也就是进行锁重入操作时,还会重复进行CAS操作,此时CAS操作一定会失败(这是第一种CAS失败的场景,后续会介绍第二种),此时会再添加一条 LockRecord 作为重入的计数
在这里插入图片描述
当轻量级锁被占用,另一个线程对该锁进行获取,那么此时就不符合线程之间没有竞争的场景了!也就是说轻量级锁不能处理线程竞争锁的情况,因为此时锁对象的MarkWord锁标志号已经是00(00代表轻量级锁),那么其他线程此时来进行CAS操作一定会失败(除了锁重入,这时第二种CAS失败的原因),此时会出现锁膨胀现象,也就是轻量级锁会升级为重量级锁!
在这里插入图片描述
当发生锁膨胀时,想要释放锁,就不能通过CAS释放锁资源了,因为此时轻量级锁已经膨胀为重量级锁,需要进入到重量级锁的解锁流程了!

4.3、重量级锁

上面提到过,重量级锁的使用场景就是多个线程竞争同一个锁资源,如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
在这里插入图片描述
轻量级锁膨胀为重量级锁后,不能采取正常轻量级锁的CAS解锁方式了,因为此时所膨胀后,对象头的MarkWord已经指向了Monitor管程,锁记录LockRecord中的对象引用依旧指向锁对象,并且膨胀后,Monitor的Owner会指向目前正在占用的锁对象的Thread,而另一个同样想要获取锁资源的线程只能默默在Monitor的EntryList中阻塞。当Thread0想要释放锁资源,只能将Monitor中的Onwer置为null(monitorexit指令),同时唤醒EntryList中等待的线程重新竞争锁资源
在这里插入图片描述

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值