作者主页:paper jie_博客
本文作者:大家好,我是paper jie,感谢你阅读本文,欢迎一建三连哦。
本文于《JavaEE》专栏,本专栏是针对于大学生,编程小白精心打造的。笔者用重金(时间和精力)打造,将MySQL基础知识一网打尽,希望可以帮到读者们哦。
其他专栏:《MySQL》《C语言》《javaSE》《数据结构》等
内容分享:本期将会分享锁的策略知识.这也是面试题常考的问题.
目录
乐观锁与悲观锁
轻量级锁与重量级锁
自旋锁与挂起等待锁
普通互斥锁和读写锁
为什么要引入读写锁
公平锁与非公平锁
可重入锁和不可重入锁
synchronized锁与操作系统自带锁对比
synchronized的优化策略
锁的升级
偏向锁阶段(没有其他锁来竞争)
轻量级锁阶段(有锁竞争,但是不多)
重量级锁阶段(有锁竞争,且很多)
锁消除
锁粗化
相关面试题
你是什么理解乐观锁和悲观锁的,具体怎么实现?
介绍一下读写锁
synchronized是可重入锁吗
什么是自旋锁,为什么要使用自旋锁呢,缺点是什么?
乐观锁与悲观锁
乐观锁就是在加锁前,预估发生锁冲突的概率不大,在进行加锁的时候做的工作不多.这样加锁的速度就会比较快,但是会更容易消耗CPU资源.
悲观锁就是在加锁前,预估发生锁冲突的概率比较大.在进行加锁的时候做的工作就比较多.这样的加锁速度就会比较慢,但这个过程发生问题的几率就不大,还可以更节省CPU资源.
举个栗子:
这就好比两个同学去问问题.A同学认为老师有时间就直接跑过去了,而B同学会先给老师打个电话确认一下老师有没有空再去.A同学就是乐观锁,B同学就是悲观锁. 显而易见,A同学就容易遇到老师很忙的情况,而B同学就可以避免.但老师要是不忙,A同学的速度就会更快.
轻量级锁与重量级锁
轻量级锁就是加锁的开销小,加锁速度快.因为轻量级锁不怎么到涉及线程的调度,所以开销就比较小.
而重量级锁就是加锁的开销比较大,加锁速度慢.因为重量级锁涉及到大量的线程调度,遇到锁竞争就需要将线程调度出CPU,等待解锁再由系统随机唤醒一个等待线程,所所以开销会比较慢.
这里可以理解为轻量级锁就是乐观锁,重量级锁就是悲观锁.而前者是站在结果发生后的角度来评价,后者是站在发生前的角度来预估.
自旋锁与挂起等待锁
自旋锁是轻量级锁的经典实现,自旋锁也就是乐观锁. 它就是在加锁的时候搭配一个while循环,加锁成功就退出循环,要是没有成功发生堵塞,也不会退出CPU,而是通过while循环不断尝试获取到锁. 这种循环就叫做自旋.一旦当其他线程释放锁后,它就可以第一时间拿到锁了. 但是自旋锁只能在锁竞争不大的情况下使用,不然就会白白自旋,浪费CPU资源.
挂起等待锁是重量级锁的经典实现.挂起等待锁也就是悲观锁. 它就是在尝试加锁失败后不会再次尝试获取锁.而是调度出CPU,等待其他线程释放锁后,由系统来随机唤醒一个等待的线程.因为它在等待的过程中会有内核调度器介入,会有大量的线程调度,获取锁的时间就会比较慢.挂起等待锁的适用场景是在锁冲突比较严重的情况下.
而在我们Java中,Synchronized这个锁是一个乐观锁/悲观锁自适应,轻量级锁/重量级锁自适应,自旋/挂起等待锁自适应.它可以根据不同的场景来做切换.
普通互斥锁和读写锁
我们Java的synchronized就是普通互斥锁,不管是读还是写都是进行加锁.而读写锁会有两种情况: 1.加读锁 2. 加写锁. 读写锁就是一个线程读时,另一个线程只能读不能写. 一个线程写时,另一个线程不能读也不能写.
1 读锁和读锁之间不会发生堵塞.
2 读锁和写锁之间会发生堵塞.
3 写锁和写锁之间会发生堵塞.
为什么要引入读写锁
因为多线线程进行读操作,它本身就是线程安全的,但是对于普通互斥锁来说,两个线程读,也会发生锁竞争,堵塞,这就会造成一定的性能损失. 但是对读操作不加锁的话,就怕一个线程读,一个线程写,导致读到的内容不完整.
基于这个情况下就引入了读写锁,它就可以解决对于读操作不加锁的问题.直接将读引发的锁竞争的开销给节省下来了,让性能会有很大的提高. 我们在实际开发中读操作是非常多的,使用读写锁对性能就有明显的提升了.
公平锁与非公平锁
公平锁就是遵循先来后到. 假设A锁先进行锁等待,B锁后进行锁等待.等到其他线程将锁释放后,就是A先获取到锁.
非公平锁就是不管你是先进行等待还是后进行等待.等到其他线程释放锁后.大家都可以竞争这把锁.系统的调度是随机的,下一个唤醒谁都不确定.
站在操作系统的角度上来看待锁,就是不公平锁.因为操作系统调度线程是随机的.下一个将谁调度进来,唤醒谁也不确定.我们Java中的synchronized也是不公平锁.而公平锁就需要引入额外的数据结构.
可重入锁和不可重入锁
可重入锁就是对于一个锁对象来说, 同一个线程可以多次获取到这把锁,不会发生死锁.
不可重入锁就是对于一个锁对象来锁,同一个线程不可以多次获取到这个把锁,不然会发生死锁.
我们的synchronized就是可重入锁,它的内部有两个重要的属性, 一个是用来记录当前持有锁的线程.一个是计数器,一次加锁就+1,一次解锁就-1.当变成0就算彻底解锁成功.
synchronized锁与操作系统自带锁对比
synchronized是:
乐观锁/悲观锁自适应
轻量/重量锁自适应
自旋/挂起等待锁自适应
不公平锁
普通互斥锁
系统自带锁是:
悲观锁
重量级锁
挂起等待锁
不公平锁
普通互斥锁
synchronized的优化策略
锁的升级
synchronized内部会有一个升级阶段.当一个线程执行到synchronized这里时,所对象还没被加锁的时候,就会经历: 1.偏向锁阶段 -> 2.轻量级锁阶段 -> 3.重量级锁阶段
偏向锁阶段(没有其他锁来竞争)
就是在这个需要加锁的线程上做一个轻量的标记,并不会真正的去进行加锁.只有等到其他的线程也要对这个锁进行加锁时.它才会在其他线程获取到锁之前将锁获取过来.这样就从偏向锁升级到了轻量级锁.
这里的核心思想就是懒汉模式.不能加锁就不加锁,迫不得已的情况下再进行加锁.这样就可以在没有其他线程需要加锁的情况下省去加锁的开销.
轻量级锁阶段(有锁竞争,但是不多)
这里的轻量级锁实现的就是自旋锁,这里的优点就是可以很快获取到锁,但是会更消耗CPU资源. synchronized内部会记录当前有多少个线程在竞争这把锁,当超过一个量后,轻量级锁就会升级到重量级锁.
重量级锁阶段(有锁竞争,且很多)
这里的重量级锁实现的就是挂起等待锁,遇到锁冲突后就直接挂起等待,也就是调度出CPU,当释放锁后,让系统随机唤醒一个来获取锁.
锁消除
这里也是编译器优化的一种方式.当发现这个代码不需要加锁时,就会将锁消除掉.
锁粗化
锁粗化就是将多个细粒度的锁合并成一个粗粒度的锁. 这里我们可以理解完synchronized{}里的代码越少粒度就越细.大多数情况下是希望粒度越细越容易并发执行代码,但有的情况下还是希望粒度粗一点.
比如:这里的加锁操作太多就会导致锁竞争的开销过大,每次加锁可能都会堵塞.这时将细粒度的锁变成粗粒度的锁就节省了锁堵塞的开销,也会提高效率.
相关面试题
你是什么理解乐观锁和悲观锁的,具体怎么实现?
乐观锁就是在加锁前认为发生锁冲突的几率不大,在加锁的时候做的工作就不多,就不会真的加锁,而是直接尝试访问数据.在访问数据的同时便辨别当前数据是不是出现访问异常.
悲观锁就是在加锁前认为发生锁冲突的概率比较大,在进行加锁的时候做的工作就比较多,每次访问变量前都会去真正的进行加锁.
悲观锁的实现就是先加锁,获取到锁再来操作数据,获取不到锁就等待.乐观锁的实现可以引入一个版本号,借助版本号识别出当前的数据访问是不是有冲突.
介绍一下读写锁
读写锁就是把读操作和写操作分别进行加锁. 读锁与读锁之间不会发生堵塞. 读锁与写锁之间会发生堵塞.写锁和写锁之间也会发生堵塞. 这就是一个线程读的时候,另一个线程只能读不能写.而一个线程写的时候,另一个线程不能读也不能写. 一般使用读写锁的都是在读非常频繁,但写不频繁的场景下.
synchronized是可重入锁吗
synchronized是可重入锁.可重入锁是指在一个线程内,可以对同一个锁进行多次加锁,不会产生死锁.而synchronized可以重入是因为它内部有两个重要的属性,一个是记录持有这个锁的线程身份,另一个是计数器(记录加锁的次数). 如果发现当前加锁的线程就是持有锁的线程,计数器就会++.
什么是自旋锁,为什么要使用自旋锁呢,缺点是什么?
自旋锁就是当一个线程发生锁竞争后,它不会挂起等待,而是通过while来不断循环来尝试再次获取到锁.一但其他线程释放锁后它就可以立刻获取到锁.
因为使用自旋锁在第一次获取失败后第二次获取尝试会很快来到,这样就可以快速获取到锁.
优点就是获取锁的速度快,更高效,在锁持有时间比较短且锁竞争小的场景下非常有用,却点就是如果锁竞争多且锁持有时间长的场景下就没有用了且非常浪费CPU资源,因为它一直在不断循环,做的都是无用功.