前言
本文已参与「新人创作礼」活动,一起开启掘金创作之路。
提到同步首先想到的可能就是 Synchronized,但 Synchronized 不足够灵活,这个时候 ReentrantLock 的好处就显示出来了!对于 ReentrantLock 有很多的名称(包含且不限于上方标题),这个地方我们就不过多地描述他俩直接的优劣了,简单的对比一下,主要还是讲一下 ReentrantLock
优劣比较
知道都不喜欢看废话,就捡重点的直接进入主题,首先要讲的还是这两个的优劣对比(后边会根据每个关键词拓展,尽量做到描述清楚每一个关键点)
首先说一下 Synchronized:
1) 是独占锁,加锁和解锁过程都是自动的,不太灵活,但操作简便(直接在方法块上使用就可以)
2) 可重入,并且不需要担心最后锁释放的问题,因为第一点说了,加解锁都是自动的
3) 不可中断(同步等待:一个线程一直获取不到锁就会一直等待,直到获取到锁)
4) 无公平锁机制(线程是随机分配的)
而 ReentrantLock:
1) 也是独占锁,但加锁和解锁过程是手动的,比较灵活,不易操作(需要自己创建锁,关闭锁)
2) 也是可重入,但因为是手动加锁解锁,且次数需要是一样的,不然其他线程会无法获得锁
3) 可以相应中断的,可轮询的
4) 有公平锁机制(白话理解)谁等待的时间长谁就先执行,(官话)如果有另一个线程持有锁或者有其他线程在等待队列中等待这个锁,那么新发出的请求的线程将被放入到队列中
下边是我们测试的例子,至于什么是重入,什么是独占,什么是公平锁,会在后边一一补充
示例
创建锁,使用锁
先创建一个 ReentrantLock 对象(最后有完整的示例代码)
// 创建对象
final static ReentrantLock lock = new ReentrantLock();
在上边我们比较优劣的时候讲了一下说这个 ReentrantLock 的锁是手动开启释放的,所以这个地方我们创建了对象之后就要手动的去开启锁,我们写一个测试方法,来看一下具体是怎么使用的
附上一个完整的测试方法和运行结果
// 创建对象
final static ReentrantLock lock = new ReentrantLock();
// 使用 mian 方法测试
public static void main(String[] args) {
// ReentrantLock 测试示例
new Thread(ReentrantLockUtils::ReentrantLockTest,"ReentrantLockTest -- 线程1").start();
}
// 要执行的测试方法
public static void ReentrantLockTest(){
// 开启锁
lock.lock();
try {
// 获取线程名称
String tr1 = Thread.currentThread().getName();
log.info(tr1+"获取了锁");
// 线程暂停两秒(作用和 Thread.sleep() 一样,相对来说可读性更高了一些)
TimeUnit.SECONDS.sleep(2);
}catch (Exception e){
e.printStackTrace();
}finally {
String tr1 = Thread.currentThread().getName();
log.info(tr1+"释放了锁");
lock.unlock();
}
}
可重入
什么是可重入?他的作用是什么?可重入(也有叫重入锁的)就是,当一个持有锁的线程,在释放锁之前,被重复访问或者访问了此锁的其他方法,那么这个线程不需要进行抢占锁,只会被记录重入次数,重入锁的作用就是为了防止死锁现象
独占锁
独占锁顾名思义, 就是一个线程获得了锁,别的线程就不能获得锁,必须等锁释放了,才能可能获取到锁
公平锁
什么是公平锁?为啥说 ReentrantLock 自带公平锁机制呢?我们接着往下看!可以点开 ReentrantLock 的源码看下,我们创建的 ReentrantLock 对象完整的应该是这个样子的,ReentrantLock 默认使用的是非公平锁,如果不传参数,默认的是 false,然后看下边第二段 ReentrantLock(boolean fair),意思是传入 true 或者 false ,传入 true 就是开启公平锁机制
final static ReentrantLock lock = new ReentrantLock(false);
我们继续跟进去看下公平锁和非公平锁有什么区别,我们先看非公平锁源码,可以看到进入方法的时候会先进行一次 CAS 判断(是否需要修改状态,详情看下方注释所在位置),成功则直接返回线程,继续往下看,进入 tryAcquire 方法可以看到 return NonfairSync
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
// compareAndSetState:如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值
// 第一个参数:expect - 预期值 第二个参数:update - 新值
// 如果成功,则返回 true,返回 false 指示实际值与预期值不相等。
// 白话理解就是:修改 state 为 1 即代表加锁成功,设置当前 AQS 独占线程为当前线程
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 否则就加锁失败调用 acquire()
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
继续跟进去,也就是下方,直接获取到线程状态,如果状态等于 0 则直接返回成功(状态为 0 就代表当前锁还未被其他线程获取,可以直接获取锁,成功就设置当前线程为独占线程),如果不为 0 (这个地方就涉及到了重入锁)判断锁独占线程为当前线程,同步状态+1,设置同步状态结束
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
// 获取线程
final Thread current = Thread.currentThread();
// 获取状态
int c = getState();
// 表示还没有被其他线程获取
if (c == 0) {
// 这个地方也是和公平锁有出入的地方
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
然后在来看一下公平锁的源码,可以看到我们下方注释的地方多了一层对线程的判断,也就是说,只有在当前队列中没有节点的时候,才会去修改 CAS 变量,从而实现了公平锁的
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 这个相比非公平锁的源码多了一层判断
// hasQueuedPredecessors:用来判断线程需不需要排队
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;
}
}
二则的实现区别就在于:公平锁 是先判断当前队列中有没有存在的队列节点,如果没有才会去进行修改状态, 非公平锁 是先进行一次 CAS 判断,只有在不成功才会到 acquire 方法中并且在 nonfairTryAcquire 方法中,并没有添加 hasQueuedPredecessors 此参数,而是直接使用 CAS 尝试获取锁
公平锁和非公平锁的区别就在于(借鉴于这个老哥的总结:理解ReentrantLock的公平锁和非公平锁):
1.非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
2.非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。(白话来讲:理解方式就是公平锁,排着队一个一个的来,谁等的时间最长谁就先执行。非公平锁就是不排队,谁先抢到谁就执行)