Java 学习:ReentrantLock

前言

Java 5.0 增加了一种新的加锁机制:ReentrantLock。ReentrantLock 并不是一种替代内置锁加锁的方法,而是当内置加锁机制不适用时,作为一种可选择的高级功能。

正题

在开始编写文章前,有几个问题需要思考一下:

  • 锁的类型
  • 公平锁工作原理
  • 非公平锁工作原理

在 ReentrantLock 的构造函数中提供了两种公平性选择:创建一个非公平的锁(默认)或者一个公平的锁。在公平的锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许“插队”:当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁。(在 Semaphore 中同样可以选择采用公平的或非公平的获取顺序。)非公平的 ReentrantLock 并不提倡“插队”行为,但无法防止某个线程在合适的时候进行“插队”。在公平的锁中,如果有另一个线程持有这个锁或者有其他线程在队列等待这个锁,那么新发出请求的线程将被放入队列中。在非公平的锁中,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中。

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

1. 锁的类型


2. 公平锁工作原理

在 ReentrantLock 使用了一个内部类来实现公平锁的加锁和解锁的功能。如下所示:

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

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

        ......
 }

当请求 lock 时,会传参数 1,是对该锁加 1。

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

首先调用公平锁的覆盖方法 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;
}

如果 A 线程在请求一个已经被其他线程持有的锁时,系统会创建一个锁节点 Node,把该节点放到锁请求链表上。在获取锁的步骤中,在 ReentrantLock 对象的内部使用了 state 来记录当前锁状态。

在 Node 节点里有几个状态标记:

static final int CANCELLED =  1;
static final int SIGNAL    = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;

volatile int waitStatus;

从线程获取锁到释放锁的这个过程中,用 waitStatus 来跟踪节点的状态变化。初始化节点 waitStatus 状态为 0。其节点 Node 包括以下状态之一:

  • SIGNAL:该节点的后继节点被阻塞,所以当前节点在释放或取消时必须唤醒等待该锁的节点线程。 为了避免竞争,获取方法必须首先表明他们需要一个信号,重试原子获取,在失败时阻塞。
  • CANCELLED:取消状态,当节点 Node 所在的线程被中断或者执行时间片用完被挂起了。当节点所属的线程处于该状态时不会再次阻塞。
  • CONDITION:该节点当前处于条件队列中。在它状态改变前被用作同步队列节点,此时的状态将被设置为0。
  • PROPAGGATE:共享锁允许多个线程同时获得该锁。 这在 doReleaseShared 中设置(仅用于头节点)以确保传播继续
2.2 AbstractQueuedSynchronizer##addWaite
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;
}

在每一个 Lock 对象里,都有一个保存锁请求的链表:

private transient volatile Node head;
private transient volatile Node tail;


在 addWaiter 方法上会把锁请求节点 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;
            }
        }
    }
}

通过对链表的复合操作来保证原子性:

headOffset = unsafe.objectFieldOffset
       (AbstractQueuedSynchronizer.class.getDeclaredField("head"));

private final boolean compareAndSetHead(Node update) {
    return unsafe.compareAndSwapObject(this, headOffset, null, update);
}

通过反射来获取 ReentrantLock 的 head 字段。

当某个线程释放锁时,会唤醒下一个获得锁的线程,在 acquireQueued 方法通过循环方式更新锁和头结点的状态。这是至关重要的一步:

  • 如果当前请求线程获得了锁,则更新头结点;
  • 设置当前节点的前置节点 pred 的 waitStatus 状态为 SIGNAL;
  • 如果当前线程请求的锁已被其他线程占用,则阻塞线程;
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);
    }
}

在公平锁类型下,获取锁的顺序是 FCFS,获得锁的顺序和请求的到来的顺序相同。

在 acquireQueued 方法里面做的几件事里,先看看设置前置节点状态过程:

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

这一步是设置 pred 节点的状态为 SIGNAL。每一个节点的 pred 都会在这一步设置为 SIGNAL 状态。

如果当前线程没有获得锁请求的锁,则阻塞:

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

主要调用 LockSupport.part() 方法来实现阻塞:

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}

最终调用 native 方式实现阻塞:

public native void park(boolean var1, long var2);
2.3 ReentrantLock##unLock

在 ReentrantLock 使用 unLock 方法实现解锁。如下所示:

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

最终还是调用内部类 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;
}

接下来看下是如何操作锁变量的。

2.4 Sync##tryRelease
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;
}

在 tryRelease 操作里主要的还是修改锁状态 state 的值。如果在同一个线程多次请求相同的锁,只是设置 state 的值。而且从上面可知:会为当前获取锁的节点设置锁的 waitStatus 为 -1。

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

在同一线程内释放了所有对同一个锁的所有请求,则会唤醒下一个等待该锁的线程:

public native void unpark(Object var1);

3. 非公平锁工作原理

在 ReentrantLock 使用了一个内部类来实现非公平锁的加锁和解锁的功能。如下所示:

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

当发出请求锁的同时该锁变为可用,线程跳过队列中的等待线程并获得锁。当锁被某个线程持有时,新发出请求的线程被放入等待队列中。

猜你喜欢

转载自blog.csdn.net/dilixinxixitong2009/article/details/80294768