Lock锁<一> _ 基础

目录

一、Lock接口

1. Lock使用

2. synchronized与Lock对比

3. Lock的API

二、AbstractQueuedSynchronizer

1. 同步队列

2. 独占式同步状态获取与释放

3. 共享式同步状态获取与释放

4. 独占式超时获取同步状态

5. 同步器的API

三、Condition接口

1. Object与Condition的等待/通知对比

2. 等待队列

3. 等待await()

​4. 通知signal()

四、参考资料


一、Lock接口

1. Lock使用

        并发包下java.util.concurrent.locks.Lock接口实现锁的功能,必须显示地获取和释放锁。如下图所示,是Lock的类图。

        如下代码是Lock的使用实例。注意lock.lock()必须在try块之外,否在获取锁失败出现异常,会导致锁无故释放。

// 重入锁
Lock lock = new ReentrantLock();

@Test
public void lockUseTest(){
    lock.lock();
    try {
        // 业务逻辑处理
        System.out.println("test use lock");
    } finally {
        lock.unlock();
    }
}

2. synchronized与Lock对比

内容synchronizedLock
层次关键字接口
实现隐式地释放和获取锁显示地释放和获取锁
释放锁

线程正常的执行完、JVM抛出异常

finally块释放锁,否则易死锁
获取锁只有释放锁才能获取锁视情况,共享锁/排他锁
锁状态无法判定可以判定
锁类型可重入 不可中断 非公平可重入 可判断 可公平(两者皆可)
性能少量同步大量同步

3. Lock的API

        Lock接口定义了锁获取和释放的基本操作,而获取和释放锁的实现原理都是队列同步器java.util.concurrent.locks.AbstractQueuedSynchronizer及其子类完成

二、AbstractQueuedSynchronizer

        队列同步器(java.util.concurrent.locks.AbstractQueuedSynchronizer)用来实现锁的获取和释放,底层使用了一个int型的变量state来表示同步状态(private volatile int state),通过内置的FIFO双向队列完成线程的排队工作

        同步器的使用主要是继承,子类继承它的抽象方法来管理同步状态state。其主要包括:同步队列、独占式同步状态获取与释放、共享式同步状态获取与释放、超时获取同步状态等同步器的核心结构。

1. 同步队列

同步队列的基本结构

        如上图所示,同步队列的基本结构是一个FIFO双向队列;同步失败的线程信息构造成Node节点。实现机制:获取同步状态失败(即:更新state值失败)时,线程进入等待状态,构造成Node节点加入到同步队列的尾部,tail节点指向尾部节点;当同步状态释放时,首节点的后继节点线程被唤醒,使其再次尝试获取同步状态。

        如上图所示,设置tail节点时,是基于CAS的compareAndSetTail(Node expect, Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点正式与之前的尾节点建立关联。注意:可能有很多失败线程,用CAS操作保证了添加尾节点是线程安全的

        如上图所示,设置head节时,是通过获取同步状态成功的线程完成释放后,唤醒head节点的后继节点,后继节点将会在获取同步状态成功时将自己设置为首节点。注意:首节点是获取同步状态成功的节点,只唤醒一个后继节点线程无需CAS操作

2. 独占式同步状态获取与释放

a. 获取同步状态

        以下代码,acquire(int arg)来获取同步状态,其主要完成tryAcquire(arg)获取同步状态、addWaiter()完成Node构造、acquireQueued()完成Node添加到同步队列尾部及同步队列自旋方式获取同步状态

/**
 * 独占式忽略中断的获取同步状态
 * 1. tryAcquire()成功后,其他线程进入同步队列中;
 * 2. tryAcquire()失败后,
 *               a. 当前线程被阻塞进入等待状态;
 *               b. 当前线程及等待状态等线程信息构造成节点Node,参考addWaiter();
 *               c. Node添加到同步队列尾部进行自旋(for(;;)死循环);
 */
public final void acquire(int arg) {
    // 尝试获取同步状态失败 + 当前线程添加到同步队列
    if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg))
        selfInterrupt();
}
/**
 * 当前线程及等待状态等线程信息构造成节点Node
 */
private AbstractQueuedSynchronizer.Node addWaiter(AbstractQueuedSynchronizer.Node mode) {
    AbstractQueuedSynchronizer.Node node = new AbstractQueuedSynchronizer.Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    AbstractQueuedSynchronizer.Node pred = tail;
    // 当前tail的前节点不为null
    if (pred != null) {
        node.prev = pred;
        // CAS操作设置tail,确保节点被线程安全添加
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 当前tail的前节点为null

    // 死循环(for(;;))确保节点正确添加
    enq(node);
    return node;
}

/**
 * Node添加到同步队列尾部进行自旋
 */
final boolean acquireQueued(final AbstractQueuedSynchronizer.Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 同步队列中每个节点自旋获取同步状态
        for (;;) {
            final AbstractQueuedSynchronizer.Node p = node.predecessor();
            /*
             * a. 当前节点的前节点是head;
             * b. head能够获取获取同步状态成功;
             * c. 当前节点设置为设置为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);
    }
}
节点自旋获取同步状态

        如上图所示,同步队列中的节点通过自旋方式获取同步状态,节点之间互不通信,只是简单判断自己的前节点是不是head节点即可。这是为什么?原因如下:

  • head是成功获取同步状态的节点,释放同步状态后,只会唤醒其后继节点,而后节点唤醒后检测自己的前节点是不是head
  • 维护同步队列的FIFO原则 
独占式同步状态获取流程

b.  释放同步状态

        如下代码所示,首节点调用release(int arg)释放同步状态之后,unparkSuccessor(Node node)方法使用LockSupport会唤醒处于等待状态的后继节点

/**
 * 释放同步状态
 * 注意:释放同步状态的节点是head节点
 * a. tryRelease(arg)返回true,表示释放成功;
 * b. 通过LockSupport.unpark()唤醒后继节点
 */
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        AbstractQueuedSynchronizer.Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

3. 共享式同步状态获取与释放

        共享式获取有多个线程同时获取到同步状态,这与独占式获取主要区别。如,写操作要求对资源的独占式访问,而读操作则为共享式访问。

a. 获取同步状态

        以下代码,acquireShared(int arg)来获取同步状态,注意:

  • tryAcquireShared(arg) >= 0时,表示获取同步状态
  • tryAcquireShared(arg) < 0时,表示获取失败,进入自旋不断获取同步状态,成功后退出自旋
/**
 * 共享式忽略中断的获取同步状态
 * a. tryAcquireShared(arg) >= 0时,表示获取同步状态
 * b. tryAcquireShared(arg) < 0时,表示获取失败
 */
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

/**
 * 自旋方式获取同步状态,tryAcquireShared(arg) >= 0时,退出自旋
 */
private void doAcquireShared(int arg) {
    final AbstractQueuedSynchronizer.Node node = addWaiter(AbstractQueuedSynchronizer.Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final AbstractQueuedSynchronizer.Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                // 成功获取同步状态,退出自旋
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

b. 释放同步状态

        与独占式的主要区别:由于多线程共享获取同步状态,tryReleaseShared()释放同步状态时通过循环和CAS保证线程安全释放同步状态

/**
 * 释放同步状态
 * 注意:tryReleaseShared(arg),由于共享多个线程获取同步状态,
 * 释放同步状态时通过循环和CAS保证线程安全释放同步状态
 */
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

4. 独占式超时获取同步状态

        如下代码所示,tryAcquireNanos(int arg, long nanosTimeout)超时时,返回false,即:获取同步状态失败。它是在响应中断acquireInterruptibly(int arg)方法上增加了超时限制,而响应中断则是在acquire(int arg)增加了中断响应。如下图所示是,独占式超时获取同步状态的流程。

/**
 * 独占式超时获取同步状态
 */
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
}

/**
 * 在nanosTimeout时间内还没有获取同步状态,则返回false,获取失败
 */
private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    final long deadline = System.nanoTime() + nanosTimeout;
    final AbstractQueuedSynchronizer.Node node = addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final AbstractQueuedSynchronizer.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)
                // 该线程继续等待状态nanosTimeout
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
独占式超时获取同步状态的流程

5. 同步器的API

        如下表所示,同步器的API主要分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态、查询同步队列中的等待线程情况等API。

三、Condition接口

        Condition接口的作用:配合Lock接口进行线程间之间的通信(等待/通知),类似于Object类中的wait()和notify()的等待/通知机制。Condition对象需要Lock,即:lock.newCondition()。

        Condition接口实现类是ConditionObject,它是AbstractQueuedLongSynchronizer内部类,主要包括:等待队列、等待、通知。

1. Object与Condition的等待/通知对比

        在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而Lock拥有一个同步队列和多个等待队列

2. 等待队列

        如下图所示,等待队列是FIFO单向队列。当前线程调用condition.await()后,当前线程改为等待状态并释放同步状态state,随后该线程构造成新的Node节点加入到等待队列尾部;当其他线程调用condition.signal()后唤醒等待的线程,并添加到同步队列。

        Condition结构包含了:一个等待队列、首节点(firstWaiter)、尾节点(lastWaiter)。但是整个Lock有多个等待队列,原因是:锁可能具有重入性、共享性

        设置lastWaiter时,并没有使用CAS保证,原因是调用await()方式,实际上该线程已经获取了锁,由锁来保证线程安全。

3. 等待await()

        如下图所示,调用condition.await()后,当前线程(head节点)释放锁,同时进入等待状态、构造新Node加入到等待队列中。从队列看:

  • 同步队列:head节点释放锁,进入等待状态,移除head;唤醒后继节点
  • 等待队列:构造新Node加入到等待队列并设置lastWaiter

4. 通知signal()

        如下图所示,调用condition.signal()后,将会唤醒在等待队列中等待时间最长的节点(首节点),首节点移动到同步队列的尾部。当成功获取同步状态之后,被唤醒的线程将从先前调用的await()方法返回,继续执行后续代码。

四、参考资料

深入理解AQS(AbstractQueuedSynchronizer)_晨初听雨的博客-CSDN博客_深入理解aqs

Java锁--Lock实现原理(底层实现)_静_默的博客-CSDN博客_lock底层原理 

详解synchronized与Lock的区别与使用_淳安郭富城的博客-CSDN博客_synchronized和lock的区别 

Java线程<三> _ 线程间通信_爱我所爱0505的博客-CSDN博客 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值