公平锁与非公平锁的核心原理
Lock锁接口方法
前面了解到了synchronized锁,也知道了synchronized锁是一种JVM提供内置锁,但synchronized有一些缺点:比如不支持响应中断,不支持超时,不支持以非阻塞的方式获取锁等。而今天的主角Lock锁,需要我们手动获取锁和释放锁,里面有很多方式来获取锁,比如以阻塞方式获取锁,在指定时间内获取锁,非阻塞模式下抢占锁等,其方法源码如下(位于package java.util.concurrent.locks包下):
//Lock接口下的方法
public interface Lock {
//阻塞式抢占锁,如果抢到锁,则向下执行程序;抢占失败线程阻塞,直到释放锁才会进行抢占锁
void lock();
//可中断模式抢占锁,线程调用此方法能够中断线程
void lockInterruptibly() throws InterruptedException;
//非阻塞式尝试获取锁,调用此方法线程不会阻塞,抢到锁返回true,失败返回false
boolean tryLock();
//在指定时间内尝试获取锁,在指定时间内抢到锁成功返回true,失败返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁,线程执行完程序后,调用此方法来释放锁资源
void unlock();
//条件队列
Condition newCondition();
}
公平锁简介
公平锁,顾名思义,所有线程获取锁都是公平的。在多线程并发情况下,线程争抢锁时,首先会检查等待队列中是否有其他线程在等待。如果等待队列为空,没有线程在等待,那么当前线程会拿到锁资源;如果等待队列中有其他线程在等待,那么当前线程会排到等待队列的尾部,好比我们排队买东西一样。
ReentranLock的公平锁
ReentranLock类实现了Lock接口,重写了里面的方法,对于ReentranLock类中的公平锁,我们可以看到如下源码:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
在上述构造方法中,新建锁对象是否为公平锁,在于传入的参数是true还是false,在三目运算符中,如果传入参数为true,则会创建一个FairSync()对象赋值给sync,线程获取的锁是公平锁;如果为false则创建一个NonfairSync()对象,线程获取的锁是非公平锁。点入FairSync()方法,得到如下源码:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/*这里将acquire方法放于此
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//拿到当前锁对象的状态
int c = getState();
//如果没有线程获取到锁
if (c == 0) {
//首先会判断是否有前驱节点,如果没有就会调用CAS机制更新锁对象状态
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//设置当前线程拥有锁资源
setExclusiveOwnerThread(current);
//如果获取锁资源成功则返回true
return true;
}
}
//或者如果拿到锁的就是当前线程
else if (current == getExclusiveOwnerThread()) {
//将锁的状态+1,考虑到锁重入
int nextc = c + acquires;
if (nextc < 0)
//超过了最大锁的数量
throw new Error("Maximum lock count exceeded");
//如果锁数量没有溢出,设置当前锁状态
setState(nextc);
//如果成功获取锁资源则返回true
return true;
}
//如果前两条都不符合,则返回false
return false;
}
}
上述代码涉及到了hasQueuedPredecessors()方法,返回true则代表有有前驱节点,点入查看得到以下源码:
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
//h!=t,首节点不等于尾节点表示队列中有节点
return h != t &&
(
//s为头结点的下一个节点,返回false表示队列中还有第二个节点,或运算符后面表示,第二个线程不是当前线程
(s = h.next) == null || s.thread != Thread.currentThread()
);
}
因此做出总结,使用ReentranLock的公平锁,当线程调用ReentranLock类中的lock()方法时,会首先调用FairSync类中的lock()方法;然后FairSync类中的lock()方法会调用AQS类(AbstractQueuedSynchronizer)中的acquire(1)方法获取资源,前面的文章里也提到过,acquire()方法会调用tryAcquire()方法尝试获取锁,tryAcquire()方法里面没有具体的实现,tryAcquire()方法具体逻辑是由其子类实现的,因此调用的还是FairSync类中的方法。在AQS中的acquire()方法中如果尝试获取资源失败,会调用addWaiter()方法将当前线程封装为Node节点放到等待队列的尾部,并且调用AQS中的acquireQueued方法使线程在等待队列中排队。
ReentranLock的非公平锁
非公平锁就是所有抢占锁的线程都是不公平的,在我们日常生活中就相当于是插队现象,不过也与插队稍微不同。在多线程并发时,每个线程在抢占锁的过程中,都会先尝试获取锁,如果获取成功,则直接指向具体的业务逻辑;如果获取锁失败,则会像公平锁一样在等待队列队尾等待。
在非公平锁模式下,由于刚来的线程可以在队首位置进行一次插队,所以当插队成功时,后面的线程可能会出现长时间等待,无法获取锁资源产生饥饿现象。但是非公平锁性能比公平锁性能更好。
对于ReentranLock类中的非公平锁,实现方式有两种,一种是默认的构造方式,另一种是和上面的一样,源码如下:
//第一种方式,默认实现
public ReentrantLock() {
sync = new NonfairSync();
}
//第二种方式,传入false来创建非公平锁对象
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
对于创建的NonfairSync()类对象,其源码如下:
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() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
在上述代码中,当线程获取锁时,并没有直接将当前线程放入等待队列中,而是先尝试获取锁资源,如果获取锁成功,设置state标志位1成功,则直接将当前线程拿到锁,执行线程业务;如果获取锁资源失败,则调用AQS中的acquire方法获取资源,而acquire()方法会回调上面NonfairSyn类中的tryAcquire()方法,然后又会回调Sync类中的nonfairTryAcquire()方法(NonfairSync类继承了Sync类) ,点击nonfairTryAcquire(acquires)查看源码:
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;
}
由上诉代码,没有将线程放入到等待队列中,只是对锁的状态进行了判断,若标识为0,代表没有线程拿到锁,当前线程会使用CAS机制改变锁状态,并调用setExclusiveOwnerThread(current)方法让当前线程拿到锁。
因此综上所述,在使用ReentranLock中的非公平锁时,首先会调用lock()方法,,而ReentranLock类中lock()方法会调用NonfairSync类中的lock()方法,接着NonfairSync类中的lock()方法会调用AQS中的acquire()方法来获取锁资源,AQS中的acquire()方法又会回调NonfairSync类中tryAcquire()方法尝试获取资源,NonfairSync类中tryAcquire()方法会调用Sync类中的nonfairTryAcquire方法尝试非公平锁获取资源;获取失败的话,AQS中的acquire()方法会调用addWaiter()方法将当前线程封装成Node节点放入到等待队列的队尾,而后AQS中的acquire()方法会调用AQS中的acquireQueued()方法让线程在等待队列中排队。这就是非公平锁的整个流程。
- 点赞
- 收藏
- 关注作者
评论(0)