美文网首页
synchronized

synchronized

作者: 晚歌歌 | 来源:发表于2021-08-23 16:12 被阅读0次

简介

synchronized是Java中的关键字,是一种可重入非公平同步锁。

三种应用方式

  • 修饰非静态方法,作用于当前对象加锁,进入同步代码前要获得当前对象的锁
  • 修饰静态方法,作用于当前类对象(.class对象)加锁,进入同步代码前要获得当前类对象的锁
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

Java对象头与Monitor

对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充
对象头由以下三部分组成、Mark Word、指向类的指针、数组长度(只有数组对象才有)


image.png

每个对象都存在着一个 monitor (即上述锁状态中的重量级锁)与之关联,通过 monitor 对对象进行加锁

字节码

锁对象:monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置

monitorenter
...
monitorexit

锁方法:方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor

public synchronized void husband();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED  // 这里
    Code:

wait / notify必须在synchronized方法/代码块中调用

  • 因为wait方法作用是线程释放对象的锁并进入休眠状态,而notify / notifyAll方法作用要通知等待对象锁的那些线程继续执行
  • 只有当前线程已经获取对象的锁时,才能在该对象上调用wait / notify / notifyAll方法
  • 否则导致java.lang.IllegalMonitorStateException

锁优化

JDK 1.6 之前,synchronized 是重量级锁,也就是说 synchronized 在释放和获取锁时都会从用户态转换成内核态,后续进行了锁的各种优化

本文我们介绍了 4 种优化 synchronized 的方案,其中锁膨胀和自适应自旋锁是 synchronized 关键字自身的优化实现,而锁消除和锁粗化是 JVM 虚拟机对 synchronized 提供的优化方案,这些优化方案最终使得 synchronized 的性能得到了大幅的提升,也让它在并发编程中占据了一席之地。

锁膨胀

image.png

所谓的锁膨胀是指 synchronized 从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁的过程,它叫做锁膨胀也叫做锁升级

偏向锁

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。下面我们接着了解轻量级锁。

轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据,通过CAS实现加锁和解锁。

线程的栈帧中有一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
线程会拷贝对象头中的Mark Word到锁记录(Lock Record)中,然后使用CAS操作尝试将锁对象的Mark Word指针指向Lock Record,并将线程栈帧中的Lock Record里的owner指针指向Object的 Mark Word。如果更新成功,则表示获取了轻量级锁。

需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

锁消除

锁消除指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。

锁消除的依据是逃逸分析的数据支持,如局部变量 StringBuffer 的 append() 方法,或 Vector 的 add() 方法,在很多情况下是可以进行锁消除的

锁粗化

锁粗化是指,将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
锁粗化的作用:如果检测到同一个对象执行了连续的加锁和解锁的操作,则会将这一系列操作合并成一个更大的锁,从而提升程序的执行效率。

案例:
如果在 for 循环中定义锁,那么锁的范围很小,但每次 for 循环都需要进行加锁和释放锁的操作,性能是很低的;但如果我们直接在 for 循环的外层加一把锁,那么对于同一个对象操作这段代码的性能就会提高很多

与ReentrantLock区别

synchronized ReentrantLock
1 可重入、非公平 可重入、可公平、可非公平
2 通过JVM底层实现,通过操作对象内置的monitor监视器进行锁的操作,字节码上体现为指令monitorenter和monitorexit,而对synchronized方法在字节码则体现为ACC_SYNCHRONIZED标记,实际上也是对对象进行加锁 基于JDK的CAS和AQS队列同步器,AQS内部通过一个int类型的成员变量state来控制同步状态,当state=0时,则说明没有任何线程占有共享资源的锁,当state=1时,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待
3 加锁解锁由JVM底层控制 需要手动lock和unlock
4 JDK1.6对锁进行偏向锁、轻量级锁、自旋锁、锁消除优化后,两者性能差不多,JAVA团队偏向于使用synchronized CAS乐观锁能够减少线程的上下文切换
5 提供Contiditon类灵活控制锁;能够中断等待锁的线程

相关文章

网友评论

      本文标题:synchronized

      本文链接:https://www.haomeiwen.com/subject/hejliltx.html