一、synchronized如何解决线程安全问题
大家对于synchronized应该不陌生,在我们实际业务中经常需要书写同步代码块来保证线程安全性,而synchronized便是我们常用的一个加锁方式。那么问题来了,在高并发的情况下,synchronized是如何保证线程安全的呢?
Monitor
monitor又叫管程,是操作系统中用来保证共享资源正确使用的结构,java中也使用此机制来保证线程安全问题。当对一个对象执行synchronized操作时,在发生竞争的状态下,该对象的markword会存放操作系统的monitor地址,通过该monitor保证并发安全问题
monitor结构图
- 初始状态:monitor的owner为null
- t1:Thread-0、Thread-1依次获得了该锁并执行wait,释放锁并移到waitSet中
- t2:Thread-2获得该锁,owner指向Thread-2
- t3:此时Thread-2并没有释放锁,而Thread-3 4 5 都来到了临界区,由于此时owner不为null,因此它们进入阻塞队列
- t4:Thread-2执行notify(),随机从waitSet中唤醒一个线程,移至EntryList
二、轻量级锁
前文中讲到synchronized是使用monitor在多线程竞争的情况下保证并发安全的,但当我们业务中某个功能使用的人很少,并没有形成竞态时,使用monitor的方式过于“沉重”,因为monitor涉及到用户态->核心态的来回切换,jdk的开发人员替我们考虑到了这一点,当锁没有触发竞态条件时,使用轻量级锁的方式处理synchronized。
轻量级锁-过程
-
创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的
Mark Word
-
让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存
入锁记录
-
如果 cas 失败,有两种情况
-
3.1 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
-
3.2 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
- 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重
入计数减一
- 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象
头
成功,则解锁成功
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
- 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重
-
锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有
竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
自旋优化
当多线程竞争锁时,竞争失败的线程并不会马上进入阻塞队列,而是重试一定次数,若获取到锁则执行临界区代码,避免了上下文的切换,若是没有获取到则进入阻塞队列
三、偏向锁
偏向锁其实是一个非常鸡肋的东西,只有在确定只有一个线程会使用该锁的情况下,偏向锁才能体现它的作用。但凡有其它线程使用它,哪怕没有构成竞态,也会造成偏向锁的撤销或重偏向,造成性能的浪费,因此在jdk15以后废弃掉了它
只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
但是一旦其它线程使用该锁,哪怕没有构成竞态,也会涉及偏向锁的撤销
偏向锁的三种撤销场景
场景一:调用锁对象的hashcode
调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被
撤销
- 轻量级锁会在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
场景二:调用 wait/notify
场景三:其他线程使用该锁
将偏向锁升级为轻量锁
偏向锁的重偏向和批量撤销
重偏向
上文中说到,当有多个线程使用该锁时(不触发竞态),会将偏向锁升级为轻量锁,但当撤销的次数超过20次时,该锁会重偏向于当前使用此锁的线程
批量撤销
当一个锁的撤销偏向锁阈值超过 40 次后,jvm 会认为此类对象的锁不适合用偏向锁优化,会撤销该类对象的所有偏向锁,哪怕是新new的也不使用偏向锁
偏向锁的竞争问题
假设在使用偏向锁的情况下出现如下状况:
t1: Thread-1获得该锁,发现该锁是偏向锁,且处于匿名偏向锁状态(可偏向未锁定),将markWord中的0替换成线程id,执行同步代码块
t2: Thread-2也要获得该锁,发现该锁是Thread-1的偏向锁,开始进行偏向锁撤销,偏向锁的撤销需要等待全局安全点(safe point,代表了一个状态,在该状态下所有线程都是暂停的),暂停持有偏向锁的线程,检查持有偏向锁的线程状态(遍历当前JVM的所有线程,如果能找到,则说明偏向的线程还存活),如果线程还存活,则检查线程是否在执行同步代码块中的代码,如果是,则升级为轻量级锁,进行CAS竞争锁;
如果持有偏向锁的线程未存活,或者持有偏向锁的线程未在执行同步代码块中的代码,则进行校验是否允许重偏向,如果不允许重偏向,则撤销偏向锁,将Mark Word设置为无锁状态(未锁定不可偏向状态),然后升级为轻量级锁,进行CAS竞争锁;