聊聊并发:(九)concurrent包之ReentrantLock分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wtopps/article/details/82558102

前言

在上两篇文章中,

聊聊并发:(七)concurrent包之AbstractQueuedSynchronizer分析

聊聊并发:(八)concurrent包之AbstractQueuedSynchronizer源码实现分析

我们介绍了AbstractQueuedSynchronizer同步器的工作原理与源码实现,了解AQS的实现机制对于我们理解Lock的各种锁的实现是至关重要的,本篇,我们来学习一下Lock的其中一个实现ReentrantLock可重入锁的实现机制。

ReentrantLock介绍

ReentrantLock重入锁,是实现Lock接口的一个实现类,是我们开发并发程序中比较常用的一种锁,它支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。

我们在前面介绍synchronized关键字的时候提到过,synchronized的锁也是支持可重入的,只不过synchronized的可重入性是基于Java语言的实现机制实现,ReentrantLock是基于代码层面进行的实现。

相比于synchronized,ReentrantLock还支持公平锁和非公平锁两种方式。构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁与非公平锁的区别在于公平锁的锁获取是有顺序的。但是公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。

接下来,我们具体来看一下它是如何实现的。

锁的获取与可重入性

ReentrantLock如何获取锁:

//默认非公平锁
Lock nonFairLock = new ReentrantLock();
nonFairLock.lock();
//创建公平锁
Lock fairLock = new ReentrantLock(true);
fairLock.lock();

ReentrantLock获取锁的操作比较简单,直接调用其lock方法,即可完成锁的获取。ReentrantLock默认使用的是非公平锁,可以在构造方法中进行指定创建“公平锁”或“非公平锁”。

ReentrantLock的公平锁与非公平锁的实现是基于AQS进行实现的,其通过构造方法传入的值,构建不同的同步器,我们来看一下源码实现:

    public void lock() {
        sync.lock();
    }
    public ReentrantLock() {
        sync = new NonfairSync();
    }

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

ReentrantLock里面大部分的功能都是委托给Sync来实现的,同时Sync内部定义了lock()抽象方法由其子类去实现,默认实现了nonfairTryAcquire(int acquires)方法,可以看出它是非公平锁的默认实现方式。

非公平锁实现

非公平锁实现的实现是基于非公平同步器的,我们首先看一下非公平同步器的实现:

/**
 * 非公平锁同步器 
 */ 
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    final void lock() {
        //当前同步状态可用,直接将当前线程设置为获取到锁的线程
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            //反之,调用AQS的acquire获取同步状态
            acquire(1);
    }

    //重写AQS的tryAcquire方法的实现
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

首先第一次会检查当前同步状态是否可用,如果是,直接将当前线程设置为获取到锁的线程;
如果不是,则调用AQS中的acquire方法:

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

acquire方法中首先会调用tryAcquire(),在介绍AQS的时候我们提到过,这个方法是由子类进行实现的,非公平同步器重写了tryAcquire方法,即会调用其子类的实现:

final boolean nonfairTryAcquire(int acquires) {
    //获取当前线程
    final Thread current = Thread.currentThread();
    //获取当前同步状态
    int c = getState();
    //如果同步状态可用
    if (c == 0) {
        //获取同步状态成功,CAS设置当前同步状态
        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;
}

上面的代码就是非公平同步器获取同步状态的实现,我们在前面提到了,ReentrantLock是支持重入的锁,那么要想支持可重入,就需要就要解决两个问题:
- 1、线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;
- 2、由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。

我们再来看上面的代码,为了支持可重入性,在第二步增加了处理逻辑,如果该锁已经被线程所占有了,会继续检查占有线程是否为当前线程,如果是的话,同步状态加1返回true,表示可以再次获取成功。

公平锁实现

OK,分析完非公平锁,我们接下来再看一下公平锁的实现,同理,公平锁是基于公平同步器的,我们看一下公平同步器的实现:

/**
 * 公平锁同步器
 */
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        //直接调用AQS的acquire获取同步状态
        acquire(1);
    }

    //重写AQS的tryAcquire方法的实现
    protected final boolean tryAcquire(int acquires) {
        //获取当前线程
        final Thread current = Thread.currentThread();
        //获取当前同步状态
        int c = getState();
        //如果同步状态可用
        if (c == 0) {
            //判断当前节点在同步队列中是否有前驱节点
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                //获取同步状态成功,CAS设置当前同步状态,并将同步状态置为当前线程所有
                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;
    }
}

上面的代码就是公平同步器获取同步状态的实现,可以看到,其实现与非公平同步器基本一致,唯一有区别的地方,在于hasQueuedPredecessors方法,我们看一下该方法的实现:

public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    //头节点 != 尾节点
    //同步队列第一个节点不为null
    //当前线程是同步队列第一个节点
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

该方法会判断判断当前节点在同步队列中是否有前驱节点,如果有前驱节点说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败。如果当前节点没有前驱节点的话,再才有做后面的逻辑判断的必要性。

公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁。

释放锁

ReentrantLock如何释放锁:

Lock lock = new ReentrantLock();
lock.lock();
//do something....
lock.unlock();

释放锁的操作很简单,使用完毕后,直接调用unlock方法,即可释放,建议释放锁的操作要在finally中进行,以免出现异常情况导致死锁:

lock.lock();
try {
    //do something.....
} finally {
    lock.unlock();
}

ReentrantLock释放锁的操作同样是基于同步器进行实现的:

public void unlock() {
    sync.release(1);
}

同步器会调用AQS的release方法:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

release方法中,会调用tryRelease模板方法,同样,会调用其子类的实现(同样的套路有木有O(∩_∩)O哈哈~),我们来看一下子类的实现:

protected final boolean tryRelease(int releases) {
    ////减掉releases
    int c = getState() - releases;
    //如果释放的不是持有锁的线程,抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //state为0时,才表示释放成功
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

上面的代码就是ReentrantLock的同步器中释放同步状态的逻辑,需要注意的是,重入锁的释放必须得等到同步状态为0时锁才算成功释放,否则锁仍未释放。如果锁被获取n次,释放了n-1次,该锁未完全释放返回false,只有被释放n次才算成功释放,返回true。

这里与上面的可重入部分的逻辑相对应,重入了多少次,在释放的时候,也要对应释放多少次,才算真正的释放成功。

公平锁 VS 非公平锁

现在我们已经了解了公平锁与非公平锁的实现机制,我们来对比一下两种锁的特性:

  • 1、公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。
  • 2、公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。

所以在选择使用哪种锁的时候,需要根据具体的业务场景进行判断:
- 如果对于顺序性要求比较严苛的场景,那么公平锁可能更加的合适;
- 如果对于顺序性不敏感的,但是对于并发吞吐量要求比较高的场景,那么使用非公平锁更加的合适。

ReentrantLock VS synchronized

ReentrantLock与synchronized都可以进行锁的实现,也都支持可重入性,那么它们之间又有什么相异之处呢?

首先他们肯定具有相同的功能和内存语义。

  • 1、与synchronized相比,ReentrantLock提供了更多,更加全面的功能,具备更强的扩展性。例如:时间锁等候,可中断锁等候,锁投票。
  • 2、ReentrantLock还提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活,所以在多个条件变量和高度竞争锁的地方,ReentrantLock更加适合(以后会阐述Condition)。
  • 3、ReentrantLock提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而synchronized则一旦进入锁请求要么成功要么阻塞,所以相比synchronized而言,ReentrantLock会不容易产生死锁些。
  • 4、ReentrantLock支持更加灵活的同步代码块,但是使用synchronized时,只能在同一个synchronized块结构中获取和释放。注:ReentrantLock的锁释放一定要在finally中处理,否则可能会产生严重的后果。
  • 5、ReentrantLock支持中断处理,且性能较synchronized会好些。

结语

本篇我们介绍了ReentrantLock的使用方法以及实现机制,了解了其“公平锁”与“非公平锁的”实现机制。

ReentrantLock还有除了获取锁与释放锁的方法之外,还提供了很多其他的方法,例如设定超时获取锁、配合condition使用的方法等等,这里暂时不一一介绍,超时设定获取锁的方法可以参见之前介绍AQS的实现,condition的使用我们会在后面的篇幅中进行介绍。

本文参考:

JDK1.8源代码

JDK1.8中文文档

彻底理解ReentrantLock

更多Java干货文章请关注我的个人微信公众号:老宣与你聊Java
这里写图片描述

猜你喜欢

转载自blog.csdn.net/wtopps/article/details/82558102