我相信初学java或者是没有深入理解过java多线程的同学,在后端学习的过程中一定也和我一样被java的这锁,那锁啥的折腾的够呛,没关系,你接着往下看,看完这篇文章我相信你对重量级锁、自旋锁、轻量级锁、偏向锁、悲观、乐观锁等的概念会有较为深刻的理解的。
这篇文章我就给大家讲一下这些锁究竟是啥,他们的由来,他们之间有啥关系,有啥区别。(敲黑板~~,重点来了)
学过多线程的同学肯定知道**锁这个东西,那么究竟程序为何要加锁呢?总结起来就一句话:因为多线程情况下线程之间的资源竞争。也就是说,如果程序自始至终都是单线程运行的,那就不会有锁机制也用不到,因为不会存在竞争的情况,但是现实往往是残酷的,在实际情况中这种情况是不存在的。
我们拿一个例子来说明多线程情况下的不安全性:
你在图书馆借了一本村上春树的《解忧杂货店》坐在位子上安静的看着,突然你室友打电话说辅导员下寝了,不在寝室的要被请去和茶,于是你只能在你正在看的那一页这了一下书角。然后放回了书架。等你从其实回来拿到这本书打开的时候,卧槽!!!怎么回事?好多页都折角了?我记得不是只有我看的那一页折了一下吗?
没错就是这样,因为你在离开的时间段里你不能保证别的同学不会阅读这本书,别的同学不会在书里面做标记。
如何解决?
很简单!把书拿回去,看完后还回来。这样别人就不会混乱你的标记,比人想看也只有等你还回来之后了。你拿回去看完之后还回来这就是一个加锁——执行——锁的释放过程。nice,这不就解决了多线程下资源不安全性的问题了吗。
咳咳,前言有点久了,快脱离主题了,停下停下!进入主题
我们知道,我们要进入一个同步、线程安全的方法时,是需要先获得这个方法的锁的,退出这个方法时,则会释放锁。如果获取不到这个锁的话,意味着有别的线程在执行这个方法,这时我们就会马上进入阻塞的状态,等待那个持有锁的线程释放锁,然后再把我们从阻塞的状态唤醒,我们再去获取这个方法的锁。
这种获取不到锁就马上进入阻塞状态的锁,我们称之为重量级锁。
我们知道,线程从运行态进入阻塞态这个过程,是非常耗时的,因为不仅需要保存线程此时的执行状态,上下文等数据,还涉及到用户态到内核态的转换。当然,把线程从阻塞态唤醒也是一样,也是非常消耗时间的。
刚才我说线程拿不到锁,就会马上进入阻塞状态,然而现实是,它虽然这一刻拿不到锁,可能在下 0.0001 秒,就有其他线程把这个锁释放了。如果它慢0.0001秒来拿这个锁的话,可能就可以顺利拿到了,不需要经历阻塞/唤醒这个花时间的过程了。
然而重量级锁就是这么坑,它就是不肯等待一下,一拿不到就是要马上进入阻塞状态。为了解决这个问题,我们引入了另外一种愿意等待一段时间的锁 — 自旋锁。
自旋锁就是,如果此时拿不到锁,它不马上进入阻塞状态,而是等待一段时间,看看这段时间有没其他人把这锁给释放了。怎么等呢?这个就类似于线程在那里做空循环,如果循环一定的次数还拿不到锁,那么它才会进入阻塞的状态。
至于是循环等待几次,这个是可以人为指定一个数字的。
上面我们说的自旋锁,每个线程循环等待的次数都是一样的,例如我设置为 100次的话,那么线程在空循环 100 次之后还没拿到锁,就会进入阻塞状态了。
而自适应自旋锁就牛逼了,它不需要我们人为指定循环几次,它自己本身会进行判断要循环几次,而且每个线程可能循环的次数也是不一样的。而之所以这样做,主要是我们觉得,如果一个线程在不久前拿到过这个锁,或者它之前经常拿到过这个锁,那么我们认为它再次拿到锁的几率非常大,所以循环的次数会多一些。
而如果有些线程从来就没有拿到过这个锁,或者说,平时很少拿到,那么我们认为,它再次拿到的概率是比较小的,所以我们就让它循环的次数少一些。因为你在那里做空循环是很消耗 CPU 的。
所以这种能够根据线程最近获得锁的状态来调整循环次数的自旋锁,我们称之为自适应自旋锁。
上面我们介绍的三种锁:重量级、自旋锁和自适应自旋锁,他们都有一个特点,就是进入一个方法的时候,就会加上锁,退出一个方法的时候,也就释放对应的锁。
之所以要加锁,是因为他们害怕自己在这个方法执行的时候,被别人偷偷进来了,所以只能加锁,防止其他线程进来。这就相当于,每次离开自己的房间,都要锁上门,人回来了再把锁解开。
这实在是太麻烦了,如果根本就没有线程来和他们竞争锁,那他们不是白白上锁了?要知道,加锁这个过程是需要操作系统这个大佬来帮忙的,是很消耗时间的,。为了解决这种动不动就加锁带来的开销,轻量级锁出现了。
轻量级锁认为,当你在方法里面执行的时候,其实是很少刚好有人也来执行这个方法的,所以,当我们进入一个方法的时候根本就不用加锁,我们只需要做一个标记就可以了,也就是说,我们可以用一个变量来记录此时该方法是否有人在执行。也就是说,如果这个方法没人在执行,当我们进入这个方法的时候,采用CAS机制,把这个方法的状态标记为已经有人在执行,退出这个方法时,在把这个状态改为了没有人在执行了。
显然,比起加锁操作,这个采用CAS来改变状态的操作,花销就小多了。
然而可能会说,没人来竞争的这种想法,那是你说的而已,那如果万一有人来竞争说呢?也就是说,当一个线程来执行一个方法的时候,方法里面已经有人在执行了。
如果真的遇到了竞争,我们就会认为轻量级锁已经不适合了,我们就会把轻量级锁升级为重量级锁了。
所以轻量级锁适合用在那种,很少出现多个线程竞争一个锁的情况,也就是说,适合那种多个线程总是错开时间来获取锁的情况。
偏向锁就更加牛逼了,我们已经觉得轻量级锁已经够轻,然而偏向锁更加省事,偏向锁认为,你轻量级锁每次进入一个方法都需要用CAS来改变状态,退出也需要改变,多麻烦。
偏向锁认为,其实对于一个方法,是很少有两个线程来执行的,搞来搞去,其实也就一个线程在执行这个方法而已,相当于单线程的情况,居然是单线程,那就没必要加锁了。
不过毕竟实际情况的多线程,单线程只是自己认为的而已了,所以呢,偏向锁进入一个方法的时候是这样处理的:如果这个方法没有人进来过,那么一个线程首次进入这个方法的时候,会采用CAS机制,把这个方法标记为有人在执行了,和轻量级锁加锁有点类似,并且也会把该线程的 ID 也记录进去,相当于记录了哪个线程在执行。
然后,但这个线程退出这个方法的时候,它不会改变这个方法的状态,而是直接退出来,懒的去改,因为它认为除了自己这个线程之外,其他线程并不会来执行这个方法。
然后当这个线程想要再次进入这个方法的时候,会判断一下这个方法的状态,如果这个方法已经被标记为有人在执行了,并且线程的ID是自己,那么它就直接进入这个方法执行,啥也不用做
你看,多方便,第一次进入需要CAS机制来设置,以后进出就啥也不用干了,直接进入退出。
然而,现实总是残酷的,毕竟实际情况还是多线程,所以万一有其他线程来进入这个方法呢?如果真的出现这种情况,其他线程一看这个方法的ID不是自己,这个时候说明,至少有两个线程要来执行这个方法论,这意味着偏向锁已经不适用了,这个时候就会从偏向锁升级为轻量级锁。
所以呢,偏向锁适用于那种,始终只有一个线程在执行一个方法的情况哦。
最开始我们说的三种锁,重量级锁、自旋锁和自适应自旋锁,进入方法之前,就一定要先加一个锁,这种我们为称之为悲观锁。悲观锁总认为,如果不事先加锁的话,就会出事,这种想法确实悲观了点,这估计就是悲观锁的来源了。
而乐观锁却相反,认为不加锁也没事,我们可以先不加锁,如果出现了冲突,我们在想办法解决,例如 CAS 机制,上面说的轻量级锁,就是乐观锁的。不会马上加锁,而是等待真的出现了冲突,在想办法解决。
到这里也大致写完了,简单介绍普及了一下,重点的大家要理解他们的由来,原理。每一种锁都有他们的应用以及各自的优缺点。
需要你牢记的东西: