美文网首页
Java并发——Java中的锁

Java并发——Java中的锁

作者: Q南南南Q | 来源:发表于2017-07-31 21:23 被阅读0次

    一 Lock接口

    Lock是一个接口用来实现锁功能,它提供了和synchronized关键字相似的同步功能,只是在使用的时候需要显式调用。Lock的使用很简单,代码如下:

    Lock lock = new ReentrantLock();
    lock.lock();
    try {
        // dosomething            
    } finally {
        lock.unlock();
    }
    

    在finally中释放锁的目的是保证正在获取锁之后,最终能够释放锁。
    Lock接口提供的synchronized关键字所不具备的主要特性如下表所示:

    Lock接口提供的synchronized关键字不具备的主要特性

    Lock是一个接口,它定义了锁获取和释放的基本操作,Lock的API如表所示:

    Lock的API

    二 队列同步器

    队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

    同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。

    锁和同步器的关系是:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。

    1 队列同步器的接口与示例

    重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。

    • getState():获取当前同步状态。
    • setState(int newState):设置当前同步状态。
    • compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。

    同步器可重写的方法与描述如表所示:

    同步器可重写的方法

    同步器的模板方法如表所示:

    同步器的模板方法

    2 同步器的实现分析

    同步队列

    同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

    同步队列中的节点用来保存获取同步状态失败的线程应用、等待状态以及前驱和后继节点,节点的属性描述如下表所示:

    节点的属性描述

    节点是构成同步队列的基础,同步器拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点并加入该队列的尾部,同步队列的结构如下图所示:

    同步队列的基本结构

    设置尾节点
    同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。
    试想一下,当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update)

    节点加入同步队列

    设置首节点
    同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点,该过程如图所示:

    设置首节点

    设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能
    够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可。

    同步器的模板方法提供了三种不同的锁获取与释放方法:独占式、共享式以及独占式超时,下面分别讲述这三种方法获取与释放锁的过程。

    (1)独占式同步状态的获取与释放

    通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出,该方法代码如下所示。

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
    1. 调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态
    2. 如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部
    3. 调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。

    节点的构造和添加至尾部代码如下:

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 快速尝试在尾部添加
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            // 如果添加成功则返回
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // CAS添加失败,则调用enq()方法以死循环的方式保证节点的正确添加
        enq(node);
        return 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;
                }
            }
        }
    }
    

    节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程),如下代码所示:

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (; ; ) {
                // 获取node的前驱节点
                final Node p = node.predecessor();
                // 如果node的前驱节点是首节点,那么尝试获取锁
                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);
        }
    }
    

    只有前驱节点是头节点才能够尝试获取同步状态,这是为什么?原因有两个:

    • 头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
    • 维护同步队列的FIFO原则。
    节点自旋获取同步状态

    acquire()方法调用流程:

    独占式同步状态获取流程

    当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。该方法代码如下所示:

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                // 使用LockSupport来唤醒等待状态的线程
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    

    (2)共享式同步状态的获取与释放

    通过调用同步器的acquireShared(int arg)方法可以共享式地获取同步状态,该方法代码如下所示:

    public final void acquireShared(int arg) {
        // 尝试获取锁,若失败则调用doAcquireShared()
        if (tryAcquireShared(arg) < 0)
            // 自旋地获取锁
            doAcquireShared(arg);
    }
    
    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋地获取锁
            for (;;) {
                // 如果前驱节点是首节点,则尝试获取锁
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null;
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    

    与独占式一样,共享式获取也需要释放同步状态,通过调用releaseShared(int arg)方法可以释放同步状态,该方法代码如下所示:

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
    

    相关文章

      网友评论

          本文标题:Java并发——Java中的锁

          本文链接:https://www.haomeiwen.com/subject/sqmrlxtx.html