「Java并发编程」synchronized相关面试题总结

说说本身对于synchronized关键字的了解

synchronized关键字用于解决多个线程之间访问资源的同步性,synchronized关键字能够保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行数组

值得注意的是,在Java早期,JDK1.6以前,synchronized属于重量级锁,效率低下。安全

缘由在于:多线程

监视器锁【monitor】依赖于底层操做系统的Mutex Lock实现,Java的线程是映射到操做系统的原生线程之上的。若是要挂起或唤醒一个线程,都须要操做系统帮忙完成,而操做系统实现线程之间的切换时须要从用户态转化到内核态,须要消耗比较长的时间ide

可是,JDK1.6以后,Java官方从JVM层面对synchronized关键字进行了较大的优化,效率不可同日而语。主要的优化有:自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减小锁操做的开销。性能

synchronized关键字的三种使用
  1. 修饰实例方法:做用于当前对象实例加锁,进入同步代码前要得到 当前对象实例的锁测试

  2. 修饰静态方法: 也就是给当前类加锁,会做用于类的全部对象实例 ,进入同步代码前要得到 当前 class 的锁优化

注意:静态成员不属于任何一个实例对象,是类成员!所以,一个线程A调用一个实例对象的非静态synchronized方法,一个线程B调用这个实例对象的所属类的静态synchronized方法,是被容许的。 由于访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁this

  1. 修饰代码块 :给括号内配置的对象加锁。synchronized(this|object) 表示进入同步代码库前要得到给定对象的锁。synchronized(类.class) 表示进入同步代码前要得到 当前 class 的锁spa

synchronized关键字的底层原理

经过对.class文件的编译能够发现:操作系统

  • 同步方法经过ACC_SYNCHRONIZED修饰。

  • 代码块同步使用monitorenter和monitorexit两个指令实现。

虽然二者实现细节不一样,但其实本质上都是JVM基于进入和退出Monitor对象来实现同步,JVM的要求以下:

  • monitorenter指令会在编译后插入到同步代码块的开始位置,而monitorexit则会插入到方法结束和异常处。

  • 每一个对象都有一个monitor与之关联,且当一个monitor被持有以后,他会处于锁定状态。

  • 线程执行到monitorenter时,会尝试获取对象对应monitor的全部权。

  • 在获取锁时,若是对象没被锁定,或者当前线程已经拥有了该对象的锁(可重进入,不会锁死本身),将锁计数器加一,执行monitorexit时,锁计数器减一,计数为零则锁释放。

  • 获取对象锁失败,则当前线程陷入阻塞,直到对象锁被另一个线程释放。

JDK1.6以后对synchronized关键字进行的优化

优化:偏向锁,轻量级锁,自旋锁,适应性自旋锁,锁消除,锁粗化。

锁主要存在的四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁能够升级不可降级,这种策略是为了提升得到锁和释放锁的效率。

Java对象头的组成

锁存在于Java对象头里,对象头的组成部分:

  • Mark Word:存储对象的hashCode或锁信息等。

  • Class Metadata Address:存储到对象类型数据的指针。

  • Array length:数组的长度(若是当前对象是数组)

Java对象头又存在于Java堆中,堆内存分为三部分:对象头,实例数据和对齐填充。

MarkWord的组成

Java对象头的MardWord中记录了对象和锁的相关信息,无锁状态下,Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。在64位的JVM中,Mark Word为64 bit

图片


在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。锁升级的功能也主要靠MarkWord中锁标志位是否偏向锁标志完成。

锁升级的过程

锁升级的过程:无锁,偏向锁,轻量级锁,重量级锁

图片


偏向锁

HotSpot的做者通过研究发现,大多数状况下,锁不只不存在多线程竞争,并且老是由同一线程屡次得到,为了让线程得到锁的代价更低而引入了偏向锁。

偏向锁的适用场景

偏向锁主要用于优化:同一线程屡次申请同一个锁的竞争,在某些状况下,大部分时间都是同一个线程竞争锁资源的。

图片


偏向锁的加锁

主要流程:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,之后该线程在进入和退出同步块时不须要进行CAS操做来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁

  • 若是测试成功,表示线程已经得到了锁。

  • 若是测试失败,则须要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):若是没有设置,则使用CAS竞争锁。若是设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

偏向锁的撤销

一旦出现其余线程竞争锁资源时,偏向锁就会被撤销。偏向锁的撤销可能须要等待全局安全点【在这个时间点上没有正在执行的字节码】。

  • 首先暂停持有该锁的线程,而后检查持有偏向锁的线程是否活着,若是线程不处于活动状态,则将对象头设置成无锁状态。

  • 若是持有偏向锁的线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么从新偏向于其余线程,要么恢复到无锁或者标记对象不适合做为偏向锁,最后唤醒在暂停的线程

偏向锁的关闭

偏向锁在Java 6和Java 7里是默认启用的,可是它在应用程序启动几秒钟以后才激活,若有必要可使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。

若是说一般处于竞争状态,能够经过- XX:-UseBiasedLocking=false,进入轻量级锁状态。

轻量级锁

如偏向锁存在,若有另外一线程竞争锁,且对象头MarkWord中的线程ID与当前线程ID不一样,则该线程将会尝试CAS操做获取锁,获取失败,表明锁存在竞争,偏向锁向轻量级锁升级

图片


轻量级锁的加锁
  • 线程在执行同步块以前,JVM会先在当前线程的栈台中建立用于存储锁记录的空间【Displaced Mark Word】,并将对象头中的Mark Word复制到锁记录中

  • 而后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。替换成功,则当前线程得到锁。替换失败,表示其余线程竞争锁,当前线程尝试使用自旋来获取锁。

轻量级锁的解锁
  • 使用原子的CAS操做将【Displaced Mark Word】替换回对象头。替换成功,表示没有竞争发生。替换失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

轻量级锁的适用场景

线程交替执行同步块,绝大部分的锁在整个同步周期内都不存在长时间的竞争

锁的优缺点对比

锁优势缺点适用场景偏向锁加锁和解锁不须要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。若是线程间存在锁竞争, 会带来额外的锁撤销的消耗。适用于只有一个线程访问同步块场景。轻量级锁竞争的线程不会阻塞,提升了程序的响应速度。若是始终得不到所竞争的线程使用自旋会消耗CPU。追求响应时间。同步块执行速度很是快。重量级锁线程竞争不使用自旋,不会消耗CPU。线程阻塞,响应时间缓慢。追求吞吐量。同步块执行速度较长。

总结
  1. JVM在JDK 1.6中引入了分级锁机制来优化synchronized

  2. 当一个线程获取锁时,首先对象锁成为一个偏向锁这是为了不在同一线程重复获取同一把锁时,用户态和内核态频繁切换

  3. 若是有多个线程竞争锁资源,锁将会升级为轻量级锁这适用于在短期内持有锁,且分锁交替切换的场景轻量级锁还结合了自旋锁避免线程用户态与内核态的频繁切换

  4. 若是锁竞争太激烈(自旋锁失败),同步锁会升级为重量级锁

  5. 优化synchronized同步锁的关键:减小锁竞争应该尽可能使synchronized同步锁处于轻量级锁偏向锁,这样才能提升synchronized同步锁的性能经常使用手段减小锁粒度:下降锁竞争减小锁的持有时间,提升synchronized同步锁在自旋时获取锁资源的成功率,避免升级为重量级锁

  6. 锁竞争激烈时,能够考虑禁用偏向锁禁用自旋锁

synchronized关键字与ReentrantLock的区别 共同点
  • 都是可重入锁:本身能够再次获取本身的内部锁【避免一个线程获取锁以后,再次尝试获取锁时形成的死锁】。同一线程每次获取锁,计数器加一,释放锁,计数器减一,计数为0,表明彻底释放该锁。

不一样点
  • synchronized依赖于JVM实现,ReentrantLock依赖于API。

  • 相比synchronized,ReentrantLock增长了一些高级功能。主要来讲主要有三点:等待可中断 : ReentrantLock提供了一种可以中断等待锁的线程的机制,经过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程能够选择放弃等待,改成处理其余事情。可实现公平锁 : ReentrantLock能够指定是公平锁仍是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先得到锁。ReentrantLock默认状况是非公平的,能够经过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是不是公平的。可实现选择性通知(锁能够绑定多个条件): synchronized关键字与wait()和notify()/notifyAll()方法相结合能够实现等待/通知机制。ReentrantLock类固然也能够实现,可是须要借助于Condition接口与newCondition()方法。

若是感受本文对你有帮助,点赞再看支持一下

相关文章
相关标签/搜索