我的家园

我的家园

JAVA.util.concurrent 同步框架(翻译三)

Posted on 2012-04-15 16:27 zljpp 阅读(109) 评论(0)  编辑  收藏

 

接上一篇: 

http://caoyaojun1988-163-com.iteye.com/blog/1290759

 

4、运用:

AbstractQueuedSynchronizer类将上述功能联系在一起,作为一个“模板方法模式[6]”中的模板类,作为其他同步器的基类。子类只是实现预定义方法,实现通过获取锁和释放锁的操作来检查和更新状态。然而,AbstractQueuedSynchronizer的子类本身不可用于ADTS,因为这些类暴露的用于内部控制获取和释放的策略的方法,应该对用户不可见。所有java.util.concurrent包中的同步类声明了一个私有的内部AbstractQueuedSynchronizer子类用于委托它的所有同步方法。这也使得public方法可以根据不太的同步器给予适当的名称。

 

例如,假设最小的Mutex类,当同步状态为0意味着解锁,为1意味着锁定。这个类的同步方法不需要参数值,因此直接默认使用零。

 

 

class Mutex {
	class Sync extends AbstractQueuedSynchronizer {
		public boolean tryAcquire(int ignore) {
			return compareAndSetState(0, 1);
		}
		public boolean tryRelease(int ignore) {
			setState(0); 
			return true;
		}
	}
	private final Sync sync = new Sync();
	public void lock() { sync.acquire(0); }
	public void unlock() { sync.release(0); }
}
 

 

此示例的更全面的版本,以及与其他使用文档,可以在J2SE文档中找到,当然还有许多变种。例如,tryAcquire可以使用“test-and-test-and-set“改变值之前检查状态值。

 

这可能是令人惊讶的,使用委托(delegation )和虚方法(virtual methods),来构造性能敏感的互斥锁, 然而,各种面向对象的设计,动态编译器早已非常成熟;当同步器频繁调用的是。他们都可以很好的优化掉这个开销;AbstractQueuedSynchronizer类还提供了一些方法,可以设置同步类的控制策略。例如,基于acquire方法的超时和中断的版本、独占模式的同步器、锁。AbstractQueuedSynchronizer类也提供了一系列的方法(如acquireShared),他们与tryAcquireShared和tryReleaseShared方法不同,可以通知框架(通过其返回值)将来的acquires方法是否可以成功;最终可以实现级联信号唤醒多个线程。

 

尽管序列化(永久存储或传输)同步器通常是不明智的,但是由于这些类通常用于构建其他类,如线程安全的集合。所以通常实现序列化。AbstractQueuedSynchronizer和ConditionObject类提供的方法来序列化同步状态,而不是被阻塞线程的底层或其他本质上临时状态。即便如此,大多数的同步类反序列化时只是复位同步状态作为初始值,与内置锁反序列化时总是设置为解锁状态的隐含的政策相同。虽然不是必须的,但仍明确支持final域的反序列化。

 

4.1 公平性

 

虽然他们是基于FIFO队列,同步器不一定是公平的。可以注意到,在基本的acquire算法中(3.3节),tryAcquire是再排队前进行检查。因此,新请求获取锁(acquiring)的线程可以优先于队列的头部第一个节点对应的线程。

 

这虽然破坏了FIFO的策略,但是也有普遍高于其他技术的总吞吐量。这减少了有锁可以用,但是因为预定的下一个线程还在唤醒(unblocking)的过程中,所以还没有线程获取到锁的时间,同时,它通过只允许一个(队列的第一个)线程唤醒,避免了过度的、无用的、竞争;可以实现自己的tryAcquire方法,在交回控制权前简单的多试几次,这样可以加剧不公平性, 如果有需要,开发者可以自己创建一个自己的简单持有的同步器;

 

FIFO同步器是相对最公平的锁;即便它会被打破,一个等待唤醒的(unpark)的线程与所有打破规则进入的线程都有一个公平的竞争机会,如果失败它会重新阻塞;当然,如果闯入的线程比获取一个等待唤醒的线程唤醒到达的快,队列中第一个线程几乎没有赢的概率,所以几乎总是reblock, 简单持有的同步器,通常用于多个闯入线程和多个等待唤醒的线程在多处理器的场景,此时队列中的第一个线程被唤醒;如下图所示,既要维持一个或多个线程时处理器的利用率,同时也要避免饥饿。


 

如果需要更加公平的策略,也相对比较简单。程序员可以自己定义 tryAcquire,如果不是队列的head节点就失败(返回false),达到严格的公平;可以通过少数提供的检查方法之一getFirstQueuedThread,来检查是否是第一个节点。

 

一个更快,更严格的变种是如果队列(暂时)是空,也让tryAcquire成功,在这种情况下,如果多个线程遇到一个空队列的情况下竞争,其中至少有一个及第一个得到执行权的线程不需要入队。这一策略使得在所有java.util.concurrent中的同步器,支持“公平”的模式。

 

虽然在实践中往往希望公平,但是即便设置公平也不会得到保证,因为Java语言规范并不提供调度保证。例如,即使有一个严格意义上的公平同步,如果他们从来不需要阻止等待对方,JVM可以纯粹按照顺序选择运行线程,而实际上,在单处理器的环境中,这些线程在被上下文切换之前可以每次运行一个时间片。如果有一个线程持有一个互斥锁,它必须暂时先得到时间片才能释放锁,不然就阻塞那些需要锁的线程,这导致延长了同步器是可用的,但是不能被线程获取到的时间;使用公平的同步器往往在多处理器有很好的表现,因为这样有更好的并发,因此出现竞争的几率更多。

 

即使他们在高竞争的场景下,性能不是很理想,但是公平锁任然可以工作的很好,同时也保持编码的简洁。例如:当维护相对较长的代码或者延长锁间的时间间隔,在这种情况下,可以提示一定的性能,但是饥饿的风险更大。同步框架将最终的决定权交由用户。

 

4.2 同步器

 

这里描述一个草图关于怎样使用这个框架定义java.util.concurrent中的同步器;ReentrantLock类使用同步状态(递归)维护锁的数量。在获取锁的时候,它会记录当前线程的ID,并且递归检查是否有异常线程获取锁导致的非法的状态异常。该类也提供ConditionObject,暴露它的监测和检查方法。同时支持“公平”的模式的选项,在内部通过实现两个不同AbstractQueuedSynchronizer的子类(公平的实现不允许打破规则),并为每个ReentrantLock的实例选择以个合适的实现。

 

ReentrantReadWriteLock类使用16位的同步状态持有写锁定计数,其余16位持有读锁计数。WriteLock使用与ReentrantLock同样的结构。ReadLock使用acquireShared的方法,以允许多个读者。

 

Semaphore类(一个计数信号)使用同步状态保持当前计数。它定义acquireShared方法递减计数,如果为负数了就阻塞当前线程;同样使用tryRelease递增计数,如果是正数的时候,就唤醒线程。

 

CountDownLatch类使用同步状态代表计数。当减到零的时候所有所有获取锁的操作全部成功返回。

 

FutureTask类使用同步状态来代表future 的运行状态(初始,运行,注销,完成)。调用释放(release)来设置或取消一个future ,等待线程处理完成,acquire返回唤醒线程。

 

SynchronousQueue类(一个CSP风格的切换)使用内部等待节点调整生产者和消费者;它采用了同步状态来作为标识,当消费者改变状态的时候允许生产者继续处理,反之亦然。

 

java.util.concurrent包的用户当然可以为自己的应用程序定义自己的同步器;例如,在那些经常遇到,但是包里没有提供的各种Win32事件的语义类,二进制闩锁,锁集中管理,并基于树的壁垒。

 

原文见第一篇的附件

下一篇:http://caoyaojun1988-163-com.iteye.com/blog/1315089






只有注册用户登录后才能发表评论。


网站导航: