synchronized和ReentrantLock有什么区别?这是一道经常会被问到的八股题,网上也已经有了很明确的答案,简单总结下:
- synchronized由JVM的Monitor对象实现,ReentrantLock由AQS和CAS实现
- synchronized的加锁和释放都是由JVM自动实现的,而ReentrantLock则需要手动的lock()和unlock()
- 相比较于synchronized的notify()和wait(),ReentrantLock则更为灵活,可以基于Condition条件性的选择唤醒
- synchronized可以用来修饰方法,锁代码块,ReentrantLoLock则不可以,它只能是基于锁对象,比如先上锁,然后try{}finally{unlock()}
- ReentrantLoLock还可以超时获取,tryLock(long timeout, TimeUnit unit)
- ReentrantLoLock还可以选择公平锁和非公平锁。
总的来说,后者的方式更为灵活,但是在效率上来说,由于synchronized也引入了锁升级的模式后,二者是差不多的。但是,AQS到底是什么?公平锁和非公平锁的实现方式到底是怎么样的?。。。 为了能够进一步了解该类,我对部分源码进行了进一步的阅读。
类成员概览
先看一下内部的构造,可以看到,内部成员属性,只有一个Sync,而且这是一个内部的抽象类,那么也就是说所有的操作都是围绕着它进行的。该类继承了AQS,并且还有两个子类,NonfairSync和FairSync,也就是说,这两个类分别实现了公平锁和非公平锁。在构造函数中,可以很清楚的看到,默认的实现用的就是非公平锁的方式,如果传入了fiar入参,则可以选择用公平锁来实现。这里如果直接去看AQS的内部实现,可能会很懵,但是没关系,先不看,大致先将其视为一个队列,里面维护者各个需要获取锁的线程。我们可以先从常用的方法作为入手点,一步一步来看。起码,现在我们已经知道了,构造函数做了什么事情,就是将成员Sync实例化。
lock方法实现
我们最常用的方法,肯定是lock了,那么接下来,我们就首先来看看这个方法到底怎么实现的,怎么样才算是获取到锁了。
什么,就这???没错,就这。这也是我之前没有推荐大家直接到AQS里面去看源码的原因,随着继承、多态这些概念的叠加,一层套一层,你很容易跑偏,不知道自己在看什么。所以我推荐以常用的方法为入手点,将困难逐步瓦解。那现在,我们接着看看Sync中,是怎么实现的这个方法。
可以看到,Sync中并没有对该方法做具体的实现,它只是一个抽象方法。而真正实现了这个方法的,其实是NonFairSync和FairSync,现在我们先来看一下NonFairSync中的实现方式:
可以看到NonFairSync中实际上,只有两个方法。一个是lock,一个是tryAcquire。compareAndSetState听名字其实就可以理解,它做的事情就是通过CAS的方式来设置state,希望在其为0的状态下,将其改为1,如果设置成功,就说明上锁成功,那么当前线程就获取到了该锁对象。
既然都是“我”的锁了,总得留下点什么标记来证明一下吧,让后面再来获取的线程知道,这里已经被别人“插旗”了,老老实实去排队吧。setExclusiveOwnerThread(Thread.currentThread());就是在做这样一件事情,将当前锁的独占模式下的拥有者设置为当前线程。
可是在没获取成功的情况下,执行的acquire又是在干嘛呢?听名字,也像是一个获取锁的方法,这里先放一下,我们先把compareAndSetState搞搞清楚。
compareAndSetState其实是AQS这个抽象类中已经实现的方法,毕竟ReentrantLock中只有Sync,而我们现在看的其子类NonFiarSync中貌似也没有定义这state成员,那么只能是其父类AQS中定义的了。果不其然,AQS中确实有这么一个对象,除此之外,还有内部类Node,以及Node的头指针,尾指针。没错,这个数据结构就是用来记录因获取不到锁而等待的线程的,不过这里可以先不展开。
径直来看AQS中的compareAndSetState方法,调用了unsafe.compareAndSwapInt这样一个方法,Unsafe是位于sun.misc包下的一个类,提供了一些底层操作的API,比如直接操作内存、线程调度、对象实例化等。这个类通常不推荐普通开发者使用,因为它可以绕过Java的安全机制,容易引发错误,而且不同JVM实现可能会有差异,导致兼容性问题。
说实话,看到这个类,其实可以不用再去揪着不放了,只要去看一下相关解释就好了,毕竟再往底层不太方便查看了。我们只要知道,它也是在做一个CAS操作就好。但是,我们可以通过传入的参数来看看它在改些什么东西。unsafe.compareAndSwapInt(this, stateOffset, expect, update),很明显,this就是对象本身,expect=0,update=1,也就是说希望通过CAS将本对象的state由0改为1。至于stateOffset。。。
可以看到,这里的getDeclaredField是在获取类对象中的“state”变量的偏移量,以便unsafe在对state中修改时得到变量的内存地址。stateoffset被声明为静态变量,也就是说明,所有的AQS对象的state的偏移量都是一样的。在复述一遍,unsafe.compareAndSwapInt(this, stateOffset, expect, update)就是在试图通过CAS将本对象,在对象所占内存的起始地址的,偏移量为stateOffset的state,由0变为1。这下算是说明白了。
再回过头来看setExclusiveOwnerThread(Thread.currentThread())方法,他做的事情更简单,就是将AQS通过继承父类得到的exclusiveOwnerThread修改为当前获取锁的线程,
unlock()
再来看看unlock方法
其实是在调用AQS中的release方法:
然而在点进去之后发现,竟然只抛出一个异常,什么都没干。
其实这里应该是为了做一个提醒,提醒你该方法应该要在被重写之后才使用,还记得前面的ReentrantLock的两种内部类(Sync,FairSync和NonFairSync)吗,一定是他们将方法进行了重写。 回到lock中直接搜索,果不其然。。。
/**
* 尝试释放独占锁(由持有锁的线程调用)
* @param releases 需要释放的锁次数(通常为1,可重入锁可能>1)
* @return true表示锁已完全释放,false表示仍有重入未释放
*/
protected final boolean tryRelease(int releases) {
// 1. 计算释放后的锁状态:当前状态 - 释放次数
int c = getState() - releases;
// 2. 安全检查:当前线程必须是锁的持有者
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException("非持有线程禁止释放锁");
// 3. 标记锁是否完全释放(初始为false)
boolean free = false;
// 4. 检查是否完全释放(状态归零)
if (c == 0) {
free = true; // 标记完全释放
setExclusiveOwnerThread(null); // 清除持有线程
}
// 5. 更新锁状态(即使未完全释放也要更新重入计数)
setState(c);
// 6. 返回释放结果
return free;
}
仅仅释放掉锁还是不够的,不要忘了还在队列里等待的线程兄弟们啊。。。
此处的行为就是在判断:如果队列中头指针不空,其等待状态!=0,进行唤醒其它线程的操作。等待状态的含义为:
| 取值 | 常量名 | 值 | 含义 |
|---|---|---|---|
1 | CANCELLED | 1 | 节点关联的线程已取消等待(如超时或中断) |
-1 | SIGNAL | -1 | 当前节点释放锁/取消时必须唤醒后继节点(表示后继节点需要被通知) |
-2 | CONDITION | -2 | 节点处于条件队列(ConditionQueue),不用于同步队列 |
-3 | PROPAGATE | -3 | 共享模式下,状态需要传播给后续节点(用于高效释放共享资源) |
0 | 默认状态 | 0 | 新建节点时的初始状态,或表示不需要唤醒后继节点 |
也就是说,头节点线程不为0的情况下,去唤醒一个线程。
首先的CAS操作目的在于,将节点的状态置为0,也就是说此节点已经释放锁了,后面不需要在唤醒其之后的线程了。这里大家可能会觉得奇怪,上层方法传递过来的不是头节点吗?你把头节点设置成0状态,头节点之后的都不唤醒了?这里我去了解之后才知道,之前说lock方法的时候漏了一点内容:
这里的acquireQueued方法其实很重要,(下班了。。。。晚会再写)