队列同步器AbstractQueuedSynchronizer简介

Lock接口

锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时 访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。在Lock接 口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java SE 5之后,并发包中新增 了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功 能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提
供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以 及超时获取锁等多种synchronized关键字所不具备的同步特性。
使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先
获取再释放。当然,这种方式简化了同步的管理,可是扩展性没有显示的锁获取和释放来的 好。例如,针对一个场景,手把手进行锁获取和释放,先获得锁A,然后再获取锁B,当锁B获得 后,释放锁A同时获取锁C,当锁C获得后,再释放B同时获取锁D,以此类推。这种场景下, synchronized关键字就不那么容易实现了,而使用Lock却容易许多。
                                                      Lock的使用也很简单,代码清单5-1是Lock的使用的方式。
 

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

在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。
不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,
异常抛出的同时,也会导致锁无故释放。
Lock接口提供的synchronized关键字所不具备的主要特性如表5-1所示。
                                        表5-1 Lock接口提供的synchronized关键字不具备的主要特性


 

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

 


Lock接口的实现基本都是通过聚合了一个同步器的子类来完成线程访问控制的。

 队列同步器

队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组 件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获 取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状 态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3 个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操
作,因为它们能够保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部
类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来
供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获 取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、 ReentrantReadWriteLock和CountDownLatch等)。
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步
器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交
互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,
它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同
步器很好地隔离了使用者和实现者所需关注的领域。

 队列同步器的接口与示例
 

同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的
方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些
模板方法将会调用使用者重写的方法。
重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。
·getState():获取当前同步状态。
·setState(int newState):设置当前同步状态。
·compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态
设置的原子性。
同步器可重写的方法与描述如表5-3所示。
                                                           表5-3 同步器可重写的方法


实现自定义同步组件时,将会调用同步器提供的模板方法,这些(部分)模板方法与描述
如表5-4所示。
                                                                      表5-4 同步器提供的模板方法


同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放
同步状态和查询同步队列中的等待线程情况。自定义同步组件将使用同步器提供的模板方法
来实现自己的同步语义。
只有掌握了同步器的工作原理才能更加深入地理解并发包中其他的并发组件,所以下面
通过一个独占锁的示例来深入了解一下同步器的工作原理。
顾名思义,独占锁就是在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能 处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁,如代码清单52所示。

 

代码清单5-2 Mutex.java

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

class Mutex implements Lock {
	// 静态内部类,自定义同步器
	private static class Sync extends AbstractQueuedSynchronizer {
		// 是否处于占用状态
		protected boolean isHeldExclusively() {
			return getState() == 1;
		}

		// 当状态为0的时候获取锁
		public boolean tryAcquire(int acquires) {
			if (compareAndSetState(0, 1)) {
				setExclusiveOwnerThread(Thread.currentThread());
				return true;
			}
			return false;
		}

		// 释放锁,将状态设置为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.acquire(1);
	}

	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()方法为例,只需要在方法实现中调用同步器的模板方法acquire(int args)即可,当
前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现
一个可靠自定义同步组件的门槛。

队列同步器的实现分析

接下来将从实现角度分析同步器是如何完成线程同步的,主要包括:同步队列、独占式同
步状态获取与释放、共享式同步状态获取与释放以及超时获取同步状态等同步器的核心数据
结构与模板方法。
1.同步队列
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取 同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其
加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再
次尝试获取同步状态。
同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和 后继节点,节点的属性类型与名称以及描述如表5-5所示。
                                                    表5-5 节点的属性类型与名称以及描述

 

节点是构成同步队列(等待队列,在5.6节中将会介绍)的基础,同步器拥有首节点(head) 和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部,同步队列的 基本结构如图5-1所示。

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

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

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

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

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

    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)方法,使得该 节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的
唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
下面分析一下相关工作。首先是节点的构造以及加入同步队列,如代码清单5-4所示。
 

    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变 得“串行化”了。
节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自
省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这 个自旋过程中(并会阻塞节点的线程),如代码清单5-5所示。

代码清单5-5 同步器的acquireQueued方法
 

 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原则。该方法中,节点自旋获取同步状态的行为如图5-4所示。

在图5-4中,由于非首节点线程前驱节点出队或者被中断而从等待状态返回,随后检查自
己的前驱是否是头节点,如果是则尝试获取同步状态。可以看到节点和节点之间在循环检查
的过程中基本不相互通信,而是简单地判断自己的前驱是否为头节点,这样就使得节点的释 放规则符合FIFO,并且也便于对过早通知的处理(过早通知是指前驱节点不是头节点的线程
由于中断而被唤醒)。
独占式同步状态获取流程,也就是acquire(int arg)方法调用流程,如图5-5所示。

在图5-5中,前驱节点为头节点且能够获取同步状态的判断条件和线程进入等待状态是获 取同步状态的自旋过程。当同步状态获取成功之后,当前线程从acquire(int arg)方法返回,如果
对于锁这种并发组件而言,代表着当前线程获取了锁。
当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能 够继续获取同步状态。通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释
放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。该方法代 码如代码清单5-6所示。
代码清单5-6 同步器的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;
    }

该方法执行时,会唤醒头节点的后继节点线程,unparkSuccessor(Node node)方法使用 LockSupport(在后面的章节会专门介绍)来唤醒处于等待状态的线程。
分析了独占式同步状态获取和释放过程后,适当做个总结:在获取同步状态时,同步器维
护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列
(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步 器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
 

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

 

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状
态。以文件的读写为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操
作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享 式访问,两种不同的访问模式在同一时刻对文件或资源的访问情况,如图5-6所示。

在图5-6中,左半部分,共享式访问资源时,其他共享式的访问均被允许,而独占式访问被
阻塞,右半部分是独占式访问资源时,同一时刻其他访问均被阻塞。
通过调用同步器的acquireShared(int arg)方法可以共享式地获取同步状态,该方法代码如 代码清单5-7所示。

代码清单5-7 同步器的acquireShared和doAcquireShared方法
 

 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)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状 态,tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同
步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是 tryAcquireShared(int arg)方法返回值大于等于0。可以看到,在doAcquireShared(int arg)方法的自 旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示
该次获取同步状态成功并从自旋过程中退出。
与独占式一样,共享式获取也需要释放同步状态,通过调用releaseShared(int arg)方法可以 释放同步状态,该方法代码如代码清单5-8所示。

代码清单5-8 同步器的releaseShared方法

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

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

独占式超时获取同步状态
 

通过调用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状 态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false。该 方法提供了传统Java同步操作(比如synchronized关键字)所不具备的特性。
在分析该方法的实现前,先介绍一下响应中断的同步状态获取过程。在Java 5之前,当一 个线程获取不到锁而被阻塞在synchronized之外时,对该线程进行中断操作,此时该线程的中 断标志位会被修改,但线程依旧会阻塞在synchronized上,等待着获取锁。在Java 5中,同步器 提供了acquireInterruptibly(int arg)方法,这个方法在等待获取同步状态时,如果当前线程被中 断,会立刻返回,并抛出InterruptedException。
超时获取同步状态过程可以被视作响应中断获取同步状态过程的“增强版”, doAcquireNanos(int arg,long nanosTimeout)方法在支持响应中断的基础上,增加了超时获取的 特性。针对超时获取,主要需要计算出需要睡眠的时间间隔nanosTimeout,为了防止过早通知, nanosTimeout计算公式为:nanosTimeout-=now-lastTime,其中now为当前唤醒时间,lastTime为上 次唤醒时间,如果nanosTimeout大于0则表示超时时间未到,需要继续睡眠nanosTimeout纳秒, 反之,表示已经超时,该方法代码如代码清单5-9所示。


代码清单5-9 同步器的doAcquireNanos方法

    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }



    //计算时间,当前时间now减去睡眠之前的时间lastTime得到已经睡眠        
    //的时间delta,然后被原有超时时间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-7所示。
从图5-7中可以看出,独占式超时获取同步状态doAcquireNanos(int arg,long nanosTimeout) 和独占式获取同步状态acquire(int args)在流程上非常相似,其主要区别在于未获取到同步状 态时的处理逻辑。acquire(int args)在未获取到同步状态时,将会使当前线程一直处于等待状 态,而doAcquireNanos(int arg,long nanosTimeout)会使当前线程等待nanosTimeout纳秒,如果当 前线程在nanosTimeout纳秒内没有获取到同步状态,将会从等待逻辑中自动返回。

.自定义同步组件——TwinsLock
 

在前面的章节中,对同步器AbstractQueuedSynchronizer进行了实现层面的分析,本节通过
编写一个自定义同步组件来加深对同步器的理解。
设计一个同步工具:该工具在同一时刻,只允许至多两个线程同时访问,超过两个线程的 访问将被阻塞,我们将这个同步工具命名为TwinsLock。
首先,确定访问模式。TwinsLock能够在同一时刻支持多个线程的访问,这显然是共享式 访问,因此,需要使用同步器提供的acquireShared(int args)方法等和Shared相关的方法,这就要 求TwinsLock必须重写tryAcquireShared(int args)方法和tryReleaseShared(int args)方法,这样才能
保证同步器的共享式同步状态的获取与释放方法得以执行。
其次,定义资源数。TwinsLock在同一时刻允许至多两个线程的同时访问,表明同步资源 数为2,这样可以设置初始状态status为2,当一个线程进行获取,status减1,该线程释放,则 status加1,状态的合法范围为0、1和2,其中0表示当前已经有两个线程获取了同步资源,此时
再有其他线程对同步状态进行获取,该线程只能被阻塞。在同步状态变更时,需要使用 compareAndSet(int expect,int update)方法做原子性保障。
最后,组合自定义同步器。前面的章节提到,自定义同步组件通过组合自定义同步器来完
成同步功能,一般情况下自定义同步器会被定义为自定义同步组件的内部类。
TwinsLock(部分)代码如代码清单5-10所示。

代码清单5-10 TwinsLock.java


import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class TwinsLock implements Lock {
	private final Sync sync = new Sync(2);

	private static final class Sync extends AbstractQueuedSynchronizer {
		Sync(int count) {
			if (count <= 0) {
				throw new IllegalArgumentException("count must large than zero.");
			}
			setState(count);
		}

		public int tryAcquireShared(int reduceCount) {
			for (;;) {
				int current = getState();
				int newCount = current - reduceCount;
				if (newCount < 0 || compareAndSetState(current, newCount)) {
					return newCount;
				}
			}
		}

		public boolean tryReleaseShared(int returnCount) {
			for (;;) {
				int current = getState();
				int newCount = current + returnCount;
				if (compareAndSetState(current, newCount)) {
					return true;
				}
			}
		}
	}

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

	public void unlock() {
		sync.releaseShared(1);
	}
	// 其他接口方法略
	
	

	@Override
	public void lockInterruptibly() throws InterruptedException {
		// TODO Auto-generated method stub
		
	}

	@Override
	public boolean tryLock() {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public Condition newCondition() {
		// TODO Auto-generated method stub
		return null;
	}
	
}

在上述示例中,TwinsLock实现了Lock接口,提供了面向使用者的接口,使用者调用lock() 方法获取锁,随后调用unlock()方法释放锁,而同一时刻只能有两个线程同时获取到锁。 TwinsLock同时包含了一个自定义同步器Sync,而该同步器面向线程访问和同步状态控制。以 共享式获取同步状态为例:同步器会先计算出获取后的同步状态,然后通过CAS确保状态的正 确设置,当tryAcquireShared(int reduceCount)方法返回值大于等于0时,当前线程才获取同步状 态,对于上层的TwinsLock而言,则表示当前线程获得了锁。
同步器作为一个桥梁,连接线程访问以及同步状态控制等底层技术与不同并发组件(比如 Lock、CountDownLatch等)的接口语义。
下面编写一个测试来验证TwinsLock是否能按照预期工作。在测试用例中,定义了工作者 线程Worker,该线程在执行过程中获取锁,当获取锁之后使当前线程睡眠1秒(并不释放锁), 随后打印当前线程名称,最后再次睡眠1秒并释放锁,测试用例如代码清单5-11所示。


代码清单5-11 TwinsLockTest.java

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.junit.Test;

public class TwinsLockTest {
	public static void main(String[] args) {
		new ReentrantLock();
		System.out.println("==================");
	}
	@Test
	public void test() {
		final Lock lock = new TwinsLock();
		class Worker extends Thread {
			public Worker(String name){
				super(name);
				
			}
			public void run() {
				int i = 1;
				while ((i--) != 0) {
					lock.lock();
					try {
						SleepUtils.second(1);
						System.out.println(Thread.currentThread().getName());
						SleepUtils.second(1);
					} finally {
						lock.unlock();
					}
				}
			}
		}

		// 启动10个线程
		for (int i = 0; i < 10; i++) {
			Worker w = new Worker("第"+i+"个");
			w.setDaemon(true);
			w.start();
		}
		
		// 每隔1秒换行
		for (int i = 0; i < 100; i++) {
			SleepUtils.second(1);
			System.out.println();
		}
	}
}
import java.util.concurrent.TimeUnit;

public class SleepUtils {
	public static final void second(long seconds) {
		try {
			TimeUnit.SECONDS.sleep(seconds);
		} catch (InterruptedException e) {
			
		}
	}
}
运行结果

第1个
第0个

第5个
第2个



第7个
第3个

第6个
第8个



第4个
第9个

运行该测试用例,可以看到线程名称成对输出,也就是在同一时刻只有两个线程能够获 取到锁,这表明TwinsLock可以按照预期正确工作。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值