1、Lock接口以及ReentrantLock可重入锁

1、序


并发编程在Java实际开发中,占有举足轻重的地位,在接下来的篇幅中,以java.util.concurrent包下重要的、常用的接口、实现类为切入点,逐步分析并发编程。

java.util.concurrent包下的类相对来讲较难,需要反复的分析、调试。每次分析的过程中都会有不同的心得体会,所以本系列的代码分析,力求细致;语言描述,力求简洁。

其中的一些插图借鉴了《Java并发编程的艺术》一书,部分接口、类、方法、以及变量的定义摘抄自JDK的源码注释。当然鉴于水平有限,也希望大家多多指正其中的不足。

2、Lock 接口

Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition。

锁是用于通过多个线程控制对共享资源的访问的工具。通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁。但是,一些锁可能允许并发访问共享资源,如ReadWriteLock的读锁。

使用synchronized方法或语句提供对与每个对象相关联的隐式监视器锁的访问,但是强制所有锁获取和释放以块结构的方式发生:当获取多个锁时,它们必须以相反的顺序被释放,并且所有的锁都必须被释放在与它们相同的词汇范围内。

虽然synchronized方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。 例如,用于遍历并发访问的数据结构的一些算法需要使用“手动”或“链锁定”:您获取节点A的锁定,然后获取节点B,然后释放A并获取C,然后释放B并获得D等。所述的实施方式中Lock接口通过允许获得并在不同的范围释放的锁,并允许获得并以任何顺序释放多个锁使得能够使用这样的技术。

Lock实现提供了使用synchronized方法和语句的附加功能,通过提供非阻塞尝试来获取锁( tryLock() ),尝试获取可被中断的锁( lockInterruptibly()) ,以及尝试获取可以超时( tryLock(long, TimeUnit) )。

内存同步 所有Lock实施必须执行与内置监视器锁相同的内存同步语义,如The Java Language Specification (17.4 Memory Model) 所述:

  1. 成功的lock操作具有与成功锁定动作相同的内存同步效果。
  2. 成功的unlock操作具有与成功解锁动作相同的内存同步效果。
  3. 不成功的锁定和解锁操作以及重入锁定/解锁操作,不需要任何内存同步效果。

虽然JDK的注释通过翻译软件直译过来有些拗口,但是没有比官方文档更为好的解释。

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

Lock接口一共提供了6个方法。虽然只是接口定义,但是对我们了解其实现原理极为重要,因为接口的实现,也是依据其定义而展开。若不对其定义有充分的了解,当分析到源码细节的时候,将举步维艰,不知所云。

  1. lock()
    1. 若锁可用,立刻获得锁
    2. 若锁不可用,阻塞当前线程直至获取到锁
  2. lockInterruptibly()
    1. 若锁可用,立刻获得锁
    2. 若锁不可用,当前线程将被阻塞,直至发生以下情况
      1. 当前线程获得锁
      2. 当前线程被其它线程中断
  3. tryLock()
    1. 若锁可用,立刻获得锁,并返回true
    2. 锁锁不可用,立刻返回false,而不阻塞当前线程
    3. 该方法可以理解为尝试获取锁、或快速获取锁,因为该方法不会阻塞线程
  4. tryLock(long time, TimeUnit unit)
    1. 若锁可用,立刻获得锁,并返回true
    2. 若锁不可用,当前线程将被阻塞,直至发生以下情况
      1. 当前线程获的锁
      2. 当前线程被其它线程中断
      3. 超时
  5. unlock()
    1. 释放锁
    2. 释放锁的线程必须是持有当前锁的线程
  6. newCondition()
    1. 创建一个与当前锁绑定的Condition(条件)
    2. 在条件满足之前,锁将被持有
    3. Condition一般可用于有界操作,如大小固定的缓冲区

3、AbstractQueuedSynchronizer

Lock接口最为典型的实现类是ReentrantLock,可重入锁。在介绍ReentrantLock之前,必须先了解一下AbstractQueuedSynchronizer,队列同步器,也就是我们平时所说的AQS。AbstractQueuedSynchronizer提供了一个框架,用于实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件等)。对于大多数依赖单个原子int值来表示状态的同步器,这个类被设计为一个有用的基础。此类支持默认独占模式和共享模式。

3.1 双端队列

AbstractQueuedSynchronizer提供了一个静态内部类Node,用来实现一个双端队列。Node类代表了双端队列中的一个节点,其中waitStatus变量以及其不同的值的含义,至关重要。

static final class Node {
    // 标记节点以共享模式阻塞
    static final Node SHARED = new Node();
    // 标记节点以独占形式阻塞
    static final Node EXCLUSIVE = null;
    // 以下四个枚举值表示了 waitStatus 不同状态
    // 节点被取消
    static final int CANCELLED =  1;
    // 后继节点等待被唤醒
    static final int SIGNAL    = -1;
    // 节点在“条件”上等待
    static final int CONDITION = -2;
    // 下一次获取共享同步状态将被无条件传递下去
    static final int PROPAGATE = -3;
    // 节点在队列中的状态,默认初始值为0
    volatile int waitStatus;
    // 上一个节点
    volatile Node prev;
    // 下一个节点
    volatile Node next;
    // 获取锁的线程
    volatile Thread thread;
}
名称说明
CANCELLED1表示由于超时或者中断,当前节点被取消。被取消后,当前节点的状态将不再发生任何变化。
SIGNAL-1表示后继节点等待获取同步状态,当前节点释放同步状态、中断、取消则唤醒其后继节点,以继续获取同步状态。
CONDITION-2表示当前节点在等待condition
PROPAGATE-3表示下一次获取共享同步状态将被无条件传递下去
0初始状态

有了节点以后,再来看AbstractQueuedSynchronizer类中其他几个重要的变量:

// 等待队列的头,被惰性地初始化。除了初始化之外,它只通过方法setHead进行修改。注意:如果head存在,它的等待状态保证不会被取消。
private transient volatile Node head;
// 等待队列的尾部,延迟初始化。仅通过enq()方法修改以添加新的等待节点。
private transient volatile Node tail;
// 同步状态
private volatile int state;

AbstractQueuedSynchronizer持有了头节点和尾节点,并分别指向队列中的第一个节点和最后一个节点。最终形成的队列如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h5vpXrzG-1605858563162)(media/16057705056625/16057803571571.jpg)]

3.2 state变量

state变量可以说是AbstractQueuedSynchronizer中最为重要的变量。jdk的注释:The synchronization state。直译过来即同步状态。

对于独占锁,state变量即可以理解为锁。当state为0时,表示锁空闲,当前线程可以获得锁;当state为1时,表示锁被其它线程占有,当前线程无法获取锁(假如不允许重入)。那么当前线程可以被构造为Node节点,加入到队列中、并阻塞。当持有锁的线程释放锁之后,将state变量重新设置为0,并唤醒阻塞的线程,继续获得锁。

对state变量的操作,可以通过以下三个方法进行:

// 获取同步状态值,该方法具有volatile读的内存语义
protected final int getState() {
    return state;
}

// 设置同步状态值,该方法具有volatile写的内存语义
protected final void setState(int newState) {
    state = newState;
}

// 如果当前状态值等于预期值,则自动将同步状态设置为给定的更新值。
// 该方法兼顾volatile读、写的内存语义
protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

关于AbstractQueuedSynchronizer就先介绍这么多,在后续的代码分析中,结合实际的例子来分析该类的其它方法。

4、ReentrantLock简介以及其非公平锁模式

ReentrantLock可重入锁是Lock接口最为典型的一个实现类,它具有以下特点:

  1. 独占锁,即同一时刻,只能有一个线程持有锁
  2. 可重入,持有锁的线程可再次获得锁,而无需等待
  3. 提供公平锁、非公平锁两种模式,默认为非公平锁。对于公平锁,ReentrantLock保证先请求获取锁的线程,一定先获得锁,即“先到先得”
4.1、lock()
// 案例:
public void testReentrantLock() {
    Lock lock = new ReentrantLock();
    // 注意:加锁不能写在try代码块,如果try代码块加锁未成功,则finally代码块释放锁会出现异常。
    lock.lock();
    try {
        System.out.println("加锁");
    } finally {
        lock.unlock();
        System.out.println("解锁");
    }
}

通过lock接口获取锁,大体上可以分为以下三种情况:

  1. 锁空闲,加锁并将持有锁的计数(state)加1。
  2. 锁被当前线程持有(重入),将持有锁的计数加一。
  3. 锁被其它线程持有,则阻塞当前线程直至获取到锁,然后将持有锁的计数设置为一。
final void lock() {
    // 无线程持有锁
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    // 已有线程持有锁
    else
        acquire(1);
}

lock()方法执行过程:

  1. 通过compareAndSetState()方法尝试立即获取同步状态
    1. 成功,返回
    2. 失败,调用acquire()方法继续获取同步状态
4.2、acquire()
public final void acquire(int arg) {
    // 尝试快速获取同步状态,若失败,则阻塞该线程直至获取到同步状态
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 若线程在获取同步状态的过程中曾被其它线程中断,则再次中断该线程
        // 注意:acquireQueued返回值标识该线程在阻塞期间是否被中断,而不是是否成功获取到同步状态
        selfInterrupt();
}
  1. 以独占形式获取锁
  2. 忽略中断
    1. 假如T1进入lock()方法后即被其它线程中断,lock()方法不会抛出InterruptedException异常
    2. acquire()在方法执行过程中会清除、并记录线程是否被中断
      1. 否,不做任何操作
      2. 是,调用selfInterrupt()中断线程,此时已经获取到同步状态,线程得以继续被执行,线程调用者可以自行决定是否中断继续中断该线程。
  3. 至少调用一次tryAcquire()方法尝试快速获取同步状态(该方法不会阻塞)
    1. 成功,返回
    2. 失败
      1. 调用addWaiter()方法将线程构造为Node节点加入同步队列
      2. 阻塞线程直至获取到同步状态,在此期间,线程可能会反复被唤醒、阻塞,直至再次通过tryAcquire()方法获取到同步状态为止

下面逐个分析tryAcquire()、addWaiter()、以及acquireQueued()方法。

4.2.1、tryAcquire()

tryAcquire()方法可以理解为“尝试获取同步状态”或“快速获取同步状态”。该方法最大的特点是立刻返回是否成功获取同步状态,而不阻塞线程。对于独占锁,获取同步状态的情况可以分为两种:锁空闲或线程重入,可以立即获取;锁被占用,以阻塞的形式获取。因为当一个线程获取锁时,无法确定锁是否被持有,可以先通过tryAcquire()快速获取锁;若失败则将线程构造为Node节点加入阻塞队列,当其满足出队条件后,将被唤醒,然后再次调用tryAcquire()方法获取同步状态。

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

// 以独占的形式快速获取同步状态
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 同步状态为0,锁空闲,可以立即获取锁
    if (c == 0) {
        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;
}
  1. 以独占的形式获取锁
    1. 成功,返回true
    2. 失败,判断是否重入
      1. 是,将同步状态加一并更新,返回true
      2. 否,锁被其它线程持有,返回false
  2. 该方法不会阻塞线程,而是立刻返回结果
4.2.2、addWaiter()

若addWaiter()方法得以被执行,说明锁被其它线程持有,则将当前获取锁的线程构造成Node节点加入到同步队列中。

  • 快速入队:
private Node addWaiter(Node mode) {
    // 将当前线程构造为Node节点
    Node node = new Node(Thread.currentThread(), mode);
    // 尝试快速在队列尾部添加
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 快速添加失败,调用全量方法将节点加入队列
    enq(node);
    return node;
}
  • 全量方法入队:
// 节点入队全量方法
private Node enq(final Node node) {
    // 自旋
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

addWaiter()方法涉及到一些队列知识,比较简单,不多赘述。

4.2.3、acquireQueued()

加入到同步队列中的节点,将以“自旋”的方式继续尝试获取同步状态。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 自旋
        for (;;) {
            // 获取前驱节点
            final Node p = node.predecessor();
            // ①
            // 为保证队列“先进先出”的特性。当前驱节点为头节点时,可尝试获取同步状态,其它节点依然阻塞
            // 注意:若某个节点的前驱节点为头节点,则说明它是队列中的第一个节点,符合“先进先出”原则
            if (p == head && tryAcquire(arg)) {
                // 获取同步状态成功,更新头结点
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // ②
            // 若前驱节点非头结点、或者获取同步状态失败,则阻塞当前节点
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  1. 第一个if,判断前驱节点是否为head节点(保证队列“先进先出”的原则)
    1. 是,尝试获取同步状态
      1. 获取成功,返回true
      2. 获取失败,进行第二个if判断
    2. 否,进行第二个if判断
  2. 第二个if,执行到此,要么前驱节点非头节点,不符合出队原则;要么前驱节点是头结点,但获取同步状态失败(持有锁的线程依然未释放锁)
    1. 判断当前节点尝试获取同步状态失败后是否应当阻塞、并更新节点状态
      1. 是,阻塞当前节点,清除、并记录当前线程中断标识
      2. 否,自旋,进行下一轮判断

第一个if判断非常简单,不多介绍,下面看第二个if语句中的两个判断条件:

4.2.3.1 shouldParkAfterFailedAcquire()
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 获取前驱节点的状态
    int ws = pred.waitStatus;
    // 前驱节点的状态为SIGNAL,则后继节点应被阻塞,返回true
    if (ws == Node.SIGNAL)
        return true;
    // 前驱节点状态为CANCELLED,则清除队列中已经被取消的节点
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    }
    // 运行至此,前驱节点的状态要么为初始状态(0)、要么为PROPAGATE。
    // 此时,将前驱节点的状态改为 SIGNAL,以便在下一轮自旋中阻塞当前节点
    else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

假设T1持有锁且不释放,T2再次获取锁,则acquireQueued()方法将被调用。因为T2的前驱节点即为头节点,其符合出队条件。那么再次调用tryAcquire()方法获取锁,因为T1不释放锁,tryAcquire()返回false,所以shouldParkAfterFailedAcquire()方法将被调用。

进入到shouldParkAfterFailedAcquire()方法后(注意该方法参数一个是前驱节点、一个是当前节点),前驱节点的状态为0,将前驱节点状态改为SIGNAL,返回false。

因为acquireQueued()方法以自旋的方式进行,所以上面的步骤被重复,当获取同步状态再次进入shouldParkAfterFailedAcquire()之后,前驱节点的状态已经变为SIGNAL,而且两次通过tryAcquire()方法获取同步状态均失败,则当前线程应被阻塞。

如果此时T3也来获取锁,则重复上面的步骤,当然此时的T3是不符合出队条件的,总而言之,通过acquireQueued()方法自旋调用,加上shouldParkAfterFailedAcquire()方法,保证了在必要时候将前驱节点的状态设置为SIGNAL。

4.2.3.2 parkAndCheckInterrupt()
// 阻塞当前节点,清除并返回当前线程中断标识
private final boolean parkAndCheckInterrupt() {
    // 阻塞当前线程
    LockSupport.park(this);
    // 清除并返回线程中断标识
    return Thread.interrupted();
}

该方法很简单,不多赘述。线程被阻塞之后,则等待前驱节点释放同步状态之后将其唤醒,继续获取同步状态即可。代码分析至此,lock()方法就结束了。

4.3、unlock()

解锁流程如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n12lMThi-1605858563164)(media/16057705056625/16058499546947.jpg)]

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

public final boolean release(int arg) {
    // 释放同步状态
    if (tryRelease(arg)) {
        // 唤醒后继节点
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
  1. 释放同步状态
  2. 唤醒后继节点
4.3.1、tryRelease()
// 释放同步状态
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 释放锁的线程必须与AQS中持有同步状态的线程相同
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 完全释放同步状态,清空独占线程
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 更新state值
    setState(c);
    return free;
}

tryRelease()方法主要是为了更新AQS中state变量的值。前文已经介绍过,对于独占锁来讲,state等于0可以表示锁空闲,state大于0可以表示锁被持有。同时要考虑锁被重入的情况,所以讲state值递减,直至为0,则同步状态完全被释放。

4.3.2、unparkSuccessor()
// 唤醒后继节点   
private void unparkSuccessor(Node node) {
    // 如果节点的waitStatus值小于0,则更新为0
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    
    Node s = node.next;
    // 回溯,保证后继节点及其状态的合法性
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 唤醒后继节点
    if (s != null)
        LockSupport.unpark(s.thread);
}    

unparkSuccessor()方法顾名思义为唤醒后继节点,但是这个说法不太准确,应该叫唤醒有效的后继节点更为贴合代码语义。

  1. 如果节点的waitStatus值小于0,则更新为0。todo 这一步的目的不太清楚
  2. 回溯,保证队列节点的合法性,防止节点状态为取消、节点为空的情况
  3. 唤醒第一个可用的后继节点

到这里,基于lock()、unlock()方法的加锁、解锁过程就分析完毕了。

4.4、lockInterruptibly()

相较于lock()接口lockInterruptibly()在获取锁的过程中,会响应中断。因为前文已经对lock()接口做了较为详细的分析,所以这里我们只简单的分析一下lockInterruptibly()是如何响应中断的。

// 以响应中断的模式获取同步状态
public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg) throws InterruptedException {
    // ①
    // 快速检查线程是否被中断
    if (Thread.interrupted())
        throw new InterruptedException();
    // 调用tryAcquire()方法获取同步状态。注意:该方法不响应中断
    if (!tryAcquire(arg))
        // 以响应中断的模式获取同步状态
        doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg) throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // ② 响应中断
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  1. lockInterruptibly()对中断的响应分别标记在代码的①、②处:
    1. 快速检查线程是否被中断
      1. 是,抛出InterruptedException异常
      2. 否,继续获取同步同步状态
    2. 判断线程在获取锁的过程中是否被中断
      1. 否,返回
      2. 是,抛出InterruptedException异常
  2. lock()方法和lockInterruptibly()对线程中断的处理方式区别:
    1. lock方法不响应中断,但是会记录中断状态,开发者需要自己去判断、并响应中断
    2. lockInterruptibly()方法响应中断,若线程被中断,抛出InterruptedException异常
4.5、tryLock()

tryLock()方法以非阻塞的形式获取锁。若获取到锁,返回true;否则,返回false,而不会阻塞获取锁的线程。tryLock()方法的具体代码,前文都有介绍,不多赘述。

4.6、tryLock(long time, TimeUnit unit)

前文已经介绍过了lock()、lockInterruptibly()、tryLock()三种获取同步状态的方式。这三种方式个有优缺点。

  1. lock() 以阻塞的形式获取锁,不响应中断
  2. lockInterruptibly() 以阻塞的形式获取锁,响应中断
  3. tryLock() 以非阻塞的形式获取锁,不响应中断

综合以上各个方法的特性,lock()、lockInterruptibly()虽然能获取到锁,但是调用者不知道会阻塞多久;tryLock()方法虽然能快速返回是否获取到锁,但是又不会阻塞。tryLock(long time, TimeUnit unit)方法正好综合了以上特点。以带超时阻塞的形式获取锁。

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
    // 响应中断
    if (Thread.interrupted())
        throw new InterruptedException();
    // 调用tryAcquire()方法尝试快速获取同步状态
    // 若tryAcquire()方法未能获取到同步状态,则调用doAcquireNanos以带超时阻塞的形式再次获取同步状态
    return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}

tryLock(long timeout, TimeUnit unit)方法中的大部分代码均已分析过,不在赘述,这里只分析doAcquireNanos()方法。

// 以超时模式获取同步状态
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
    // 超时
    if (nanosTimeout <= 0L)
        return false;
    // 计算超时到期时间
    final long deadline = System.nanoTime() + nanosTimeout;
    // 线程构造为AQS节点、入队
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            // 符合出队条件,再次获取同步状态
            if (p == head && tryAcquire(arg)) {
                // 获取同步状态成功,重新设置头节点、清空释放同步状态的节点
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            // 计算需要阻塞的时长
            nanosTimeout = deadline - System.nanoTime();
            // 超时
            if (nanosTimeout <= 0L)
                return false;
            // spinForTimeoutThreshold:自旋超时阈值,1000纳秒(1秒=1000毫秒;1毫秒=1000微秒;1微秒=1000纳秒)
            // 如果nanosTimeout大于spinForTimeoutThreshold,则将线程阻塞nanosTimeout纳秒
            if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)
                // 阻塞nanosTimeout纳秒,到期唤醒后,再次以自旋的形式获取锁
                LockSupport.parkNanos(this, nanosTimeout);
            // 响应中断
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

到这里,基于ReentrantLock可重入锁的非公平模式下的lock()、lockInterruptibly()、tryLock()、tryLock(long time, TimeUnit unit)、以及unlock()方法都以分析完毕。这一部分内容相对来讲较难,需要多多分析、多调试才能有更为深刻的了解。笔者水平有限,还望多多指正。

5、ReentrantLock的公平锁模式

公平锁模式保证先获取锁的线程一定能够先获得锁,简单理解就是“先到先得”。前文已经分析过非公平锁模式,下文的分析我们着重分析两者之间的区别,而不再逐个分析每个方法。

5.1、lock()
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;
}

针对于lock()方法,非公平锁和公平锁的区别就在于在tryAcquire()方法中多了一个hasQueuedPredecessors()判断。即判断是否有线程等待获取的时间长于当前线程。

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());
}
5.2、tryLock()

tryLock()方法会破坏锁的公平性。若能立即获取锁,返回true;否则返回false。而不会考虑是否有其他线程先于此线程获取锁。

5.3、tryLock(long time, TimeUnit unit)

tryLock(long time, TimeUnit unit)保持锁的公平性与lock()方法一致,不多赘述。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
1. Lock接口的实现 Lock接口定义了一组锁的操作方法,主要包括锁定、解锁和获取锁状态等操作。Java提供了多种锁的实现方式,如ReentrantLock、ReentrantReadWriteLock、StampedLock等。 2. 可重入锁ReentrantLock 可重入锁指的是同一个线程可以多次获取同一把锁,而不会出现死锁的情况。ReentrantLock是Java中最常用的可重入锁的实现,它可以替代synchronized关键字进行线程的同步控制。 3. 原理 ReentrantLock实现可重入锁的原理主要是基于一个计数器来实现的,当一个线程第一次获取锁时,计数器的值会加1,而当这个线程再次获取锁时,计数器的值会再次加1,这样就可以保证同一个线程可以多次获取锁。 同时,ReentrantLock还支持公平锁和非公平锁的机制,公平锁会按照线程的等待时间进行锁的获取,而非公平锁则不区分等待时间。另外,ReentrantLock还支持可中断锁和超时锁的机制,可以避免死锁的情况。 4. 读写锁 读写锁是一种特殊的锁,它允许多个线程同时读取共享资源,但只有一个线程可以写入共享资源。Java中的ReentrantReadWriteLock就是读写锁的实现方式,它可以提高读取操作的并发性能,同时保证写入操作的原子性和操作顺序。 读写锁的原理是基于一个共享锁和一个排它锁来实现的,当有线程读取共享资源时,可以获取共享锁,这样多个线程可以同时读取共享资源。当有线程写入共享资源时,需要获取排它锁,这样只有一个线程可以写入共享资源,其他线程需要等待排它锁释放。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

闲来也无事

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值