java并发框架

1 java的大部分同步器比如locks,   barriers都是基于一个基础框架用AbstractQueuedSynchronizer来实现的。这个框架为实现同步功能,提供了基本的机制。包括原子维护同步状态,阻塞和唤醒线程,队列操作。

2 同步器需要提供的功能

  • Nonblocking   synchronization   attempts   (for   example,tryLock) as well as blocking versions.
  • Optional  timeouts,  so applications can giveupwaiting.
  • Cancellability   via   interruption,   usually   separated   into   one version of acquire that is cancellable,  andone that  isn't.

 同步器除了提供互斥状态,还需要提供共享状态,例如semaphores,可以允许多个线程获得permit.

并发框架还同时提供了condition,支持监视器的await和signal操作。condition需要和互斥lock关联,他的实现通常和所关联的lock有很大关系。

3 对于jvm内置的synchronized,他的优势是节约空间,因为每个对象都可以作为一个lock。并且在单cpu线程竞争不激烈的情况下有较高的速度。但是对于并发框架而言,更多的应用场景是多核cpu,多线程高并发,synchronized无法满足要求。同步器还需要能够监控到有多少的线程被blocked住。

4 同步器的基本设计思路

An acquire operation proceeds as:

while (synchronization state doesnot allow acquire) {

enqueuecurrent thread if notalready queued;

possibly block current thread;

}

dequeue current threadif it was queued;

 

Anda release operation is:

update synchronization state;

if (state may permit a blocked threadto acquire)

unblock one or more queued threads;

 要实现这个算法,需要三个基本组件:

• Atomically managing synchronization state

• Blocking and unblockingthreads

• Maintaining queues

 

5 同步状态

采用CAS来维护同步状态,进行原子操作。

 

Concrete classes based on AbstractQueuedSynchronizer must  define  methods  tryAcquire  and  tryRelease  in terms of   these   exported   state   methods   in   order   to   control   the   acquire

and   release   operations.     The  tryAcquire  method   must   return true  if   synchronization   was   acquired,   and   the  tryRelease method   must   return  true  if   the   new  synchronization   state   may

allow   future   acquires.

 

6 阻塞和唤醒线程

以前的方法已经废弃使用了Thread.suspend 和Thread.resume,因为它有一个不可解决的竞争问题:

当一个非阻塞方法在线程A调用suspend 之前调用了Thread.resume,那么这个resume无效,这种情况会发生死锁。(请参考http://docs.oracle.com/javase/6/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html

http://blog.csdn.net/dlite/article/details/4218105

http://blog.csdn.net/dlite/article/details/4212915)

一个新版的方法的LockSupport.park挂起线程,LockSupport.unpark唤醒线程。在park之前多次调用unpark,只会唤醒一个park。调用park有可能会马上返回,因为之前有可能遗留了unpark。当没有遗留unpark的时候,park会阻塞当前线程。

 

参见http://www.cjsdn.net/doc/jdk50/java/util/concurrent/locks/LockSupport.html:

用来创建锁定和其他同步类的基本线程阻塞原语。

此类与每个使用它的线程关联,这是一种许可(从 Semaphore 类的意义上说)。如果该许可可用,并且可在进程中使用,则调用 park 将立即返回;否则可能 阻塞。如果许可尚不可用,则可以调用 unpark 使其可用。(但与 Semaphore 不同的是许可不能累积。最多只能有一个。)

park 和 unpark 方法提供了阻塞和解除阻塞线程的有效方法,并且不会遇到导致不被赞成的方法 Thread.suspend 和 Thread.resume 因为以下目的变得不可用的问题:由于许可的存在,调用 park 的线程和另一个试图将其 unpark 的线程之间的竞争将保持活性。此外,如果调用方线程被中断,并且支持超时,则 park 将返回。park 方法还可以在其他任何时间“无缘无故”地返回,因此通常必须在复查返回条件的循环里调用此方法。从这个意义上说,park 是“忙碌等待”的一种优化,并且不会浪费这么多的时间进行自旋,但是必须将它与 unpark 配对使用才更高效。

 

活性的介绍http://hi.baidu.com/lnrfsbjbbibdmvq/item/f30e63f1ccf22310e3e3bdfc

 7 维护队列

并发框架的核心就为阻塞线程维护队列,该队列限制为先进先出队列,不支持基于优先级的同步。

适合这个队列的数据结构是非阻塞的队列,有两个候选:

Mellor-Crummey   and   Scott   (MCS)   locks的变种和Craig,  Landin,  and Hagersten (CLH)  locks的变种。

CLH队列只支持自旋锁,并发框架选择了CLH。因为它更方便用于实现超时机制和取消机制。

另外的原因可以参考

http://hill007299.iteye.com/blog/1806504

CLH原本只用于自旋锁,而在并发框架里面使用的CLH变种队列是支持阻塞\唤醒模式的。原生CLH的节点只有前驱指针,而变种CLH有后继指针,因为节点需要定位到后继节点,以唤醒后继节点进入下一轮自旋。另外维护一个双向链表,节点插入、删除不能保证原子性,CAS不能做到两个指针赋值的原子操作。

所以后继指针只是一个快捷通道,当后继节点不存在的时候,需要从tail节点往前遍历以精确定位到后继节点。

另一个变化是状态字段,比原生CLH有更多的状态,用于维护唤醒标记、取消标记。

尽管变种CLH已经不是完全的自旋锁,原生CLH分散内存区域,提高cpu缓存命中率的特点派不上用场。但是这种数据结构,在阻塞锁里面,可以减少大量不必要的park和unpark,减少用户态和内核态交互产生的开销,无论竞争的线程数量有多少。每次锁的释放,只会有一个后继节点唤醒来竞争锁,这样也减少了内存竞争。

 变种完之后的CLH算法如下:

if (!tryAcquire(arg)) {
node = create and enqueue new node;
pred = node's effective predecessor;
while (pred is not head node ||  !tryAcquire(arg)) {
if (pred's signal bit is set)
park();
else
compareAndSet pred's signal bit to true;
pred = node's e ective predecessor;
}
head = node;
}
And the release operation is:
if (tryRelease(arg) && head node's signal bit is set) {
compareAndSet head's signal bit to false;
unpark head's successor, if one exists
}

 框架采用的是模板方法,把tryAcquire和tryRelease留给具体子类实现,子类只需要实现一个线程是否能够成功拿到锁或者成功释放锁。基类AbstractQueuedSynchronizer提供基本是CAS操作、阻塞\唤醒操作、维护CLH队列。

 8 条件等待ConditionObject

条件对象和锁绑定,一个锁可以有多个条件对象。提供了和内置同步机制await,signal相同的语义,但是更具有灵活性。线程拿到锁之后才能进行await操作,await之后,线程会创建一个节点进入条件队列。当前线程await释放锁之后,其他获得锁的线程可以进行signal,signal会把条件队列的节点移动到同步队列。该线程释放锁之后,原先调用await的线程就可以重新去竞争锁了。可以这么说同步框架结合条件队列和同步队列来实现锁竞争和条件等待的同步机制。

必须注意的一点是,对ConditionObject做任何操作的线程都必须先拿到锁。一个经典场景是生产者和消费者模式,可以参考ArrayBlockingQueue的实现。

 

9 AbstractQueuedSynchronizer提供了基础功能和服务,采用模板方法的模式。具体同步器作为子类只需要实现状态的观察和更新,用于控制acquire和release策略。并且子类同步器对用户是不可见的,他通常被更高层的同步器封装为一个子类和field。高层的同步器把实现委派给AbstractQueuedSynchronizer,对外提供更为合适具体的接口命名。

 

10 公平策略

虽然CLH采用的FIFO队列来实现,但是并不保证竞争锁的完全公平的,因为有些同步器允许新到线程和队列头结点线程进行竞争,来得早不如来得巧,这种机制可以提高效率,减少线程切换的开销,但是可能会带来更高的饥饿概率。也有些同步器可以保证公平性,任何新到的线程都必须先入队再竞争锁,这样会有更多的线程切换开销。这就是公平和效率的权衡,并发框架同时支持这两种策略,由具体的子类同步器去决定。

 

11 具体同步器

ReentrantLock使用同步状态来维护重入的次数,并且防止竞争失败的线程试图release,还提供了ConditionObject用于支持条件等待。另外用户构造ReentrantLock可以选择公平策略。

ReentrantReadWriteLock使用同步状态的前16位维护读的状态,后16位维护写的状态。写锁是互斥锁,实现方式和ReentrantLock相似。而读锁则是使用了acquireShared来允许多个读者。

Semaphore信号量,使用同步状态来维护当前的计数,实现acquireShared来减少计数,当计数为负数时,阻塞线程。用tryRelease来增加计数,当计数为正时会唤醒阻塞线程。

CountDownLatch也是使用同步状态来维护计数,当计数为0时,所有的acquires都可以通过。

SynchronousQueue类似接力棒的生产-消费模式,用同步状态来维护当有消费者take的时候,生产者才能put。

用户可以基于AbstractQueuedSynchronizer实现更多的同步器。

 

 

 

 

 

 

学习材料

http://gee.cs.oswego.edu/dl/papers/aqs.pdf

http://blog.csdn.net/aesop_wubo/article/details/7555956

你可能感兴趣的:(java并发)