深入学习Lock锁(1)——队列同步器

   对锁以及Lock接口的简单介绍参考https://my.oschina.net/u/3352298/blog/1802403

1. 队列同步器

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

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

    同步器的子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、 ReentrantReadWriteLock和CountDownLatch等)。

    同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节同步器面向的是锁的实现者, 它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域,也就是说同步器是锁或某个同步组件开发者所需要实现的基础插件,而程序员作为锁和同步组件的使用者只需要调用开发者开发的锁或同步组件进行使用即可

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

    同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些 模板方法将会调用使用者重写的方法。

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

    (1)getState(),获取当前同步状态。

    (2)setState(int newState),设置当前同步状态。

    (3)compareAndSetState(int expect,int update),使用CAS设置当前状态,该方法能够保证状态设置的原子性。

    同步器可重写的方法有:

    (1)protected boolean tryAcquire(int arg):独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再使用CAS设置同步状态。

    (2)protected boolean tryRelease(int arg):独占式释放同步状态,等待同步状态的线程将有机会获取同步状态。

    (3)protected int tryAcquireShared(int arg):共享式获取同步状态,返回大于等与0的值则表示获取成功,否则获取失败

    (4)protected boolean tryReleaseShared(int arg):共享式释放同步状态。

    (5)protected boolean isHeldExclusively():当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占。

    同步器提供的模板方法具体有三类,独占式释放与获取同步状态,共享式获取与释放同步状态,以及查询同步队列中等待线程的状态,我们主要使用这些模板方法来实现自定义的同步组件。所谓独占式也就是同一时刻,只能有一个线程获取或释放同步状态,共享式则相反。同步器提供的模板方法有:

    (1)public final void acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则会返回,否则当前线程会进入同步队列等待,该方法需要调用重写的tryAcquire(int arg)方法

    (2) public final void acquireInterruptibly(int arg) throws InterruptedException:独占式获取同步状态,如果当前线程获取同步状态成功,则会返回,否则当前线程会进入同步队列等待,但同时该方法也会响应中断,如果线程在同步队列中被中断,则该方法会抛出InterruptedException并返回。

    (3)public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException:在acquireInterruptibly(int arg)增加了超时限制,超时未获得同步状态返回false,否则返回true

    (4)public final void acquireShared(int arg):共享式获取同步状态,如果未获取到则进入同步队列等待,与独占式不同的是,共享式可以同时有多个线程获取到同步状态。

    (5)public final void acquireSharedInterruptibly(int arg):共享式获取同步状态,如果当前线程获取同步状态成功,则会返回,否则当前线程会进入同步队列等待,但同时该方法也会响应中断,如果线程在同步队列中被中断,则该方法会抛出InterruptedException并返回。

    (6)public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
            throws InterruptedException:在acquireSharedInterruptibly(int arg)基础上增加了超时限制,超时未获得同步状态返回false,否则返回true

    (7) public final boolean release(int arg):独占式释放同步状态,该方法会在释放同步状态之后,唤醒同步队列中第一个节点中的线程。

    (8)public final boolean releaseShared(int arg):共享式释放同步状态。

    (9)public final Collection<Thread> getQueuedThreads():获取在同步队列上等待的线程集合。

    依据一个独占锁的实现来了解同步器工作原理,独占锁,也就是同一时刻只能有一个线程获取到锁,而其他线程只能在同步队列中等待,只有持有锁的线程释放锁,后继的线程才能获取锁,相当于synchronized锁和ReentrantLock锁。共享锁的实现可以参考Semaphore线程工具类的内部实现。我们依据ReentrantLock锁的内部同步器实现来作为示例:

class Mutex implements Lock {
	// 静态内部类,自定义同步器
	private static class Sync extends AbstractQueuedSynchronizer {
		// 是否处于占用状态,或者说是否当前线程将要获得的锁是否被其他线程持有,
		//是则将状态置为1
		protected boolean isHeldExclusively() {
			return getState() == 1;
		}

		/* 如果线程尝试获取锁时,获取的锁未被其他线程持有(即同步器状态为0时),
		 * 则获取锁并返回true,
		 * 否则(获取的锁被其他线程持有,即同步器状态为1时)返回false
		 */
		public boolean tryAcquire(int acquires) {
			if (compareAndSetState(0, 1)) {
				setExclusiveOwnerThread(Thread.currentThread());
				return true;
			}
			return false;
		}
		//自定义方法,用该方法来加锁
		final void lock() {
			/* 如果线程尝试获取锁时,获取的锁未被其他线程持有(即同步器状态为0时),
			 * 则获取锁并返回,否则(获取的锁被其他线程持有,即同步器状态为1时)
			 * 将一直阻塞,直到获取锁
			 */
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
		// 释放锁,将同步器状态设置为0
		protected boolean tryRelease(int releases) {
			if (getState() == 0)
				throw new IllegalMonitorStateException();
			setExclusiveOwnerThread(null);//设置当前持有同步器的线程对象
			setState(0);
			return true;
		}

		// 返回一个Condition,每个condition都包含了一个condition队列
		Condition newCondition() {
			return new ConditionObject();
		}
	}

	// 仅需要将操作代理到Sync上即可
	private final Sync sync = new Sync();

	public void lock() {
		sync.lock();
	}

	public boolean tryLock() {
		return sync.tryAcquire(1);
	}

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

	public Condition newCondition() {
		return sync.newCondition();
	}

	public boolean isLocked() {
		return sync.isHeldExclusively();
	}

	public boolean hasQueuedThreads() {
		return sync.hasQueuedThreads();
	}

	public void lockInterruptibly() throws InterruptedException {
		sync.acquireInterruptibly(1);
	}

	public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
		return sync.tryAcquireNanos(1, unit.toNanos(timeout));
	}
}

    到了这里,我们应该就能明白一个队列同步器的简单工作原理。以上面的代码为例,独占锁Mutex是一个自定义同步组件,它在同一时刻只允许一个线程占有锁。Mutex中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。在tryAcquire(int acquires)方法中,如果经过CAS设置成功(同步状态设置为1),则代表获 取了同步状态,而在tryRelease(int releases)方法中只是将同步状态重置为0。用户使用Mutex时 并不会直接和内部同步器的实现打交道,而是调用Mutex提供的方法,在Mutex的实现中,以获取锁的lock()方法为例,只需要在方法实现中调用同步器的模板方法lock()即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现 一个可靠自定义同步组件的门槛。

    如果对”同步队列器中的获取与释放同步状态”中的同步状态难以理解,你可以理解为是否当前同步器正在被某一个线程占用的状态,也就是是否有某个线程正持有通过该同步器对象实现的锁,用0和1表示。

1.2 队列同步器的实现分析

1.同步队列

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

static final class Node {
		
        /* 表明一个节点正处于共享模式 */
        static final Node SHARED = new Node();
        
        /*表明一个节点正处于独占模式 */
        static final Node EXCLUSIVE = null;
        
        /* 这个节点状态值表示这个节点的线程等待超时或被中断,需要从同步队列中删除该节点,
         * 节点进入该状态后不会变化,并且会释放同步状态,使得后续节点可以运行
         * 也就是说相当于应用中一个线程在同步队列中或正在运行的过程中被异常中断或
         * 等待超时都会释放锁,并返回,将锁交给其他线程
         */
        static final int CANCELLED =  1;
        
        /* 这个节点状态值表示这个节点的线程释放了同步状态或者被取消,则会通知后面的节点,
         * 也就是相当于使用中线程的wait()/signal()机制
         */
        static final int SIGNAL    = -1;
        
        /* 相当于使用Lock锁的Condition条件对象,表示该线程是否是通过Condition条件对象
         * 来实现阻塞的
         */
        static final int CONDITION = -2;
        /*
         * 表示下一次共享式同步状态获取将会无条件传播下去
         */
        static final int PROPAGATE = -3;

        /*
         * waitStatus取以上CANCELLED,SIGNAL,CONDITION,PROPAGATE值,或初始值0
         */
        volatile int waitStatus;

        /*
         * 当前节点的前一个节点
         */
        volatile Node prev;

        /*
         * 连接到当前节点的下一个节点
         */
        volatile Node next;

        /*
         * 连接到当前节点被持有的线程
         */
        volatile Thread thread;

        /*
         * 是等待队列的后继节点,连接到下一个在同一个Condition条件对象上等待的节点。
         * 或者当前节点是共享式节点,则该变量是特殊值Node类中的常量SHARED,比如在Semaphore线程并发类中就是使用该 
         * 节点类型
         */
        Node nextWaiter;

        /*
         * Returns true if node is waiting in shared mode.
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
        Node() {    // Used to establish initial head or SHARED marker
        }
        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

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

    同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。 试想一下,当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转 而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此 同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式 与之前的尾节点建立关联。

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

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

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

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

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

    其主要逻辑是:首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法 保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式 Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node) 方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。

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

    上述代码通过使用compareAndSetTail(Node expect,Node update)方法来确保节点能够被线程安全添加。试想一下:如果使用一个普通的LinkedList来维护节点之间的关系,那么当一个线 程获取了同步状态,而其他多个线程由于调用tryAcquire(int arg)方法获取同步状态失败而并发 地被添加到LinkedList时,LinkedList将难以保证Node的正确添加,最终的结果可能是节点的数量有偏差,而且顺序也是混乱的。

在enq(final Node node)方法中,同步器通过“死循环”来保证节点的正确添加,在“死循 环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线 程不断地尝试设置。可以看出,enq(final Node node)方法将并发添加节点的请求通过CAS变 得“串行化”了。

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

    节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程),在acquireQueued(final Node node,int arg)方法中,当前线程在“死循环”中尝试获取同步状 态,而只有前驱节点是头节点才能够尝试获取同步状态,因为头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点,而且需要维护同步队列的FIFO原则。

    图解独占式获取同步状态流程:

    在图中,前驱节点为头节点且能够获取同步状态的判断条件和线程进入等待状态是获取同步状态的自旋过程。当同步状态获取成功之后,当前线程从acquire(int arg)方法返回,如果对于锁这种并发组件而言,代表着当前线程获取了锁。

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

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

    该方法执行时,会唤醒头节点的后继节点线程,unparkSuccessor(Node node)方法使用 LockSupport来唤醒处于等待状态的线程。

    (3)总结:在获取同步状态时,同步器维 护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列 (或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

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

    (1)共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状 态。以文件的读写为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问,共享式同步队列的使用与实现具体可以参考Semaphore。

    public final void acquireShared(int arg) {
        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; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

    通过调用同步器的acquireShared(int arg)方法可以共享式地获取同步状态,在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状 态,tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是 tryAcquireShared(int arg)方法返回值大于等于0。可以看到,在doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示 该次获取同步状态成功并从自旋过程中退出。

    (2)与独占式一样,共享式获取也需要释放同步状态,通过调用releaseShared(int arg)方法可以 释放同步状态,该方法在释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够支持多个线 程同时访问的并发组件(比如Semaphore),它和独占式主要区别在于tryReleaseShared(int arg) 方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的,因为 释放同步状态的操作会同时来自多个线程。

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

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

    通过调用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false。超时获取同步状态过程可以被视作响应中断获取同步状态过程的“增强版”, doAcquireNanos(int arg,long nanosTimeout)方法在支持响应中断的基础上,增加了超时获取的特性。针对超时获取,主要需要计算出需要睡眠的时间间隔nanosTimeout,为了防止过早通知, nanosTimeout计算公式为:nanosTimeout-=now-lastTime,其中now为当前唤醒时间,lastTime为上 次唤醒时间,如果nanosTimeout大于0则表示超时时间未到,需要继续睡眠nanosTimeout纳秒, 反之,表示已经超时。

    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final 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)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

    该方法在自旋过程中,当节点的前驱节点为头节点时尝试获取同步状态,如果获取成功 则从该方法返回,这个过程和独占式同步获取的过程类似,但是在同步状态获取失败的处理上有所不同。如果当前线程获取同步状态失败,则判断是否超时(nanosTimeout小于等于0表示 已经超时),如果没有超时,重新计算超时间隔nanosTimeout,然后使当前线程等待 nanosTimeout纳秒(当已到设置的超时时间,该线程会从LockSupport.parkNanos(Object blocker,long nanos)方法返回)。

    注意:如果nanosTimeout小于等于spinForTimeoutThreshold(1000纳秒)时,将不会使该线程进行 超时等待,而是进入快速的自旋过程。原因在于,非常短的超时等待无法做到十分精确,如果 这时再进行超时等待,相反会让nanosTimeout的超时从整体上表现得反而不精确。因此,在超 时非常短的场景下,同步器会进入无条件的快速自旋。

    独占式超时获取同步状态方法流程图

5.总结

    同步队列器实际上对于所有的同步组件,如ReentrantLock,Semaphore等都通过该组件进行实现。其获取同步状态,在独占式情况下可以视为获取锁(如ReentrantLock),在共享式情况下可以视为获取访问同步资源的权限或是条件(如Semaphore)。所以,同步器的使用不仅仅是用于Lock锁的实现,Java中提供的许多同步组件都通过同步器实现,我们甚至可以根据需要,重写队列同步器的方法来实现自己所需要的同步组件。

    

猜你喜欢

转载自my.oschina.net/u/3352298/blog/1818221