上一篇《并发编程原理与实战(十二)并发协同利器Phaser之应用举例与总结》总结了CountDownLatch、CyclicBarrier、Semaphore、Phaser这四个多线程并发协同工具类的特性和使用场景,本文继续学习另一个并发协同利器Exchanger。Exchanger同样是java.util.concurrent包下的一个线程并发协同工具类,主要用于两个线程之间交换数据,本文就来详细学习下这个工具。
认识交换器Exchanger
/**
* A synchronization point at which threads can pair and swap elements
* within pairs. Each thread presents some object on entry to the
* {@link #exchange exchange} method, matches with a partner thread,
* and receives its partner's object on return. An Exchanger may be
* viewed as a bidirectional form of a {@link SynchronousQueue}.
* Exchangers may be useful in applications such as genetic algorithms
* and pipeline designs.
...
*/
Exchanger的字面意思是“交换器”的意思,它提供了一个同步点,两个线程在此交换各自持有的数据。每个线程通过调用exchange()方法呈现某些(数据)对象,然后匹配一个伙伴线程,伙伴线程接收(数据)对象并返回。一个Exchanger可以当成一个双向的阻塞队列,Exchanger通常在管道设计类的应用程序中很有用处。
插槽
/*
* This works great in principle. But in practice, like many
* algorithms centered on atomic updates to a single location, it
* scales horribly when there are more than a few participants
* using the same Exchanger. So the implementation instead uses a
* form of elimination arena, that spreads out this contention by
* arranging that some threads typically use different slots,
* while still ensuring that eventually, any two parties will be
* able to exchange items.
...
*/
在多线程的场景下,多个线程使用同一个插槽是可怕的,所以Exchanger采用了消除争用的实现方式,通过安排不同的线程使用不同的插槽,最终保证两个线程都能够交换数据。插槽可以理解成 Exchanger 内部的一个数据暂存区(通常是一个变量或数组),用于临时存储线程待交换的数据对象。
单槽与多槽
/*
* Implementing an effective arena requires allocating a bunch of
* space, so we only do so upon detecting contention (except on
* uniprocessors, where they wouldn't help, so aren't used).
* Otherwise, exchanges use the single-slot slotExchange method.
* On contention, not only must the slots be in different
* locations, but the locations must not encounter memory
* contention due to being on the same cache line (or more
* generally, the same coherence unit).
...
*/
实现一个有效的多插槽区域(arena)需要分配一堆内存,只有在检测到争用的情况下才会这样做;否则在低并发的情况下,就用一个单一的插槽进行数据交换。在争用(高并发)的情况下,不仅要插槽在不同的位置,而且在这些位置上不能因为连续的单元存在内存争用。在高并发的情况下,通过切换到多个插槽,减少线程之间对插槽的竞争。
碰撞跟踪
高并发情况下,多个线程竞争同一插槽会导致 CAS 操作失败(CAS(Compare-And-Swap) 是一种无锁的原子操作机制,支持实现线程安全的数据更新),即发生“碰撞”。
/*
* The arena starts out with only one used slot. We expand the
* effective arena size by tracking collisions; i.e., failed CASes
* while trying to exchange. By nature of the above algorithm, the
* only kinds of collision that reliably indicate contention are
* when two attempted releases collide -- one of two attempted
* offers can legitimately fail to CAS without indicating
* contention by more than one other thread. (Note: it is possible
* but not worthwhile to more precisely detect contention by
* reading slot values after CAS failures.) When a thread has
* collided at each slot within the current arena bound, it tries
* to expand the arena size by one. We track collisions within
* bounds by using a version (sequence) number on the "bound"
* field, and conservatively reset collision counts when a
* participant notices that bound has been updated (in either
* direction).
*/
多插槽区域通常从一个已经使用的插槽开始,通过跟踪碰撞来扩展有效的插槽区域的大小,例如当交互数据时失败。当一个线程在当前多插槽区域中的每个插槽中发生碰撞,碰撞次数超过阈值,就会尝试将多插槽区域扩大一个(触发多槽扩展),内部通过一个顺序字段来记录碰撞次数,以此来跟踪碰撞。
三阶段等待策略
spin(自旋)->yield(让步)->block(阻塞) 是处理多线程竞争插槽时渐进式的等待策略,旨在平衡 CPU 资源消耗与线程响应速度。
/*
* The effective arena size is reduced (when there is more than
* one slot) by giving up on waiting after a while and trying to
* decrement the arena size on expiration. The value of "a while"
* is an empirical matter. We implement by piggybacking on the
* use of spin->yield->block that is essential for reasonable
* waiting performance anyway -- in a busy exchanger, offers are
* usually almost immediately released, in which case context
* switching on multiprocessors is extremely slow/wasteful.
...
*/
当线程等待了“一会儿”后还没有抢到插槽时,它可能会放弃等待并试图缩小多槽区域的大小,这个“一会儿”是一个经验值,设置等待多长通常根据实际的业务需求来确定。通过spin(自旋)->yield(让步)->block(阻塞) 策略来实现,这个策略对合理的等待是关键的。在一个繁忙的交换器中,交换通常是瞬间释放的,在这种场景下,在多处理器中上下文切换是极其缓慢或者浪费资源的。
1、自旋(Spin)阶段
线程通过CAS操作尝试抢占插槽失败后,先进入短时间(也就是“一会儿”)的忙等待(通常循环数十次),避免立即进入阻塞状态导致上下文切换,因为上下文切换是需要额外的开销的,这个短暂的循环等待通常称为自旋。
2、让步(Yield)阶段
若自旋后仍未能成功抢占插槽,调用主动让出 CPU ,允许其他线程运行。
3、阻塞(Block)阶段
长期竞争抢占插槽失败后,线程通过挂起进入阻塞状态并释放 CPU 资源,其他线程被唤醒后重新尝试交换操作。
讲解了交换器、插槽、单槽与多槽、碰撞跟踪、三阶段等待策略等概念后,下面我们来分析下Exchanger的api。
核心方法
1、构造函数
/**
* Creates a new Exchanger.
*/
public Exchanger() {
participant = new Participant();
}
该类中唯一的一个构造函数,创建一个交换器。
2、exchange()方法
/**
* Waits for another thread to arrive at this exchange point (unless
* the current thread is {@linkplain Thread#interrupt interrupted}),
* and then transfers the given object to it, receiving its object
* in return.
*
* <p>If another thread is already waiting at the exchange point then
* it is resumed for thread scheduling purposes and receives the object
* passed in by the current thread. The current thread returns immediately,
* receiving the object passed to the exchange by that other thread.
*
* <p>If no other thread is already waiting at the exchange then the
* current thread is disabled for thread scheduling purposes and lies
* dormant until one of two things happens:
* <ul>
* <li>Some other thread enters the exchange; or
* <li>Some other thread {@linkplain Thread#interrupt interrupts}
* the current thread.
* </ul>
* <p>If the current thread:
* <ul>
* <li>has its interrupted status set on entry to this method; or
* <li>is {@linkplain Thread#interrupt interrupted} while waiting
* for the exchange,
* </ul>
* then {@link InterruptedException} is thrown and the current thread's
* interrupted status is cleared.
*
* @param x the object to exchange
* @return the object provided by the other thread
* @throws InterruptedException if the current thread was
* interrupted while waiting
*/
@SuppressWarnings("unchecked")
public V exchange(V x) throws InterruptedException {
Object v;
Node[] a;
Object item =

最低0.47元/天 解锁文章
162

被折叠的 条评论
为什么被折叠?



