01--Java并发编程简析--Lock 接口

Java并发编程简析

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

下文的一些插图借鉴了《Java并发编程的艺术》!当然笔者水平有限,还请指正错误!

1、 Lock 接口

自JDK1.5开始,Java提供了Lock接口,实现锁的机制,相对于Synchronize,Lock更为轻量级、同时增加了锁的可操作性。

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

Lock接口一共提供了五个方法。

  1. lock 加锁,如果当前线程无法获取到锁,则阻塞并休眠线程直至获取到锁。
  2. tryLock 尝试获取锁,若加锁成功,返回true;若加锁失败,当前线程不会阻塞,而是返回false。
  3. tryLock(long time, TimeUnit unit) 尝试获取锁,并指定超时时间。若当前锁处于空闲状态,并且当前线程未被中断,则可成功获取锁。若锁处于非空闲状态,则阻塞当前线程直至发生以下三个条件之一:
    1. 当前线程加锁成功
    2. 当前线程被中断
    3. 超时时间到
  4. unlock 解锁。

Lock接口具有多种实现类、并且该接口作为JDK提供的标准接口,也广泛应用于一些开源框架中,掌握Lock接口及其常见实现类,对实际项目开发有重要意义。下面通过介绍其部分实现类,来解开Lock接口的神秘面纱,毕竟多线程相关的东西,总给人一种比较难的感觉。

1.1、AbstractQueuedSynchronizer简介

以ReentrantLock类切入源码:

// 部分源码 。。。

// 同步器
abstract static class Sync extends AbstractQueuedSynchronizer{
     
    
}
// 非公平锁同步器
static final class NonfairSync extends Sync{
     
     
}
// 公平锁同步器
static final class FairSync extends Sync{
     
     
}

ReentrantLock提供了NonfairSync和FairSync两个静态内部类,以实现非公平锁和公平锁,这两个类的父类继承了SyncAbstractQueuedSynchronizer类,而AbstractQueuedSynchronizer是整个Lock接口实现类的基石,即队列同步器,也就是平时所说的AQS

AQS使用了模板方法模式,使用者需要继承并重写其中的部分方法:

// 维护state
protected final int getState() {
     
    return state;
}
protected final void setState(int newState) {
     
    state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
     
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

// 独占式获取、释放锁
protected boolean tryAcquire(int arg) {
     
    throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
     
    throw new UnsupportedOperationException();
}

// 共享式获取、释放锁
protected int tryAcquireShared(int arg) {
     
    throw new UnsupportedOperationException();
}
protected boolean tryReleaseShared(int arg) {
     
    throw new UnsupportedOperationException();
}

以上就是自己实现一个同步器可能需要重写的方法,分为三个部分:

  1. state状态维护。
  2. 独占式获取、释放锁
  3. 共享式获取、释放锁。

state代表同步状态(The synchronization state),也可以理解为锁。获取锁:0代表初始状态,1代表获取到锁的状态,如果锁可重入,相同线程再次获取则state为2;释放锁:将state值减1,直至为0(如果已经重入),代表线程释放了锁。当然state只是一个变量,你可以自定义其值以代表锁不同的状态。

除了上述的方法之外,AQS还提供了一个静态内部类Node:

static final class Node {
     
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
    // 节点状态
    volatile int waitStatus;
    // 前驱节点
    volatile Node prev;
    // 后继节点
    volatile Node next;
    // 线程
    volatile Thread thread;
    // condition的下一个节点
    Node nextWaiter;
}

通过其数据结构,可以发现这是一个双端队列,prev记录前驱节点,next记录后继节点,waitStatus变量记录了节点入队后的状态:

名称 说明
CANCELLED 1 表示由于超时或者中断,当前节点被取消。被取消后,当前节点的状态将不再发生任何变化。
SIGNAL -1 表示后继节点等待获取同步状态,当前节点释放同步状态、中断、取消则唤醒其后继节点,以继续获取同步状态。
CONDITION -2 表示当前节点在等待condition
PROPAGATE -3 表示下一次获取共享同步状态将被无条件传递下去
0 初始状态

双端队列示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8DIwhFHM-1597051143251)(media/15953305070670/15959327478856.jpg)]

AQS持有head和tail节点,分别指向队首和队尾,队列中的各个节点分别包含了对上一个、下一个节点的引用。如:对于互斥锁,当多个线程同时获取一把锁时,只能有一个线程可以获取到该锁,其他的线程被构造成Node节点,入队并阻塞。当持有锁的线程释放锁之后,唤醒下一个节点,下一个节点可以继续获取锁。这是AQS的基本原理,跟生活中排队的场景十分相似。

1.2、ReentrantLock

ReentrantLock顾名思义即可重入锁,可重入指已经获取锁的当前线程可再次获取锁,而不必等待当前锁的释放。

ReentrantLock中的Sync抽象类继承了AbstractQueuedSynchronizer类,并提供了两个实现类FairSync、NonfairSync以实现公平锁和非公平锁,并提供了两个构造函数,默认为非公平锁,或通过指定构造函数的参数实现公平锁。

接下来通过非公平锁锁的方式分析lock、tryLock以及unlock等方法,来了解加锁、解锁以及AQS。

加锁、释放锁案例:

@Test
public void testReentrantLock() {
     
    Lock lock = new ReentrantLock();
    lock.lock();
    try {
     
        System.out.println("加锁");
    } finally {
     
        lock.unlock();
        System.out.println("解锁");
    }
}

注意:加锁不能写在try代码块,如果try代码块加锁未成功,则finally代码块释放锁会出现异常。

1.2.1 lock()

final void lock() {
     
    // 设置同步状态
    if (compareAndSetState(0, 1))
        // 设置独占锁的线程为当前线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

lock方法尝试立刻获取锁。lock方法的特性,下面会逐步介绍。

  1. 加锁成功,更新同步状态为1。
  2. 加锁失败,调用acquire(1)方法阻塞当前线程直至获取到锁。当然这是整体的逻辑,在acquire调用过程中,依然会先尝试再次获取同步状态,当前线程并不一定被构建为Node节点并入队、阻塞。
public final void acquire(int arg) {
     
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

acquire预分析:

  1. 以独占线程的形式获取锁。
  2. 忽略中断。如ThreadA因获取锁失败而阻塞,此时手动调用interrupt()方法中断ThreadA,lock()方法不会响应该中断。
    1. 清除线程中断标志。
    2. 记录并返回线程是否被中断。
    3. 根据上一步返回值(此时已经获取到锁),判断是否恢复线程的中断状态。
    4. 既然lock方法不响应中断,为什么要多此一举做上面的操作呢?因为当线程获取锁失败加入同步队列时,会调用LockSupport.park(this)方法,该方法是会响应中断的。如ThreadA获取锁失败,入队并阻塞(线程状态为WAITING),此时手动调用interrupt()方法,则会唤醒ThreadA。这就违背了lock方法不响应中断的定义。
  3. 调用tryAcquire方法尝试获取同步状态,若成功,返回。
  4. 若失败,将当前线程构造为Node节点,入队并阻塞,此时线程会进入一个“自旋”状态,直至获取到同步状态。

附,中断:
中断可以简单理解为线程的一个标识属性,表示其是否被其他线程进行了中断操作。线程被中断不代表该线程的操作被终止,而是要线程自己去判断是否被中断过,并做出后续的处理。

1.2.1.1 tryAcquire

整体逻辑:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yPZKBNKz-1597051143252)(media/15953305070670/15959337500275.jpg)]

// 非公平锁加锁
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;
}
  1. 获取state。
  2. state为0,直接获取同步状态。
  3. state非0,则已经有线程获取了同步状态,尝试重入。
  4. 第二、第三个条件都不满足,则返回false。说明有其他线程获取了同步状态,当前线程需等待。
1.2.1.2 addWaiter

将等待获取同步状态的线程构造为Node节点,并入队。

private Node addWaiter(Node mode) {
     
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
     
    node.prev = pred;
    if (compareAndSetTail(pred, node)) {
     
        pred.next = node;
        return node;
    }
}
enq(node);
return node;
  1. 首先尝试快速在尾部添加节点。
  2. 若快速添加未成功,则调用enq方法入队。
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;
            }
        }
    }
}

enq方法通过一个for“死循环”来保证节点能够被添加到队列尾部。
1、如果队列的tail(尾节点)节点为空,则队列为空,此时需要实例化head节点、tail节点(注意此时并未跳出for循环)。
2、如果tail节点不为空,则队列至少已经包含了一个节点,将新添加节点的前驱节点设置为tail节点,并将tail节点设置为新增的节点。而保证节点能够线程安全的入队则采用了CAS机制。

1.2.1.3 acquireQueued

线程入队之后会调用acquireQueued方法,以“自旋”的方式获取同步状态。在获取到同步状态之前,这个for循环并不会一直无限循环下去,看代码:

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

第一步:获取前驱节点,并判断前驱节点是否为head节点,即获取同步状态的节点必须是队列中的第一个节点,以保证队列先进先出的原则。
第二步:没有成功获取同步状态,则判断并更改节点的状态,清除并记录当前线程中断标志(如果有)、阻塞当前获取锁的线程。

第一步代码:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
     
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
     
        do {
     
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
     
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

第二步代码:

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

// 这里已经到了获取同步状态最底层了
public static void park(Object blocker) {
     
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}

阻塞线程使用了JDK提供的LockSupport类的park方法。调用park方法后线程会进入阻塞状态,可以通过unpark()方法或者interrupt()方法唤醒该线程。这也是要清除当前线程中断标识的原因。park方法调用涉及到JVM的源码,超出本文的分析范围了。

park()方法调用后,线程进入阻塞状态,for循环也被阻塞,直至前驱节点释放锁将其唤醒,并继续获取同步状态。

1.2.1.4 lock(fair)

公平锁和非公平锁的区别在于,公平锁保证先请求获取锁的线程一定能够先获得锁

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

公平锁与非公平锁的唯一不同在于多了!hasQueuedPredecessors()判断,该方法的作用是查询是否有线程等待获取的时间长于当前线程。通过这个判断就保证了线程获取同步状态的绝对时间顺序,即先请求的线程一定先获得同步状态。

而确保这一逻辑的是acquire方法,回顾一下:

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

所有获取同步状态的线程都要先调用tryAcquire尝试获取同步状态,从而保证了先到的线程一定能够先获取到同步状态,或者先加入到同步队列,从而保证了锁的公平性。

1.2.1.5 unlock

通过unlock方法执行解锁操作,调用release方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i6spUNSi-1597051143254)(media/15953305070670/15959338321882.jpg)]

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、唤醒后继节点。

释放同步状态:

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);
    }
    setState(c);
    return free;
}

需要注意一点,如果当前锁已经发生了重入行为,需逐步递减同步状态,当同步状态值为0时,表示完全释放了同步状态。

唤醒后继节点:

Node h = head;
if (h != null && h.waitStatus != 0)
    unparkSuccessor(h);
    
private void unparkSuccessor(Node node) {
     
    
    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);
}    

前文提到,获取同步线程失败的线程,被构造成Node节点,加入到队列并阻塞,这里调用LockSupport.unpark(s.thread)方法,将其唤醒以继续获取同步状态。LockSupport工具类底层使用了unsafe的本地方法,涉及到JVM源码这里不做深入的分析,因为我也不会。

1.2.2 tryLock(long time, TimeUnit unit)

除了lock()方法外,Lock接口还提供了tryLock()和tryLock(long time, TimeUnit unit),定义如下:

// lock 
void lock();

// tryLock
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

从方法定义上对比lock()方法,tryLock()方法、tryLock(long time, TimeUnit unit)有返回值,且后者可以指定获取锁的超时时间、会抛出InterruptedException异常,也就是说tryLock(long time, TimeUnit unit)是可以响应线程中断的。

这里只分析非公平锁模式下的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();
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}
  1. 响应中断。如果当前线程被中断,抛出InterruptedException异常。
  2. 首先调用tryAcquire尝试直接获取锁,成功,返回true;失败,调用doAcquireNanos继续获取锁。

tryAcquire方法前文已有介绍,下面看doAcquireNanos方法。

1.2.2.1 doAcquireNanos
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
     
    // 1.超时
    if (nanosTimeout <= 0L)
        return false;
    final long deadline = System.nanoTime() + nanosTimeout;
    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;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
     
        if (failed)
            cancelAcquire(node);
    }
}

方法整体逻辑与前文介绍的acquireQueued方法相似,但是多了对超时时间的处理、对线程中断的响应、取消获取同步状态等操作。

  1. 处理超时时间(纳秒级)
    处理超时时间依靠LockSupport.parkNanos(this, nanosTimeout)来完成。用线程超时截止时间减去当前时间,得出一个时间段nanosTimeout,作为parkNanos方法的超时时间,parkNanos超时后唤醒当前线程继续获取同步状态。如果nanosTimeout小于等于0的时候依然未获取到同步状态,则超时。
  2. 响应中断
    抛出InterruptedException异常,这一点与acquireQueued截然不同。
  3. 取消获取同步状态
    若获取同步状态失败,在finally代码块取消当前线程对同步状态的获取。

1.2.3 tryLock()

这里依然只分析非公平锁下的tryLock()方法。相对前文介绍的两种加锁方式,这种加锁就相当简单了:

public boolean tryLock() {
     
    return sync.nonfairTryAcquire(1);
}

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

与前面提到的两种加锁方式最大的不同:当前线程获取同步状态失败时,tryLock方法直接返回false。

1.2.4 lockInterruptibly()

前文提到tryLock(long time, TimeUnit unit)方法会响应中断,除了该方法,Lock接口还提供了一种专门可响应中断获取锁的方法:lockInterruptibly()

public final void acquireInterruptibly(int arg) throws InterruptedException {
     
    if (Thread.interrupted())
        throw new InterruptedException();
    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);
    }
}

相对于Lock方法,在①处的处理是直接抛出异常。其他的逻辑前文有介绍,不多赘述。

你可能感兴趣的:(并发编程简析,并发编程,可重入锁,ReentrentLock,Lock,中断)