Synchronized
本篇文章将围绕synchronized关键字,使用大量图片、案例深入浅出的描述CAS、synchronized Java层面和C++层面的实现、锁升级的原理、源码等
大概观看时间17分钟
可以带着几个问题去查看本文,如果认真看完,问题都会迎刃而解:
1、synchronized是怎么使用的?在Java层面是如何实现?
2、CAS是什么?能带来什么好处?又有什么缺点?
3、mark word是什么?跟synchronized有啥关系?
4、synchronized的锁升级优化是什么?在C++层面如何实现?
5、JDK 8 中轻量级锁CAS失败到底会不会自旋?
6、什么是object monitor?wait/notify方法是如何实现的?使用synchronized时,线程阻塞后是如何在阻塞队列中排序的?
...
synchronized Java层面实现
synchronized作用在代码块或方法上,用于保证并发环境下的同步机制
任何线程遇到synchronized都要先获取到锁才能执行代码块或方法中的操作
在Java中每个对象有一个对应的monitor对象(监视器),当获取到A对象的锁时,A对象的监视器对象中有个字段会指向当前线程,表示这个线程获取到A对象的锁(详细原理后文描述)
synchronized可以作用于普通对象和静态对象,当作用于静态对象、静态方法时,都是去获取其对应的Class对象的锁
synchronized作用在代码块上时,会使用monitorentry和monitorexit字节码指令来标识加锁、解锁
synchronized作用在方法上时,会在访问标识上加上synchronized
指令中可能出现两个monitorexit指令是因为当发生异常时,会自动执行monitorexit进行解锁
正常流程是PC 12-14,如果在此期间出现异常就会跳转到PC 17,最终在19执行monitorexit进行解锁
Object obj = new Object(); synchronized (obj) { }
在上篇文章中我们说过原子性、可见性以及有序性
synchronized加锁解锁的字节码指令使用屏障,加锁时共享内存从主内存中重新读取,解锁前把工作内存数据写回主内存以此来保证可见性
由于获取到锁才能执行相当于串行执行,也就保证原子性和有序性,需要注意的是加锁与解锁之间的指令还是可以重排序的
CAS
为了更好的说明synchronized原理和锁升级,我们先来聊聊CAS
在上篇文章中我们说过,volatile不能保证复合操作的原子性,使用synchronized方法或者CAS能够保证复合操作原子性
那什么是CAS呢?
CAS全称 Compare And Swap 比较并交换,读取数据后要修改时用读取的数据和地址上的值进行比较,如果相等那就将地址上的值替换为目标值,如果不相等,通常会重新读取数据再进行CAS操作,也就是失败重试
synchronized加锁是一种悲观策略,每次遇到时都认为会有并发问题,要先获取锁才操作
而CAS是一种乐观策略,每次先大胆的去操作,操作失败(CAS失败)再使用补偿措施(失败重试)
CAS与失败重试(循环)的组合构成乐观锁或者说自旋锁(循环尝试很像在自我旋转)
并发包下的原子类,依靠Unsafe大量使用CAS操作,比如AtomicInteger的自增
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } //var1是调用方法的对象,var2是需要读取/修改的值在这个对象上的偏移量,var4是自增1 public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { //var5是通过对象和字段偏移量获取到字段最新值 var5 = this.getIntVolatile(var1, var2); //cas:var1,var2找到字段的值 与 var5比较,相等就替换为 var5+var4 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
CAS只能对一个变量进行操作,如果要对多个变量进行操作,那么只能对外封装一层(将多个变量封装为新对象的字段),再使用原子类中的AtomicReference
不知各位同学有没有发现,CAS的流程有个bug,就是在读数据与比较数据之间,如果数据从A被改变到B,再改变到A,那么CAS也能执行成功
这种场景有的业务能够接受,有的业务无法接受,这就是所谓的ABA问题
而解决ABA问题的方式比较简单,可以再比较时附加一个自增的版本号,JDK也提供解决ABA问题的原子类AtomicStampedReference
CAS能够避免线程阻塞,但如果一直失败就会一直循环,增加CPU的开销,CAS失败后重试的次数/时长不好评估
因此CAS操作适用于竞争小的场景,用CPU空转的开销来换取线程阻塞挂起/恢复的开销
锁升级
早期版本的synchronized会将获取不到锁的线程直接挂起,性能不好
JDK 6 时对synchronized的实现进行优化,也就是锁升级
锁的状态可以分为无锁、偏向锁、轻量级锁、重量级锁
可以暂时把重量级锁理解为早期获取不到锁就让线程挂起,新的优化也就是轻量级锁和偏向锁
mark word
为了更好的说明锁升级,我们先来聊聊Java对象头中的mark word
我们下面的探究都是围绕64位的虚拟机
Java对象的内存由mark word、klass word、如果是数组还要记录长度、实例数据(字段)、对其填充(填充到8字节倍数)组成
mark word会记录锁状态,在不同锁状态的情况下记录的数据也不同
下面这个表格是从无锁到重量级锁mark word记录的内容
|----------------------------------------------------------------------|--------|--------| | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | 无锁 |----------------------------------------------------------------------|--------|--------| | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | 偏向锁 |----------------------------------------------------------------------|--------|--------| | ptr_to_lock_record:62 | lock:2 | 轻量级锁 |----------------------------------------------------------------------|--------|--------| | ptr_to_heavyweight_monitor:62 | lock:2 | 重量级锁 |----------------------------------------------------------------------|--------|--------|
identity_hashcode 用于记录一致性哈希
age 用于记录GC年龄
biased_lock 标识是否使用偏向锁,0表示未开启,1表示开启
lock 用于标识锁状态标志位,01无锁或偏向锁、00轻量级锁、10重量级锁
thread 用于标识偏向的线程
epoch 记录偏向的时间戳
ptr_to_lock_record 记录栈帧中的锁记录(后文介绍)
ptr_to_heavyweight_monitor 记录获取重量级锁的线程
jol查看mark word
比较熟悉mark word的同学可以跳过
了解mark word后再来熟悉下不同锁状态下的mark word,我使用的是jol查看内存
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core --> <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.12</version> </dependency>
无锁
各位同学实验时的mark word可能和我注释中的不同,我们主要查看锁标识的值和是否启用偏向锁
public void noLock() { Object obj = new Object(); //mark word 00000001 被unused:1,age:4,biased_lock:1,lock:2使用,001表示0未启用偏向锁,01表示无锁 //01 00 00 00 (00000001 00000000 00000000 00000000) //00 00 00 00 (00000000 00000000 00000000 00000000) ClassLayout objClassLayout = ClassLayout.parseInstance(obj); System.out.println(objClassLayout.toPrintable()); //计算一致性哈希后 //01 b6 ce a8 //6a 00 00 00 obj.hashCode(); System.out.println(objClassLayout.toPrintable()); //进行GC 查看GC年龄 0 0001 0 01 前2位表示锁状态01无锁,第三位biased_lock为0表示未启用偏向锁,后续四位则是GC年龄age 1 //09 b6 ce a8 (00001001 10110110 11001110 10101000) //6a 00 00 00 (01101010 00000000 00000000 00000000) System.gc(); System.out.println(objClassLayout.toPrintable()); }
轻量级锁
public void lightLockTest() throws InterruptedException { Object obj = new Object(); ClassLayout objClassLayout = ClassLayout.parseInstance(obj); //1334729950 System.out.println(obj.hashCode()); //0 01 无锁 //01 4e c0 d5 (00000001 01001110 11000000 11010101) //6a 00 00 00 (01101010 00000000 00000000 00000000) System.out.println(Thread.currentThread().getName() + ":"); System.out.println(objClassLayout.toPrintable()); Thread thread1 = new Thread(() -> { synchronized (obj) { // 110110 00 中的00表示轻量级锁其他62位指向拥有锁的线程 //d8 f1 5f 1d (11011000 11110001 01011111 00011101) //00 00 00 00 (00000000 00000000 00000000 00000000) System.out.println(Thread.currentThread().getName() + ":"); System.out.println(objClassLayout.toPrintable()); //1334729950 //无锁升级成轻量级锁后 hashcode未变 对象头中没存储hashcode 只存储拥有锁的线程 //(实际上mark word内容被存储到lock record中,所以hashcode也被存储到lock record中) System.out.println(obj.hashCode()); } }, "t1"); thread1.start(); //等待t1执行完 避免 发生竞争 thread1.join(); //轻量级锁 释放后 mark word 恢复成无锁 存储哈希code的状态 //01 4e c0 d5 (00000001 01001110 11000000 11010101) //6a 00 00 00 (01101010 00000000 00000000 00000000) System.out.println(Thread.currentThread().getName() + ":"); System.out.println(objClassLayout.toPrintable()); Thread thread2 = new Thread(() -> { synchronized (obj) { //001010 00 中的00表示轻量级锁其他62位指向拥有锁的线程 //28 f6 5f 1d (00101000 11110110 01011111 00011101) //00 00 00 00 (00000000 00000000 00000000 00000000) System.out.println(Thread.currentThread().getName() + ":"); System.out.println(objClassLayout.toPrintable()); } }, "t2"); thread2.start(); thread2.join(); }
偏向锁
public void biasedLockTest() throws InterruptedException { //延迟让偏向锁启动 Thread.sleep(5000); Object obj = new Object(); ClassLayout objClassLayout = ClassLayout.parseInstance(obj); //1 01 匿名偏向锁 还未设置偏向线程 //05 00 00 00 (00000101 00000000 00000000 00000000) //00 00 00 00 (00000000 00000000 00000000 00000000) System.out.println(Thread.currentThread().getName() + ":"); System.out.println(objClassLayout.toPrintable()); synchronized (obj) { //偏向锁 记录 线程地址 //05 30 e3 02 (00000101 00110000 11100011 00000010) //00 00 00 00 (00000000 00000000 00000000 00000000) System.out.println(Thread.currentThread().getName() + ":"); System.out.println(objClassLayout.toPrintable()); } Thread thread1 = new Thread(() -> { synchronized (obj) { //膨胀为轻量级 0 00 0未启用偏向锁,00轻量级锁 //68 f4 a8 1d (01101000 11110100 10101000 00011101) //00 00 00 00 (00000000 00000000 00000000 00000000) System.out.println(Thread.currentThread().getName() + ":"); System.out.println(objClassLayout.toPrintable()); } }, "t1"); thread1.start(); thread1.join(); }
重量级锁
public void heavyLockTest() throws InterruptedException { Object obj = new Object(); ClassLayout objClassLayout = ClassLayout.parseInstance(obj); Thread thread1 = new Thread(() -> { synchronized (obj) { //第一次 00 表示 轻量级锁 //d8 f1 c3 1e (11011000 11110001 11000011 00011110) //00 00 00 00 (00000000 00000000 00000000 00000000) System.out.println(Thread.currentThread().getName() + ":"); System.out.println(objClassLayout.toPrintable()); //用debug控制t2来竞争 //第二次打印 变成 10 表示膨胀为重量级锁(t2竞争) 其他62位指向监视器对象 //fa 21 3e 1a (11111010 00100001 00111110 00011010) //00 00 00 00 (00000000 00000000 00000000 00000000) System.out.println(Thread.currentThread().getName() + ":"); System.out.println(objClassLayout.toPrintable()); } }, "t1"); thread1.start(); Thread thread2 = new Thread(() -> { synchronized (obj) { //t2竞争 膨胀为 重量级锁 111110 10 10为重量级锁 //fa 21 3e 1a (11111010 00100001 00111110 00011010) //00 00 00 00 (00000000 00000000 00000000 00000000) System.out.println(Thread.currentThread().getName() + ":"); System.out.println(objClassLayout.toPrintable()); } }, "t2"); thread2.start(); thread1.join(); thread2.join(); //10 重量级锁 未发生锁降级 //3a 36 4d 1a (00111010 00110110 01001101 00011010) //00 00 00 00 (00000000 00000000 00000000 00000000) System.out.println(Thread.currentThread().getName() + ":"); System.out.println(objClassLayout.toPrintable()); }
轻量级锁
轻量级锁的提出是为了减小传统重量级锁使用互斥量(挂起/恢复线程)所产生的开销
面对较少的竞争场景时,获取锁的时间总是短暂的,而挂起线程用户态、内核态的开销比较大,使用轻量级锁减少开销
那么轻量级锁是如何实现的呢?
轻量级锁主要由lock record、mark word、CAS来实现,lock record存储在线程的栈帧中,来记录锁的信息
加锁
查看对象是不是无锁状态,如果对象是无锁状态,会将mark word复制到lock record锁记录中的displaced mark word
然后再尝试使用CAS尝试将mark word中部分内容替换指向这个lock record,如果成功表示获取锁成功
如果对象持有锁,会查看持有锁的线程是不是当前线程,这种可重入的情况下lock record中记录不再是mark word而是null
可重入的情况下,只需要进行自增计数即可,解锁时遇到null的lock record则扣减
如果CAS失败或者持有锁的线程不是当前线程,就会触发锁膨胀
关键代码如下:
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) { //当前对象的mark word markOop mark = obj->mark(); assert(!mark->has_bias_pattern(), "should not see bias pattern here"); //如果当前对象是无锁状态 if (mark->is_neutral()) { //将mark word复制到lock record lock->set_displaced_header(mark); //CAS将当前对象的mark word内容替换为指向lock record if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) { TEVENT (slow_enter: release stacklock) ; return ; } } else //如果有锁 判断是不是当前线程获取锁 if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) { assert(lock != mark->locker(), "must not re-lock the same lock"); assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock"); //可重入锁 复制null lock->set_displaced_header(NULL); return; } //有锁并且获取锁的线程不是当前线程 或者 CAS失败 进行膨胀 lock->set_displaced_header(markOopDesc::unused_mark()); ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD); }
15000字、6个代码案例、5个原理图让你彻底搞懂Synchronized(下)
https://developer.aliyun.com/article/1504425?spm=a2c6h.13148508.setting.16.55974f0e4rkwEt