ReentrantLock

构造方法

  1. 内部维护了**Sync**内部类,Sync继承自**AQS**,ReentrantLock通过Sync来实现同步工具类
  2. 在无参构造时,默认为的Sync实现类为 **NonfairSync** 即默认为非公平锁实现

image.png

非公平锁获取锁

没有竞争时

  • 在调用 **reentrantLock.lock()** 时,本质是调用的 **Sync** 中的**lock**方法
  • 抢占锁的过程就是通过**compareAndSetState(0, 1)**
  • 在没有竞争时第一个线程来抢占锁,此时锁的状态为0,因此该线程可以成功抢占到锁
  • 此时该线程会将该锁的持有线程设置为自己

image.png

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    @ReservedStackAccess
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

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

当出现锁竞争时

tryAcquire
  1. **Sync**中的lock方法中,如果 CAS获取锁失败,则会进入到**acquire()**中,Sync是继承自AQS的,acquire是AQS提供的方法。
  2. **tryAcquire()**方法中会**继续尝试获取锁**,在第一次获取锁的CAS操作失败可能有两种原因
  3. 第一种原因,该锁在当时**被其他线程所占有**,但在现在**可能该线程已经将锁释放**,此时会再次尝试CAS获取锁,如果获取锁成功则锁的持有线程设置为自己
  4. 第二种原因,该锁被自己线程占有,所以state不为0,此时需要判断持有锁的线程是否是自己,如果是自己需要将state+1
@ReservedStackAccess
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;
}

addWaiter和enq
  • **addWaiter**的方法作用是在当前线程争抢锁失败后,将该**线程包装成一个Node节点**
  • 并且将该Node节点通过**CAS**的方式添加到CLH**队列的尾部**
  • 在多线程环境下添加队列可能会出现竞争情况,如果一次CAS添加没有成功,就会调用进入到enq方法
  • enq方法通过**自旋+CAS**的方式,确保该线程所在的Node节点成功添加到CLH队列尾部
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;
}
  • enq方法中会自旋操作,在第一次进入enq方法时,head和tail都指向null
  • 此时会先调用 compareAndSetHead(new Node()) ,此时head节点会指向哨兵节点,同时将 tail指向哨兵节点
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;
            }
        }
    }
}

image.png
image.png

acquireQueued
  • 进入**acquireQueued**方法后会**自旋**获取锁,当且**仅当该节点为第一个节点**时才能尝试获取锁
  • 如果该**节点不是第一个节点或没有争抢到锁**,需要调用 **shouldParkAfterFailedAcquire** 方法,找到**真正的首节点**后,将其改为 **SIGNAL**状态,然后该线程会被**LockSupport.park(this)**挂起

image.png

  • 线程由挂起状态被唤醒,因为是使用的 **Park**挂起,此时该线程仍存在中断标记,但此时需要使用

**Thread.interrupted()** 来清除中断标记,否则在自旋获取锁的过程中,如果该线程在 tryAcquire()中**争抢锁失败**,此时再次被 **LockSupport.park(this)**挂起时,会因为该线程**已经存在中断标记而无法正常挂起**

  • 这样会导致进入**死循环空转**的情况,可能会出现CPU100%的情况,因此需要用 **interrupted**变量记录下线程被打断,在**acquire**方法中根据此处的返回值,将**中断标记重新赋值**

image.png

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);
    }
}
//该方法的返回值为true  说明该线程应当被park挂起
//否则不应当被挂起 应当继续自旋获取锁
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //获取前置节点的状态
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        //如果前置节点的状态已经挂起 则不需要进行处理
        return true;
    if (ws > 0) {
        //线程的状态仅有cancel是大于0的
        //会一直向前寻找到正常状态的Node节点
        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.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

释放锁

release
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

tryRelease
  1. 先对基础信息进行校验,只有**持有锁的线程**才能对锁做锁释放操作
  2. 因为锁是可重入的,state为**锁的重入次数**,当且仅当state为0后,才会真正进行锁的释放操作
  3. 锁的释放操作会先将锁的持有线程设置为**null**,然后再将锁的state设置为0
  4. 状态设置和线程变量设置顺序不能交换,因为state有**Volatile**修饰,可以保证state修改前语句的可见性
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;
}

唤醒线程
  1. 在唤醒线程时会唤醒**哨兵节点的下一个节点**,如果头节点的下一个节点**为空或是过期节点**,则需要从后往前进行查询,最靠近哨兵节点的挂起节点。
  2. **addWait()**时,尾插法插入,先操作的是**prev**指针,后操作上一个节点的**next**节点,可能由于**cpu时间片**的问题,节点已经插入,prev已经修改了指向,但next节点还没有修改指向
  3. 当节点取消时**tryRelease()**时,先修改的是**prev**指针,因此需要以prev指针为准
  4. 从而唤醒线程需要从tail节点往head节点进行查询,即**从后往前进行查询**
private void unparkSuccessor(Node node) {
        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.
         */
        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);
    }

锁竞争
  1. **ReentrantLock**中,因为是**独占锁**,只有该节点的前驱节点是**哨兵节点**时才会被唤醒,此时会在 **acquireQueued** 方法中自旋获取锁
  2. 如果此时成功通过**tryAcquire**获取到锁,则会将当前**线程所在的node节点**代替原先的**哨兵节点**,同时将原来的哨兵节点从CLH链表中删除
  3. 如果此时有其他的线程也在通过**tryAcquire**一起竞争锁,并且该线程没有成功获取锁,则会继续park挂起该线程,等待持有锁的线程释放锁
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. 加锁时,在调用**tryAcquire**时,如果判断出持有锁的线程是当前线程,会将**state++**
  2. 释放锁时,在调用**tryRelease**时,如果判断出持有锁的线程是当前线程,会将**state--**,如果此时**state为0**,才会真正释放锁

非公平锁与公平锁

ReentrantLock默认

  • **ReentrantLock**的默认实现中,是使用的**非公平锁**(NofairSync)实现,因为**非公平锁的效率比公平锁更高**
  • 因为在一个线程**释放锁时**,才会通过**unpark**的方式唤醒CLH队列中**哨兵节点后的第一个节点****unpark**操作需要切换到内核态,是非常耗时的操作
  • 在唤醒线程的过程中,如果此时有其他的线程也来争抢锁,则可以直接获取到锁
  • 如果是**公平锁**实现,只能由线程释放锁后唤醒CLH队列的节点争抢锁,则都需要**等待unpark节点**的时间,因此非公平锁的效率要高于公平锁

非公平锁

  • 在非公平锁中,任意线程在调用**ReentrantLock.lock()**时,都会**先尝试CAS获取锁**,无论CLH队列中是否有元素正在等待
  • 因此在CLH队列中**被唤醒的元素**,在尝试tryAcquire获取锁时,仍然会**有其他的CLH队列以外的其他线程**正在尝试获取锁
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

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

公平锁

  • **公平锁**的实现中,线程在调用ReentrantLock.lock()时,**不会先调用CAS尝试获取锁**,都会**调用acquire获取锁**
  • 在FairSync的**tryAcquire**实现中,在state为0时,此时没有线程占有锁,**不会直接通过CAS**修改state获取锁
  • 而是会先通过 **hasQueuedPredecessors** 判断CLH队列中是否含有元素,如果队列中仍然存在元素,则不会通过CAS获取锁
  • **tryAcquire**争抢锁失败后,后续会通过**addWaiter****acquireQueued** 方法加入到CLH队列中
  • 并且从CLH队列中唤醒的元素,没有外界的线程与其争抢,因此保证了线程获取**锁的顺序性**,体现了公平锁的特点
final void lock() {
    acquire(1);
}

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

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

条件变量原理

每一个条件变量对应着一个ConditionObject,内部维护着一个双向队列

await

  1. 当线程0持有锁时,并且调用await方法,此时会在ConditionObject中创建出Node节点,并且加入到双向链表中

image.png

  1. 此时要调用AQS中的 fullyRelease 流程,释放掉所有的锁,因为该线程持有的可能是可重入锁
  2. 当线程0的锁释放掉后,此时会唤醒队列中的第一个节点,同时与外部的线程进行锁竞争。

signal

  1. 此时线程1要来唤醒线程0,则会取出ConditionObject中的第一个节点
  2. 然后将该节点加入到队列的末尾中,同时将该节点的状态改为0,将前驱节点的状态改为-1

image.png
image.png

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值