阅读 48

ReentrantLock学习总结

什么是ReentrantLock

ReentrantLock是继承Lock接口的一个在并发编程中常用的类,和synchronize一样,都是防止在并发场景下发生线程同步的问题。

使用方法

synchronize 加锁方法和 ReentrantLock 方法比较

写一个测试类,循环10次开辟10个线程,每个线程循环1000次,理论上total的出的结果是 10 * 10000 = 100000


private static int total = 0;

public static void main(String[] args) throws InterruptedException {
    lockTest();
}

public static void lockTest() throws InterruptedException {
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            for (int j = 0; j < 10000; j++) {
                total++;
            }
        }).start();
    }
    Thread.sleep(5000);
    System.out.println(total);
}
复制代码

可是输出结果却和理想的不一样,这就是发生了线程同步问题,以往解决这个问题,第一时间想到的是使用synchronize加锁。 现在将方法 lockTest 稍作修改:

public static void lockTest() throws InterruptedException {
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            for (int j = 0; j < 10000; j++) {
                synchronized (Object.class) {
                    total++;
                }
            }
        }).start();
    }
    Thread.sleep(5000);
    System.out.println(total);
}
复制代码

此时再输出 total 的值,就是 10 * 10000 = 100000 次。

除了使用synchronize还可以使用 ReentrantLock 来实现加锁的目的。 ReentrantLock相比 synchronize 是显性加锁方式,需要手动加锁和释放锁。 要使用 ReentrantLock 需要先创建一个 ReentrantLock 对象,使用ReentrantLock的lock()开启加锁,unlock()方法释放锁,在这两个方法中写需要加锁的代码即可。

private static int total = 0;
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
    lockTest();
}


public static void lockTest() throws InterruptedException {
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            for (int j = 0; j < 10000; j++) {
                lock.lock();
                total++;
                lock.unlock();
            }
        }).start();
    }
    Thread.sleep(5000);
    System.out.println(total);
}
复制代码

这样 lockTest() 方法打印出的 total 值也是 10000.

ReentrantLock 实现锁的方法是实现了AQS的框架。

AQS

AQS定义了一套多线程访问共享资源的同步框架,是一个依赖状态(State)的同步器。

三大核心原理

  1. 自旋
  2. LockSupport(可以阻塞线程和唤醒线程的工具)
  3. CAS

在 ReentrantLock 中还使用了双向链表,如果一个线程没有获取到锁,就回将这个线程放入队列中,ReentrantLock 类中有两个属性: head 指向链表头, tail 指向链表尾。

链表节点对象 Node 中几个属性:

  • prev:指向前一个Node节点
  • next:指向后一个Node节点
  • thread:记录当前线程
  • waitStatus:节点生命状态
    • CANCELLED = 1 代表出现异常,中断引起的,需要被废弃结束
    • SIGNAL = -1 可被唤醒的节点
    • CONDITION = -2 条件等待
    • PROPAGATE = -3 传播
    • 初始状态 = 0

ReentrantLock.lock() 方法

ReentrantLock 中有一个 Sync 的内部抽象类,继承了 AbstractQueuedSynchronizer(AQS) Sync 中有 lock()抽象方法,实现这个方法的有两个内部类 FairSync(公平锁实现)和 NofairSync(非公平锁实现)。

image.png

公平锁实现的lock() 方法

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

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt(); // 中断当前线程的便捷方法。

}
复制代码

tryAcquire()尝试获取锁,如获取失败返回 false ,继续走 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法,addWaiter(Node.EXCLUSIVE), arg)这个方法创建排队的Node节点。 acquireQueued()方法,获取已在队列中的线程。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 进行自旋
        for (;;) {
            final Node p = node.predecessor(); // 获取当前线程的上一个节点指向
            // 首先再次尝试在链表头的节点获取锁,这样操作的原因是当代码走到这里的时候,
            // 可能上一个线程已经释放了锁资源,这里可以直接在获取锁资源,也可以帮助GC
            if (p == head && tryAcquire(arg)) { 
                // p == head 判断当前线程的上一个节点是不是链表头
                // 如果是链表头,则进行获取锁,获取成功后,将 head 指向当前 Node 节点。
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted; // 返回false 当前线程无需阻塞。
            }
            // 当 head 不是指向 p 或 获取锁失败后
            // shouldParkAfterFailedAcquire()检查和更新未能获取的节点的状态。 
            // 如果线程应该阻塞,则返回 true。 
            // 这是所有获取循环中的主要信号控制。
            // parkAndCheckInterrupt()阻塞当前线程。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
复制代码

shouldParkAfterFailedAcquire()方法:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // ws:当前节点前一个节点的节点状态
    int ws = pred.waitStatus;
    // 如果 ws = -1 说明当先线程 可被唤醒
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
         // 将 pred 节点的状态设置为 -1 在下一次自旋中,就可返回true
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
复制代码

公平锁实现的unlock() 方法

// 如果当前线程是此锁的持有者,则持有计数递减。 
// 如果保持计数现在为零,则锁定被释放。 
// 如果当前线程不是此锁的持有者,则抛出IllegalMonitorStateException 。
public void unlock() {
    // 释放当前线程的锁资源
    sync.release(1);
}


public final boolean release(int arg) {
    // 
    if (tryRelease(arg)) {
        Node h = head;
        // 链表头部位 null 切链表头的节点状态不为 0 
        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;
        // 将持有锁的线程属性置为 null
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
     // unpark 的线程保留在后继节点中,通常只是下一个节点。
     // 但如果被取消或明显为空,则从尾部向后遍历以找到实际的未取消后继者。
    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);
}
复制代码

由于是多线程调用上述的几个方法,这些方法不是线性的进行的,大部分情况下是多个线程调用这些方法穿插进行。

文章分类
后端
文章标签