Java并发------AbstractQueuedSynchronizer之ReentrantLock(一)

前言

本文介绍 Java 用 ReentrantLock 实现线程安全方式,ReentrantLock 是使用代码实现的,系统无法自动释放锁。ReentrantLock 是 jdk1.5 出现,这是与原有的 synchronized 完全不同的实现线程安全的方式。高并发的情况下,ReentrantLock是个不错的方案。

ReentrantLock 底层使用 CAS 方式来实现线程安全的,这是一种性能很高的实现线程安全的一种方式。jdk8 中 ConcurrentHashMap 底层就是大量使用的 CAS 操作,源码没有使用 synchronized 的关键字。

我们看一下 ReentrantLock 的基本使用方法:

public static void main(String[] args) {
    ReentrantLock reentrantLock = new ReentrantLock(true);
    new Thread(() -> {
        reentrantLock.lock();
        System.out.println("线程1获取到锁");
        try {
            Thread.sleep(3000);
            System.out.println("线程1执行完成");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }).start();
    new Thread(() -> {
        long start = System.currentTimeMillis();
        reentrantLock.lock();
        long end = System.currentTimeMillis();
        System.out.println("等待" + (end - start)  + "毫秒后线程2获取到锁");
        try {
            System.out.println("线程2执行完成");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }).start();
}

一、AbstractQueuedSynchronizer简介

AbstractQueuedSynchronizer 是 Java 并发包的基础工具类,是实现 ReentrantLock、CountDownLatch、Semaphore、FutureTask 等的基础。后面 AbstractQueuedSynchronizer 简称 AQS。

ReentrantLock 与 AQS 的实现关系:

ReentrantLock 的创建机制:

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

Sync 是 ReentrantLock 的内部抽象实现类,它初步实现了 AQS:

abstract static class Sync extends AbstractQueuedSynchronizer {

}

NonfairSync(非公平锁)和 FairSync(公平锁)是 Sync 的两个实现类。

二、AQS 基本实现原理

AQS的基本属性:

// 头结点,当前持有锁的线程
private transient volatile Node head;
// 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个隐视的链表
private transient volatile Node tail;
// 这个是最重要的,不过也是最简单的,代表当前锁的状态,0代表没有被占用,大于0代表有线程持有当前锁,是因为锁可以重入嘛,每次重入都加上1
private volatile int state;
// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入
// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁
private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer

这四个属性就是实现线程安全的基本属性:当第一条线程进来执行时,把当前线程放入 head 中,把 state 的初始状态修改为 1,代表当前线程持有锁,后续等待的线程进来后放入 tail 这个链表中。第一条线程执行完成后 state 会恢复初始 0,等待队列 tail 会拿出头部节点放入 head 继续执行。exclusiveOwnerThread 用来存储当前有锁的线程,每次都会比较 currentThread 与 exclusiveOwnerThread 是否是同一条线程。

记住这个概念:head 代表即将已经获得锁的线程节点 Node,tail 代表等待队列最尾的线程节点 Node。

在这里插入图片描述

线程都会被包装成 Node 对象,Node 是一个链表结构:

static final class Node {
    // 标识节点当前在共享模式下
    static final Node SHARED = new Node();
    // 标识节点当前在独占模式下
    static final Node EXCLUSIVE = null;
    // ======== 下面的几个int常量是给waitStatus用的 ===========
    volatile int waitStatus;
    // 代表此线程取消了争抢这个锁
    static final int CANCELLED =  1;
    // 代表当前node的后继节点对应的线程需要被唤醒
    static final int SIGNAL    = -1;
    // 代表当前node是在条件队列中,后续有介绍
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
    // 处于阻塞队列的节点属性,代表前驱节点的引用
    volatile Node prev;
    // 处于阻塞队列的节点属性,代表后续节点的引用
    volatile Node next;
    // 这个就是线程本尊
    volatile Thread thread;
    // 处于条件队列中的节点属性,代表后续节点的引用
    Node nextWaiter;
}

head 和 tail 的初始化讲一下(一直没有释放锁的情况):

  • Node 的初始化 waitStatus 默认值是 0,thread 默认值是 null;
  • 当第一条线程来获得锁时,直接获取到不会再向下走;
  • 当第二条线程进来时,会开始初始化 head 和 tail,这时 tail 指向的其实就是 head。
  • 当第三条线程进来时,会把 head 的 waitStatus 置为 -1,tail 指向这个新加入的 Node,这时候 tail 的 waitStatus 是 0,thread 是当前线程;
  • 以此类推,每次进来的新线程都会把前驱节点的 waitStatus 置为 -1,tail 永远指向这个新 Node。

Node 节点的 prev、next 和 nextWaiter 这三个属性代表的含义是不同,prev 和 next 是处于阻塞队列才会有的属性,nextWaiter 是处于条件队列才会有的属性,条件队列后面再介绍。也就是说,如果一个节点的 next 属性不为空,那么可以直接判断这个节点一定不在条件队列中。这个可以直接用于判断,后面代码也会看到加入到不同的队列后会把其他不相关的属性置为 null。

下面是AQS保证线程安全的实现思路,与下面源码对比理解:
在这里插入图片描述

三、FairSync(公平锁)的实现原理

1、加锁

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;
      // 争锁
    final void lock() {
        acquire(1);
    }
}    

acquire() 这个方法直接调用父类 AQS:

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

前面说到 AQS 中的 state 为 0 时,代表没有线程来抢锁;为 1 时代表抢到了锁,所以 acquire(1) 传入 1,然后调用 if 中的 tryAcquire(1) ,就是试着抢一下锁,抢到了锁就 ok 了,也不需要向下执行。

没抢到锁后继续执行 addWaiter(null)。addWaiter 这个方法就是将当前线程包装成 Node 对象并返回,使用 EXCLUSIVE 独占锁的模式。再继续调用
acquireQueued(node, 1) ,将当前线程加入到等待队列挂起等着抢到锁再退出。

下面分步来说明这几个方法:

1)tryAcquire

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // state == 0 此时此刻没有线程持有锁
    if (c == 0) {
        // 虽然此时此刻锁是可以用的,但是这是公平锁,既然是公平,就得讲究先来后到,
        // 看看有没有别人在队列中等了半天了
        if (!hasQueuedPredecessors() &&
                // 如果没有线程在等待,那就用CAS尝试一下,成功了就获取到锁了,
                // 不成功的话,只能说明一个问题,就在刚刚几乎同一时刻有个线程抢先了
                compareAndSetState(0, acquires)) {
            // 到这里就是获取到锁了,标记一下,告诉大家,现在是我占用了锁
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 会进入这个else if分支,说明是重入了,需要操作:state=state+1
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 如果到这里,说明前面的if和else if都没有返回true,说明没有获取到锁
	return false;
}
public final boolean hasQueuedPredecessors() {
    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());
}

抢锁的代码比较简单,就是先查看队列中是否有等待的线程。为什么 state 为 0 时还需要查看是否有等待队列呢,因为第一种情况就是当前线程是第一条进来抢锁的线程,等待队列为空,自己拿到锁就可以了;第二种情况是上一条线程刚好释放锁,将 state 置为 0,但是后续还有等待的线程,所以当前线程需要乖乖的去排队等候。这就是公平锁的意思,如果是非公平锁,它会和等待队列中的头部节点去争抢这个锁,后续有介绍。最后还需要一个 CAS 操作,如果两条线程同时执行到这里,通过 CAS 操作只会有一条线程可以修改 state ,代表当前线程已经拿到锁了,方法返回继续后面的操作。

2)addWaiter

private Node addWaiter(Node mode) {
    // 将当前线程包装为Node
    Node node = new Node(Thread.currentThread(), mode);
    // 进入这个方法说明没有抢到锁,就需要判断等待队列是否有等待线程
    Node pred = tail;
    if (pred != null) {
        // 设置自己的前驱 为当前的队尾节点
        node.prev = pred;
        // 用CAS把自己设置为队尾, 如果成功后,tail == node了
        if (compareAndSetTail(pred, node)) {
            // 进到这里说明设置成功,当前node==tail, 将自己与之前的队尾相连,
            // 上面已经有 node.prev = pred
            // 加上下面这句,也就实现了和之前的尾节点双向连接了
            pred.next = node;
            // 线程入队了,可以返回了
            return node;
        }
    }
    enq(node);
    return node;
}

这个方法比较简单,就将当前线程插入到等待队列中,但是会有失败的情况,第一种 pred 为 null,说明当前可能是两条线程同时进来,另外一条拿到了锁,所以现在等待队列为空;第二种情况就是 CAS 操作更新 tail 时候失败了,也就是其他线程率先加入了队列。这两种情况都会进入自旋入队模式,看下面。

3)enq

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        // 这是队列为空的情况
        if (t == null) { // Must initialize
            // 创建一个空node对象,相当于初始化tail和head
            if (compareAndSetHead(new Node()))
                // 给后面用:这个时候head节点的waitStatus==0, 看new Node()构造方法就知道了
                // 然后再次循环进入else中
                tail = head;
        } else {
            // 这就是竞争的方式把当前线程添加队列中,直到添加成功
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

上面说过当前线程添加到等待队列失败会有两种情况:

  • 第一种就是等待队列为空的情况,进入 if 分支,初始化一个全为空的 Node,可以看 Node 的构造函数,给 head 和 tail 都赋此值。然后才可以进入 else 中,因为 else 才有返回值。
  • 第二种是上个方法中竞争入队失败,在这里继续竞争入队,直到有返回。

进入到 else 中后通过不断的循环尝试将当前线程的 Node 放入到等待队列中,返回的是前驱节点 Node,也就是入队前的 tail 节点,这就没什么好说的了。

4)acquireQueued

把当前线程加入到等待队列后,携带当前线程的 Node 进入到这个方法,这个方法才是最重要的。addWaiter 后说明当前线程已经进入阻塞队列,把线程阻塞、等待锁的相关操作都在这个方法中。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        //线程是否处于中断状态
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            // p == head 说明当前节点虽然进到了阻塞队列,但是是阻塞队列的第一个,因为它的前驱是head
            // 所以当前节点可以去试抢一下锁
            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);
    }
}

这里先说明一点线程挂起和中断的区别:线程挂起,也就是 park 是真正的将线程阻塞,挂起等待唤醒;线程中断,只是一个状态,不影响程序的执行,但是进入 wait、jion、sleep 这些方法会抛出异常。

p == head 时可以试着去抢锁,因为 head 有可能是刚刚初始化空 Node,里面没有线程所以可以试着去抢,如果抢到了返回 false 即可,这个后面再说;如果没有抢到,将会进入下面两个方法中:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // 前驱节点的 waitStatus == -1 ,说明前驱节点状态正常,当前线程需要挂起,直接可以返回true
    if (ws == Node.SIGNAL)
        return true;
    // 前驱节点 waitStatus大于0 ,之前说过,大于0 说明前驱节点取消了排队。
    if (ws > 0) {
        // 向前找到一个没有取消排队的节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 前驱节点的waitStatus不等于-1和1,那也就是只可能是0,-2,-3
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

shouldParkAfterFailedAcquire 这个方法检查当前线程是否需要挂起:

  • 当前驱节点的 waitStatus 为 -1 时,代表前驱节点的后续节点,也就是当前线程节点需要挂起,然后进入 parkAndCheckInterrupt 这个方法,这个方法就很简单了,挂起线程,返回当前线程是否被中断的状态。
  • 当前驱节点的 waitStatus 大于 0,说明前面的节点取消了排队,那么就不能等前面的节点来唤醒自己,所以向前一直找到可以唤醒自己的前驱节点即可。
  • 当前驱节点的 waitStatus 等于 0,说明是刚刚初始化的 Node,需要设置前驱节点的 waitStatus 为 -1。当下次循环进入时候,这个方法就会返回 True,然后把线程挂起,等待唤醒。

所以 acquireQueued 这个方法内的线程挂起相当于把线程挂起阻塞,并返回一个是否中断的状态,只有当线程拿到锁的时候才会退出循环。最后在回到 acquire 抢锁的这个方法:

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

tryAcquire 拿到锁返回 True ,不会进入后续的方法,就是代表拿到锁了;tryAcquire 没抢到锁则进入队列阻塞的这个方法,直到有人唤醒这条线程并且拿到了锁,才会返回 True,进入 selfInterrupt 这个方法:

Thread.currentThread().interrupt();//清除中断状态

2、解锁

解锁的代码比较简单,还是分开看:

1)tryRelease

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;
}
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 是否完全释放锁
    boolean free = false;
    // 其实就是重入的问题,如果c==0,也就是说没有嵌套锁了,可以释放了,否则还不能释放掉
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

tryRelease 就是尝试去解锁,如果第一次解锁后 state 为 0,说明解锁成功;如果不为 0 ,说明是嵌套锁,主程序还需要再调用 tryRelease,直到 state 为 0,完全释放锁为止。我们可以理解开始执行释放锁的线程一定是头部节点,所以查看头部节点是否为空,是否是刚刚初始化,不是的话,开始唤醒后续的线程。

2)unparkSuccessor

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    // 如果head节点当前waitStatus<0, 将其修改为0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // 下面的代码就是唤醒后继节点,但是有可能后继节点取消了等待(waitStatus==1)
    // 从队尾往前找,找到waitStatus<=0的所有节点中排在最前面的
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从后往前找,仔细看代码,不必担心中间有节点取消(waitStatus==1)的情况
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        // 唤醒线程
        LockSupport.unpark(s.thread);
}

唤醒线程就是直到找到后续没有取消等待的节点为止,将后续节点的线程取消挂起的状态即可。

四、NonfairSync(非公平锁)的实现原理

看懂公平锁的原理,非公平锁就很好理解了,它只是在抢锁时候进行了一些争抢的操作:

static final class NonfairSync extends Sync {
    final void lock() {
        // 和公平锁相比,这里会直接先进行一次CAS,成功就返回了
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}
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;
}

公平锁和非公平锁的区别就在:非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了;非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值