公平性锁和非公平锁:ReentrantLock源码解析

在Java中为了保证代码同步时的安全问题,通常会使用线程同步机制,前面讲过了使用重量级锁Synchronized来保证线程安全,虽然Java对Synchronized做了优化,性能上已经有了大的提升,但是仍然会推荐使用Lock,也就是今天介绍的另外一种保证线程安全的方式:可重入锁

使用

可重入锁(ReentrantLock)的使用比较简单,而且非常灵活
我们只需要new一个ReentrantLock对象,然后调用该对象的lock方法来加锁,调用unlock方法来解锁。

 ReentrantLock lock = new ReentrantLock();
 lock.lock();
 //需要同步的代码
 lock.unlock;

使用方法非常简单,从源码上看看是如何实现的
ReentrantLock分为公平性锁和非公平性锁,公平性锁就是所有的线程按照先来后到的顺序来获取锁,而非公平性锁是随机获取锁,是抢占式的,有非公平性锁的需求的原因是非公平性锁保证了效率,使那些优先级别高,重要的线程可以先执行。

  1. 先从构造函数看起
public ReentrantLock() {
        sync = new NonfairSync();
}

这是一个无参构造函数,默认调用了NonfairSync的构造,从字面意思上来看,可以知道实例化了一个非公平性锁给sync属性,所以ReentrantLock默认是非公平性锁。
但是我们也可以通过传参控制是否为公平性锁

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
}

这是一个有参构造函数,通过传入的布尔值来给sync赋值,如果是true就是公平性锁,如果是false就是非公平性锁,所以我们就可以通过传入一个布尔值来控制该锁是公平性锁还是非公平性锁。

下来我们再来说sync属性,我们可以看到sync属性可以接受NonfairSync类型,也可以接收FairSync类型,而sync属性类型是Sync,在ReentrantLock中有三个内部类分别是Sync,NonFairSync和FairSync,NonFairSync和FairSync都继承自Sync,而Sync继承了AbstractQueuedSynchronizer,也就是常说的AQS,Sync是他们两的父类,所以可以用来接收子类实例。
大致浏览一下下图了解一下大致结构
在这里插入图片描述

可重入锁ReentrantLock能实现公平性和非公平性,就依赖于Sync,刚才说了Sync继承了AbstractQueueSynchronizer,AQS为我们提供了一个队列和一些用来标记和逻辑处理的东西,从而实现非公平性锁和公平性锁。

非公平锁

先来看非公平性锁

final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

在这里需要注明的是state属性,state是AQS类提供的一个属性,用来记录当前锁的状态,如果state为0,则说明当前锁是处于空闲状态,而每次加锁,state都会加1,但是只有当state减为0时才表示当前锁被释放掉,每进行一次tryRelease state都会减一,但是可以想到这次tryRelease只是和最近的一次lock进行对应,有可能前面还有多次lock,所以没有真正释放掉锁,这样理解起来会比较容易

上面是NonFairSync的lock操作,我们可以看到首先会执行compareAndSetState操作,这就是一个CAS操作,去获取当前锁,如果获取成功,则把当前持有锁的线程设置为当前线程,如果不成功,执行acquire(1)操作,跟踪acquire方法

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

在这里插入图片描述
这个方法是AQS中的一个方法,这个方法首先会执行tryAcquire操作,而tryAcquire在子类NonFairSync中有实现,所以会先执行NonFairSync中的方法,下面是NonFairSync中的tryAcquire方法

protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
}

子类中的这个方法直接代理给了父类Sync的nonfairTryAcquire方法,下面看看父类的该方法是如何实现的

inal boolean nonfairTryAcquire(int acquires) {
        	//获取当前线程
            final Thread current = Thread.currentThread();
            int c = getState();
			//判断当前锁是否空闲
            if (c == 0) {
				//通过CAS获取锁,获取成功则将state置为1,并且将当前线程记录下来,直接返回
                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");
				//若没有到达上限,则直接更新state,直接返回
                setState(nextc);
                return true;
            }
			//当前获取锁的线程非当前线程
            return false;
        }
		//释放锁操作
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
			//获取锁的线程非当前线程,直接抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
				//当state减为0的情况下,才真正释放锁,将持有锁的线程置为null
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

回到nonfairTryAcquire,这时非公平性锁第二次去尝试性的获取锁,首先进行的操作是判断锁是否空闲,如果空闲直接获取锁成功,设置持有锁的线程为当前线程,修改state(+1),如果不是,继续去判断非空闲状态下是否是当前线程持有该锁,如果是,修改state(+1),
如果这两个条件都不满足,那么获取锁失败,返回false,往上追溯,回到AQS
在这里插入图片描述
再次回到AQS的acquire方法
tryAcquire如果是ture,也就是第二次获取锁成功,短路与不再执行,函数执行完毕,如果返回false,则短路与继续执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg),也就是以独占的方式将当前线程加入到等待队列中去,如果添加成功,当前线程自我中断,进入阻塞状态。

公平性锁

公平性锁的lock操作如下

final void lock() {
            acquire(1);
 }

公平性锁直接调用acquire操作,追踪这个方法,可以发现还是AQS中的这个方法,如下
在这里插入图片描述
这个方法继续去找tryAcquire,这回子类FairSync没有直接调用父类Sync的这个方法,而是自己实现了这个方法,如下:

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                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;
        }

在这里插入图片描述
我们可以看到这个逻辑和非公平性锁调用Sync中的方法逻辑非常相似,就是先去判断是否空闲,然后继续判断持有锁线程是否是当前线程,但是有一个非常关键的函数,也就是箭头所指的函数,hasQueuePredecessors这个方法,这个方法是实现公平性锁的关键。
下面看看这个方法的源码

public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
}

这个方法的意思去判断队列中是否没有数据或者队列中的第一个数据线程是否是当前线程,如果没有数据返回false,则!hasQueuedPredecessors()为true,继续获取锁,如果第一个数据线程等于当前线程,则!hasQueuedPredecessors()为true,继续获取锁。所以两个条件只要满足一个就可以继续去获取锁,如果不是第一个等待的线程,返回true,!hasQueuedPredecessors()为false,直接退出这个if判断,进入到下一个环节:判断是否为当前线程持有锁,后面的逻辑就和非公平性锁一样。

总结

公平性锁和非公平性锁的区别:

  1. 非公平性锁上去就会直接常识性的获取一次锁,而公平性锁则不会
  2. 非公平性锁在第一次获取锁失败的时候,第二次获取锁是直接通过CAS去获取锁,而公平性锁第一次获取锁就需要执行一步hasQueuedPredecessors()这个方法来判断等待队列中第一个等待的线程是否为当前线程,大白话说就是排队看当前线程是不是第一个,

猜你喜欢

转载自blog.csdn.net/weixin_42220532/article/details/89854696