在java这条不归路上,随着我们经历的项目越来越多,花里胡哨的东西也会见了不少,总会有些许小膨胀。抽颗烟静下心来想一想(我不抽),也只是会用而已,要是想彻底掌握,还得静下心来去学习。征服自己不算什么本事,你得去让面试官闭嘴(开玩笑~)。
在多个线程去访问公共资源的时候,我们知道很容易引发数据错乱和数据安全等问题。为了避免这种问题,java提供了synchronized和lock这俩种锁。我们先看看这俩种锁到底有什么不同吧!
synchronized是java底层支持的,而lock所在的concurrent包则是jdk实现的。synchronized是一个关键字,lock是一个接口(我们这里主要用到的是ReentrantLock这个实现类)。再去看一下它们分别是怎么去释放锁的:
synchronized释放锁的情况有俩种:
1、获取到锁的线程执行完代码块主动去释放锁
2、线程执行发生异常,此时JVM会让线程自动释放锁。
lock释放锁就只有一种情况了,就是调用unlock()方法(这也是和synchronized区别比较大的一点,synchronized不会主动去释放锁,而lock必须主动的去调用unlock()方法,否则会造成死锁现象)。因此如果这个获取锁的线程由于要等待IO或者其他原因被阻塞了,但是又不会主动去释放锁,其他线程便只能等待,效率方面就会受到较大的影响。
我们先来看看lock这个接口都有哪些实现:
接下来我们去看一看lock这个接口里边到底有些什么东西:
下面就是对接口中的方法进行介绍:
lock():这个就是获取锁的方法了,若锁被其他线程获取,则等待(阻塞)。
lockInterruptibly():可中断地获取锁,该方法会响应中断,在锁的获取过程中可以中断当前线程。这个就算是lock的一个招牌了,因为在使用synchronized时,一个线程在等待获取锁的过程中是不能中断的;而在使用lockInterruptibly()方法去获取锁的时候,如果获取不到,是可以在锁的获取过程中响应中断的。
tryLock():它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit):方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
unlock():这个就是释放锁了。
newCondition():这个太明显了,主要是去获取一个Condition实例。因为Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知。
上边就是对lock这个接口一些大概的介绍了,今天我们主要去看看ReentrantLock。
ReentrantLock是一种重入锁,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁,该线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞。该锁还提供了获取锁时的公平和非公平性选择。
先去看一下ReentrantLock的构造方法:
我们就以非公平锁为例,进去看一看:
没拿到锁怎么办啊,我们看看它是怎么处理的:
这个方法是AbstractQueuedSynchronizer这个类里边的,先大概的知道一下它都干了什么:
tryAcquire():会尝试再次通过CAS获取一次锁。
addWaiter():从表面意思来看就是让他去排队去了(这个我们后边会再做解释)
acquireQueued():进入等待状态,等待其他线程释放锁之后唤醒自己,直到获取到锁之后返回。
selfInterrupt():如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt()。
到了这里,有必要去看一下AbstractQueuedSynchronizer这个东西了,先用我们的散装英语猜一下就知道个大概了,这是个抽象的同步队列,我们去看一下这个队列到底是个什么东西:
说一下这里的state,其实就是标识是否拿到锁了,state为0的时候没拿到,state为1表示拿到锁了。
有头有尾的,从命名可能就会有一些想法了,再去看看这个Node:
好了,真相大白了,双向链表都被玩出花来了。
我们回过头来,再去看看没有拿到锁acqiire()会执行的那几个操作:
1、首先是再次去尝试获取锁 --- tryAcquire:
2、然后是将节点放入双向链表中 --- addWaiter():
这里提一下CAS自旋是个什么鬼?
说一下上边的enq()方法。进入循环的第一次,拿到链表的尾节点,然后去判断,尾节点是不是null,是null的话就说明这个链表就根本不存在,于是创建一个新的链表,让头结点指向尾节点;然后进入第二次循环,t肯定不为null了,就进入else了,将当前节点的前置节点指向尾节点,然后再进行CAS操作,如果t真的和内存中的尾节点是一样的,那么node就作为尾节点放入链表中,然后返回;如果CAS失败了的话(被其他线程改了),就再循环,直到CAS成功。
3、 等待被唤醒 --- acquireQueued():
锁上好了,去看看怎么开锁!
和获取锁一样,还是一个一个的点进去看:
再去看看它具体怎么释放锁的:
获取锁和解锁就这些东西了,解锁相对来说没那么复杂。画个流程图展示一下上锁的整个过程吧!
经过这些分析之后,我们知道了lock是否获取到锁是通过修改AQS(AbstractQueuedSynchronizer)中的state属性来实现的,如果是1就是拿到锁了,是0就是没拿到,>1就是lock作为一个重入锁的体现了,没有获取到锁的线程会放到同步队列的尾部,它其实是一个双向链表,然后通过自旋操作,不断的去唤醒线程,再去通过CAS获取锁,大概就是这个过程了。
好多都是在这里学的,文章本身写的就很清晰,我这里边学边总结,做个记录。大家有什么不明白的可以来这看看一文带你理解Java中Lock的实现原理-云栖社区-阿里云。
多学习,多总结,多记录。是学习也是蜕变,不驰于空想,不骛于虚声 !