![bbec9672f75541c776a060ec92214243.png](https://img-blog.csdnimg.cn/img_convert/bbec9672f75541c776a060ec92214243.png)
ReentrantLock和synchronized的区别
开门见山,先说结论,在1.6之前的synchronized是一个重量级锁,这个锁性能属实拉跨,因为重量锁不管怎么样都要调用CPU内核态,在内核态和用户态中切换是需要耗费大量的性能的。
![d98c8542e1187078eedb666b60160a82.png](https://img-blog.csdnimg.cn/img_convert/d98c8542e1187078eedb666b60160a82.png)
所以Doug Lea为了解决这个问题,写出了ReentrantLock这个类,试图利用CAS+AQS尽量在用户态把问题解决,用过的程序员纷纷直呼真香,sun公司就看不下去了啊,凭啥我写的亲生儿子不如你这个外来的,于是在1.8之后借鉴ReentrantLock思想之后在内存模型中优化了synchronized,使其也拥有锁膨胀、锁消除、锁粗化、自旋锁等特性。
并发与并行
因为最近经常有小伙伴问,你这个线程又不是同一时间运行的,怎么叫并发呢?所以在说到锁这个概念,得先科普一下并发与并行。
你在吃饭中,电话打进来,你吃完饭再接电话,这叫不支持并发与并行;
你再吃饭中,电话打进来,你放下筷子聊完电话再吃饭,这叫并发;
你再吃饭中,电话打进来,你边吃饭边聊电话,这叫并行。
所以说,如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个并发系统。如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。只要把线程看出一个动作就能明白了。
应用场景
在绝大多数场景下可以选择synchronized,但synchronized也是有缺陷的,比如锁退化、条件锁、公平锁,读写锁等很难用synchronized来实现,这种需要考虑灵活性的情况下选择ReentrantLock会更好。
ReentrantLock原理
ReentrantLock是基于Lock之上对其进行补充完善了一些特性的一个类。
- 基本锁的特性
1、加锁
2、解锁
- ReentrantLock的补充特性
1、可重入
2、公平与非公平锁
让我们来看下ReentrantLock是如何实现这些特性的。
1、AQS
在JDK文档中这样描述AbstractQueuedSynchronizer:
Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues
大意是提供了一个用于实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器的框架,而ReentrantLock则是基于AQS实现的可重入锁。
![60ffee7e90a3d5da6b599ab03452791d.png](https://img-blog.csdnimg.cn/img_convert/60ffee7e90a3d5da6b599ab03452791d.png)
在图中可以看到ReentrantLock中有一个内部类叫做Sync,它是用来实现这个锁的同步控制功能的基础类,这个类继承了以AQS类为基础实现了各种特性。
2、可重入锁
ReentrantLock和synchronized都是可重入锁,那什么是可重入锁呢,让我们看如下代码:
public class Test{
Lock lock = new Lock();
public void methodA(){
lock.lock();
methodB();
lock.unlock();
}
public void methodB(){
lock.lock();
...........;
lock.unlock();
}
}
当A方法获取lock锁去锁住一段需要做原子性操作的B方法时,如果这段B方法又需要锁去做原子性操作,那么A方法就必定要与B方法出现死锁,说人话就是:我要拿到锁才能执行B方法的同步代码块,但是A方法没执行完我又拿不到锁。以上,说明这就个锁无法实现可重入,一个可重入的锁在遇到这种情况的时候会进行判断,如果来抢锁的还是持有锁的线程,则它依旧可以拿到锁。
而在ReentrantLock中是如何实现可重入特性的呢?
AQS类中在获取锁之前,会进行一次判断,其中一个判断条件就是如果有线程持有锁,且当前线程为持有锁的线程,则获取锁。
![3f5b3ff5379ee108de54139d313113a4.png](https://img-blog.csdnimg.cn/img_convert/3f5b3ff5379ee108de54139d313113a4.png)
3、公平锁与非公平锁
大家想象一个场景,在一个售票窗口购买火车票的时候大家都不排队,每一个人买完火车票大家就一拥而上去抢着买票,而你每次都没抢到,甚至比你后来的人比你还先买到票了,你会不会非常的生气感觉非常不公平。上面这个例子抽象一下就是非公平锁的体现,把票当成需要争抢的资源,一个人比作一个线程,一个窗口比作一个锁,即每次获取锁的线程是随机的,这样是不公平的,synchronized就是这样的非公平锁。
而在ReentrantLock中,则在构造方法中提供了公平锁和不公平锁两种实现。
![9e568d1c5e9b3cb3919f364772dd9cb8.png](https://img-blog.csdnimg.cn/img_convert/9e568d1c5e9b3cb3919f364772dd9cb8.png)
公平锁也就是在售票窗口前放一个安保人员,强制大家排队(AQS),分个先来后到,这样就是公平的了。在接下来讲解加锁/解锁的流程使用就是的ReentrantLock实现的公平锁来举例。
4、加锁流程
![74e49407aa977785b5ac79a5b962f753.png](https://img-blog.csdnimg.cn/img_convert/74e49407aa977785b5ac79a5b962f753.png)
在阅读源码之前首先要了解几个参数:
- state:持有锁的状态,没人持有则为0,有人持有则为1,大于1则为该锁重入次数,例如state=3,说明该锁被当前持有锁的线程重入两次。
- Node.waitStatus:当前等待线程的状态,为-1则是沉睡状态,0为唤醒状态,大于0则是线程取消,线程死亡等状态。
- compareAndSet开头的方法:通过自旋方法来设置参数,例如compareAndSetState,通过自旋来设置State参数
首先让我们看下加锁的核心代码如下:
public final void acquire(int arg) {
//先尝试获得锁,如果没有获得则继续执行入队代码。具体代码看tryAcquire方法
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//如果在入队过程中被其他线程打断则把当前线程打断,具体代码看acquireQueued方法
selfInterrupt();
}
tryAcquire方法:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//判断当前锁是否被持有,没被持有则为0
if (c == 0) {
//如果不需要入队且CAS设置state值成功了则获取锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果持有锁的线程是当前线程则实现可重入
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
acquireQueued方法:
final
值得注意的是,AQS队列头Node永远为空,因为头节点代表的持有锁的线程的状态,如果现在线程二已经拿到锁了,那么该线程所在的node就不用排队了,node对Thread的引用也就没有意义了,所以队列的head里面的Thread永远为null。其次就是自己的状态必须下一个入队的线程来设置,因为自己是无法判断自己的状态。
5、解锁流程
![4737b20cfbd0eee8338b65b4b047bef6.png](https://img-blog.csdnimg.cn/img_convert/4737b20cfbd0eee8338b65b4b047bef6.png)
解锁核心代码如下:
public
有两个点值得注意,为了实现可重入锁,解锁的时候也并不是全解锁,每次解锁将state-1,直到减成0才是成功解锁。还有就是在unparkSuccessor方法中考虑到线程安全的复杂情况,有时候不一定能拿到等待的第一个元素,可能出现线程被取消,线程死亡等情况,这时就不是从队首开始拿元素,而是从队尾开始拿。代码如下:
private void unparkSuccessor(Node node) {
/*
* 设置等待线程状态为0
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* 如果等待线程为空或者等待线程状态不为正常状态(0或1)
* 则说明该线程可能取消或者死亡
* 则从队尾开始获得沉睡线程
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//唤醒沉睡的等待线程
if (s != null)
LockSupport.unpark(s.thread);
}
6、打断
![786dd435db67f1550afc7a048dd7f62e.png](https://img-blog.csdnimg.cn/img_convert/786dd435db67f1550afc7a048dd7f62e.png)
直接使用lock也可以打断,但是使用这个方法可以执行打断策略,在catch里写入打断策略。
7、读写锁和条件锁
JUC中提供了内置的读写锁类ReentrantReadWriteLock,所谓读写锁也就是读和写使用两把不同的锁,读的锁较写的锁更为轻量。
想实现条件锁的话ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。
总结
ReentrantLock提供了对死锁,线程饥饿、线程中断等问题的处理方式,并提供多种读写锁和条件锁的实现。使用起来较synchronized更为灵活,使用的好的话性能也会有所提升。