Java并发AQS应用之独占锁ReentrantLock

前几篇文章我对同步队列框架AQS做了详细的讲解,接下来我将介绍在JDK中它使用的地方,AQS包含两种模式:独占资源模式和共享资源模式,本篇文章将实例讲解独占资源模式的用法,ReentrantLock内部就是实现AQS框架的独占模式,它和synchronized一样是可重入的独占锁。

一、举例说明ReentrantLock的用法

如果有一个共享变量count,有10个线程对它进行累加,每一个线程累加1000次,这段代码怎样设计呢?

我们有很多种办法,可以利用synchronized关键字,也可以利用原子类AtomicInteger,那我们利用ReentrantLock怎样处理的

public class ReentrantLockTest {
private static int count = 0;
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<10;i++){
new Thread(()->{
try{
lock.lock();
for(int j=0;j<1000;j++){
count++;
}
}finally {
lock.unlock();
}
}).start();
}
Thread.sleep(5000);
System.out.println("count="+count);
}
}

如果上面的代码不加锁的情况下,可能每一次运算的结果都不相同,而加锁后每一次运行结果都是一样的。运行结果如图:

上面是锁最简单的一个应用,在上一篇文章讲解AQS的Condition时,我利用了synchronized+wait()+notify()/notifyAll()阐述了生产者和消费者模式,那与之匹配的另一种方法如下:

上一篇文章的缓冲区代码改变如下:

public class SyncCache {
//缓冲区
private String[] data = new String[10];
//缓冲区数组索引
private int index;
//创建一个锁
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//生产数据的方法
public void product(String productData) {
try {
lock.lock();
if (index == data.length) {
System.out.println("缓冲区已满,生产者被阻塞");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
data[index++] = productData;
condition.signal();
} finally {
lock.unlock();
}
}
//消费数据的方法
public String consume() {
try {
lock.lock();
if (index == 0) {
System.out.println("缓冲区已空,消费者被阻塞");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
condition.signal();
index--;
return data[index];
} finally {
lock.unlock();
}
}
}

运行结果如下图:

二、从源码角度分析ReentrantLock

从上面的demo我们学会了使用ReentrantLock,那它的内部实现原理是怎样的呢?接下来我们进入它的源码一探究竟。

首先通过一张图来整体了解一下ReentrantLock的全貌:

从上面的demo中获取lock()方法,释放锁unlock()方法

//获取独占锁
public void lock() {
sync.lock();
}
//释放独占锁
public void unlock() {
sync.release(1);
}

上面锁的获取和释放的源码并没有更多的逻辑,而核心的逻辑都交给了Sync,那么Sync又是什么呢?在讲解AQS时我说过,在并发包中锁的底层实现都是通过AQS框架实现的,

如果想实现独占锁,子类只需要实现如下方法:

1:获取锁:tryAcquire()
2:释放锁:tryRelease()

如果想实现共享锁:

1:获取锁:tryAcquireShared()
2:释放锁:tryReleaseShared()

Sync就是AQS的子类,并且是独占锁模式。如图

在ReentrantLock中有两种模式:一种是非公平模式获取锁,另一种是公平模式获取锁。默认情况下是非公平的,我们看一下构造方法

//默认情况下是非公平模式
public ReentrantLock() {
sync = new NonfairSync();
}
//如果fair=true:表示是公平模式获取锁
//如果fair=false:表示是非公平模式获取锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

1:非公平模式获取锁:也是默认的模式

static final class NonfairSync extends Sync {
//非公平下获取锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//尝试获取资源
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}

从上面代码中可以看出,在非公平模式下,线程首先通过CAS机制尝试获取锁,如果获成功,则代表获取锁成功,如果获取失败,则直接调用AQS中的acquire()方法,而在讲解AQS之独占模式下资源的获取已经讲解过,acquire()方法首先会调用tryAcquire()方法尝试获取资源,如果获取失败则加入到CLH等待队列中,而非公平的tryAcquire()直接调用了父类Sync中的nonfairAcquire()方法。

final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取共享资源state
int c = getState();
//如果共享资源等于0:说明还没有线程获取到资源
if (c == 0) {
//通过CAS机制将资源从0变成acquires,成功则说明获取到锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果共享资源不等于0,但是获取资源的是当前线程,由于是可重入锁
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机制获取资源,如果成功则说明获取锁成功。如果不成功,则需要判断拥有资源的线程是否是当前线程,如果是当前线程,由于锁是可重入的,所以成功获取到锁,如果拥有资源的线程不是当前线程,则获取锁失败。

非公平模式下tryAcquire()获取共享资源的流程如下:

2:公平模式获取锁

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

考虑到公平原则,并没有像非公平模式下那样上来第一步就尝试获取锁,这样做的原因:可能在CLH等待队列中存在等待获取锁的线程,按照公平性,需要直接调用AQS中的acquire()方法,让它去判断是获取锁还是放到等待队列中,在公平模式下的tryAcquire()如下:

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;
}

上面的代码和在非公平模式下的代码极其类似,但是有一个非常重要的不同点就是:在公平模式下当共享资源还没有被任何线程获取时,它并没有直接通过CAS机制获取资源,而是首先调用hasQueuedPredecessors()方法来判断等待队列中是否有比自己早的线程在等待获取资源,流程图如下:

上面我分析了ReentrantLock公平模式和非公平模式获取锁的不同,一句话总结:非公平性允许线程插队获取资源,而公平性模式则不允许插队。

上面讲解了获取锁的过程,接下来就是释放锁了,释放锁就是释放获取到的资源,公平模式和非公平模式在释放锁时机制是一样的,所以释放锁在他们的父类中。

protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

首先判断当前线程是否是获取锁的线程,如果不是则直接抛出异常,然后判断此时的状态是否为0,因为是可重入锁,所以把全部的资源都释放才能释放锁,所以当共享资源为0时说明资源已全部释放。

发表评论
留言与评论(共有 0 条评论)
   
验证码:

相关文章

推荐文章

'); })();