Synchronization Tool - Exchanger

Posted by biltong on Fri, 18 Feb 2022 10:40:51 +0100

1. Introduction to Exchanger

Exchanger - Exchanger, JDK1. A synchronizer was introduced at 5th hour, and it can be seen literally that the main function of this class is to exchange data.

Exchanger is a bit like CyclicBarrier, we know that CyclicBarrier is a fence, and threads that reach it need to wait for a certain number of other threads to arrive before they can pass through it.

Exchanger can be seen as a two-way fence, as shown below:

When the Thread1 thread reaches the fence, it first looks to see if any other threads have reached the fence, and if no other threads have arrived, it will exchange the information it carries in pairs, so Exchanger is ideal for data exchange between two threads.

 

2. Exchanger example

Let's take an example to understand what Exchanger does:

Example: Suppose you have a producer and a consumer. If you want to implement the producer-consumer model, the general idea is to use queues as a message queue, and producers keep producing messages, and then join the queue. Consumers continue to cancel interest from the message queue for consumption. If the queue is full, the producer waits, and if the queue is empty, the consumer waits.

Let's see how to implement the producer-messenger model with Exchanger:
Producer:

public class Producer implements Runnable {
    private final Exchanger<Message> exchanger;

    public Producer(Exchanger<Message> exchanger) {
        this.exchanger = exchanger;
    }

    @Override
    public void run() {
        Message message = new Message(null);
        for (int i = 0; i < 3; i++) {
            try {
                Thread.sleep(1000);

                message.setV(String.valueOf(i));
                System.out.println(Thread.currentThread().getName() + ": Production data[" + i + "]");

                message = exchanger.exchange(message);

                System.out.println(Thread.currentThread().getName() + ": Exchange to get data[" + String.valueOf(message.getV()) + "]");

            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

Consumer:

public class Consumer implements Runnable {
    private final Exchanger<Message> exchanger;

    public Consumer(Exchanger<Message> exchanger) {
        this.exchanger = exchanger;
    }

    @Override
    public void run() {
        Message msg = new Message(null);
        while (true) {
            try {
                Thread.sleep(1000);
                msg = exchanger.exchange(msg);
                System.out.println(Thread.currentThread().getName() + ": Consumed data[" + msg.getV() + "]");
                msg.setV(null);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

Main:

public class Main {
    public static void main(String[] args) {
        Exchanger<Message> exchanger = new Exchanger<>();
        Thread t1 = new Thread(new Consumer(exchanger), "Consumer-t1");
        Thread t2 = new Thread(new Producer(exchanger), "Producer-t2");

        t1.start();
        t2.start();
    }
}

Output:

Producer-t2: Production data[0]
Producer-t2: Exchange to get data[null]
Consumer-t1: Consumed data[0]
Producer-t2: Production data[1]
Consumer-t1: Consumed data[1]
Producer-t2: Exchange to get data[null]
Producer-t2: Production data[2]
Consumer-t1: Consumed data[2]
Producer-t2: Exchange to get data[null]

 

In the example above, the producer produced three data: 0, 1, 2. Exchange with consumers. As you can see, empty messages are exchanged to the producer after the consumer has consumed them.

3. Exchanger Principles

Construction of Exchanger

Let's start with the Exchanger construct, which has only one empty constructor:

public Exchanger() {
    participant = new Participant();
}

When constructed, a Participant object is created internally, which is an internal class of Exchanger, essentially a ThreadLocal To save the thread local variable Node:

static final class Participant extends ThreadLocal<Node> {
    public Node initialValue() { return new Node(); }
}

We can think of Node objects as swapping data carried by each thread itself:

@sun.misc.Contended static final class Node {
    int index;              // Arena index
    int bound;              // Last recorded value of Exchanger.bound
    int collides;           // Number of CAS failures at current bound
    int hash;               // Pseudo-random for spins
    Object item;            // This thread's current item
    volatile Object match;  // Item provided by releasing thread
    volatile Thread parked; // Set to this thread when parked, else null
}

Exchanger Unit Exchange

Exchanger has two ways of data exchange. When the concurrency is low, it uses "single slot exchange" internally. Multi-slot switching is used when concurrency is high.

Let's first look at the exchange method:

public V exchange(V x) throws InterruptedException {
        Object v;
        Object item = (x == null) ? NULL_ITEM : x; // translate null args
        if ((arena != null ||
             (v = slotExchange(item, false, 0L)) == null) &&
            ((Thread.interrupted() || // disambiguates null return
              (v = arenaExchange(item, false, 0L)) == null)))
            throw new InterruptedException();
        return (v == NULL_ITEM) ? null : (V)v;
    }

You can see that exchange is actually a way of judging how data is exchanged. Internally, it determines whether slot Exchange or arenaExchange should be used at the current time based on the state of some fields in Exchanger. The flow chart of the whole judgment is as follows:

 

Exchanger's arena field is a Node-type array that represents a slot array and is only used when swapping across multiple slots. In addition, Exchanger has a slot field that represents a single-slot exchange node and is only used for single-slot exchange.

The slot field eventually points to the Node node of the first arrival thread, indicating that the thread occupied the slot.

    //Multi-slot Swap Array
    private volatile Node[] arena;
    //Single slot exchange node
    private volatile Node slot;

Single slot exchange diagram:

 

Let's see how Exchanger implements single-slot swapping. The slotExchange method is not complex. The entry item of slotExchange represents the data carried by the current thread and returns the data normally carried by the paired threads:

/**
 * Single slot switching
 *
 * @param item Data to be exchanged
 * @return Data from other paired threads; Return null if multislot swap is activated or interrupted, TIMED_if timeout OUT (an Obejct object)
 */
private final Object slotExchange(Object item, boolean timed, long ns) {
    Node p = participant.get();         // Exchange Node Carried by Current Thread
    Thread t = Thread.currentThread();
    if (t.isInterrupted())              // Interrupt state check for threads
        return null;

    for (Node q; ; ) {
        if ((q = slot) != null) {       // Slot!= Null, indicating that a thread has arrived first and occupied slot
            if (U.compareAndSwapObject(this, SLOT, q, null)) {
                Object v = q.item;      // Get Exchange Value
                q.match = item;         // Set Exchange Value
                Thread w = q.parked;
                if (w != null)          // Wake up the thread waiting in this slot
                    U.unpark(w);
                return v;               // Exchange succeeded and results returned
            }
            // Create an arena array with more than one CPU core and bound of 0 and set bound to SEQ size
            if (NCPU > 1 && bound == 0 && U.compareAndSwapInt(this, BOUND, 0, SEQ))
                arena = new Node[(FULL + 2) << ASHIFT];
        } else if (arena != null)       // slot == null && arena != null
            // An operation to initialize arena occurred in the middle of single-slot switching, requiring re-routing directly to arena Exchange
            return null;
        else {                          // The slot is occupied if the current thread arrives first
            p.item = item;
            if (U.compareAndSwapObject(this, SLOT, null, p))    // Occupy slot slot slot
                break;
            p.item = null;              // CAS operation failed, continue next spin
        }
    }

    // Execution to this indicates that the current thread arrives first, has occupied the slot slot slot, and needs to wait for the paired thread to arrive
    int h = p.hash;
    long end = timed ? System.nanoTime() + ns : 0L;
    int spins = (NCPU > 1) ? SPINS : 1;             // Number of spins, in relation to the number of CPU cores
    Object v;
    while ((v = p.match) == null) {                 // p.match == null indicates that the paired thread has not arrived yet
        if (spins > 0) {                            // Optimized operation: random release of CPU during spin
            h ^= h << 1;
            h ^= h >>> 3;
            h ^= h << 10;
            if (h == 0)
                h = SPINS | (int) t.getId();
            else if (h < 0 && (--spins & ((SPINS >>> 1) - 1)) == 0)
                Thread.yield();
        } else if (slot != p)                       // Optimize operation: The pairing thread has arrived, but is not fully ready, so it needs to spin a little longer
            spins = SPINS;
        else if (!t.isInterrupted() && arena == null &&
                (!timed || (ns = end - System.nanoTime()) > 0L)) {  //Has been spinning for a long time or can't wait for a pair before blocking the current thread
            U.putObject(t, BLOCKER, this);
            p.parked = t;
            if (slot == p)
                U.park(false, ns);               // Blocking current thread
            p.parked = null;
            U.putObject(t, BLOCKER, null);
        } else if (U.compareAndSwapObject(this, SLOT, p, null)) {   // Timeout or other (cancel) to make slot s for other threads
            v = timed && ns <= 0L && !t.isInterrupted() ? TIMED_OUT : null;
            break;
        }
    }
    U.putOrderedObject(p, MATCH, null);
    p.item = null;
    p.hash = h;
    return v;
}

The overall flow of the above code is roughly as follows:

First arrival thread:

  1. If the current thread is the first one to arrive, the slot field is pointed to its Node node, indicating that the slot is occupied.
  2. Then the thread spins for a while, and if it does not wait for the paired thread to arrive after a period of spin, it enters the blockage. (Spin, instead of blocking directly, is an optimization tool given the overhead of thread context switching)

Pairing threads arriving later:
If the current thread (the paired thread) is not the first to arrive, the slot is already occupied, pointing to the Node node that first arrives at the thread itself. The pairing thread empties the slot and returns the item from Node as the data it exchanges. In addition, the pairing thread stores the data it carries into Node's match field and wakes Node up. The thread that parked points to (that is, the first arrival thread).

The first arrival thread is awakened:
When the thread wakes up, it exits the spin because the match is not empty (it holds the data that the paired thread carries), and the value of the match is returned.

Thread A and Thread B then exchange data without synchronization.

Exchanger Multi-slot Switching

The most complex thing about Exchanger is its arenaExchange. Let's see first when it triggers the exchange.
As we have said before, it is not accurate to say that multi-slot switching is triggered when there is a large amount of concurrency.

There is such a piece of code in slotExchange:

That is, if there are multiple paired threads competing to modify slot slots in a single-slot swap and a thread CAS fails to modify slots, the arena multi-slot array will be initialized and all subsequent swaps will follow arena Exchange:

/**
 * Multislot switching
 *
 * @param item Data to be exchanged
 * @return Data from other paired threads; Return null if interrupted, TIMED_if timed out OUT (an Obejct object)
 */
private final Object arenaExchange(Object item, boolean timed, long ns) {
    Node[] a = arena;
    Node p = participant.get();                     // Exchange Node Carried by Current Thread
    for (int i = p.index; ; ) {                     // arena index of current thread
        int b, m, c;
        long j;

        // Select the element from the arena array with the offset address (i << ASHIFT) + ABASE, which is the really available ode
        Node q = (Node) U.getObjectVolatile(a, j = (i << ASHIFT) + ABASE);

        if (q != null && U.compareAndSwapObject(a, j, q, null)) {   // CASE1: The slot is not empty, indicating that a thread has arrived and is waiting
            Object v = q.item;                     // Gets the value carried by the arrived thread
            q.match = item;                        // Swap values carried by the current thread to threads that have arrived
            Thread w = q.parked;                   // q.parked points to an already arrived thread
            if (w != null)
                U.unpark(w);                       // Wake Up Arrived Threads
            return v;
        } else if (i <= (m = (b = bound) & MMASK) && q == null) {       // CASE2: Valid slot location and empty slot location
            p.item = item;
            if (U.compareAndSwapObject(a, j, null, p)) {            // Occupy this slot, successful
                long end = (timed && m == 0) ? System.nanoTime() + ns : 0L;
                Thread t = Thread.currentThread();
                for (int h = p.hash, spins = SPINS; ; ) {               // Spin for a while to see if any other pairing threads have reached the slot
                    Object v = p.match;
                    if (v != null) {                                    // A paired thread reached the slot
                        U.putOrderedObject(p, MATCH, null);
                        p.item = null;
                        p.hash = h;
                        return v;   // Returns the value exchanged by the pairing thread
                    } else if (spins > 0) {
                        h ^= h << 1;
                        h ^= h >>> 3;
                        h ^= h << 10;
                        if (h == 0)                // initialize hash
                            h = SPINS | (int) t.getId();
                        else if (h < 0 &&          // approx 50% true
                                (--spins & ((SPINS >>> 1) - 1)) == 0)
                            Thread.yield();        // Time to give CPU twice per wait
                    } else if (U.getObjectVolatile(a, j) != p)       // Optimize operation: The pairing thread has arrived, but is not fully ready, so it needs to spin a little longer
                        spins = SPINS;
                    else if (!t.isInterrupted() && m == 0 &&
                            (!timed || (ns = end - System.nanoTime()) > 0L)) {      // Can't wait for the pairing thread, blocking the current thread
                        U.putObject(t, BLOCKER, this);
                        p.parked = t;                           // Node references current thread to wake me up when paired threads arrive
                        if (U.getObjectVolatile(a, j) == p)
                            U.park(false, ns);
                        p.parked = null;
                        U.putObject(t, BLOCKER, null);
                    } else if (U.getObjectVolatile(a, j) == p &&
                            U.compareAndSwapObject(a, j, p, null)) {    // Attempt to reduce the size of arena slot array
                        if (m != 0)                // try to shrink
                            U.compareAndSwapInt(this, BOUND, b, b + SEQ - 1);
                        p.item = null;
                        p.hash = h;
                        i = p.index >>>= 1;        // descend
                        if (Thread.interrupted())
                            return null;
                        if (timed && m == 0 && ns <= 0L)
                            return TIMED_OUT;
                        break;                     // expired; restart
                    }
                }
            } else                                 // Failed to occupy slot
                p.item = null;
        } else {                                   // CASE3: Invalid slot location, capacity expansion required
            if (p.bound != b) {
                p.bound = b;
                p.collides = 0;
                i = (i != m || m == 0) ? m : m - 1;
            } else if ((c = p.collides) < m || m == FULL ||
                    !U.compareAndSwapInt(this, BOUND, b, b + SEQ + 1)) {
                p.collides = c + 1;
                i = (i == 0) ? m : i - 1;          // cyclically traverse
            } else
                i = m + 1;                         // grow
            p.index = i;
        }
    }
}

/**
 * Single slot switching
 *
 * @param item Data to be exchanged
 * @return Data from other paired threads; Return null if multislot swap is activated or interrupted, TIMED_if timeout OUT (an Obejct object)
 */
private final Object slotExchange(Object item, boolean timed, long ns) {
    Node p = participant.get();         // Exchange Node Carried by Current Thread
    Thread t = Thread.currentThread();
    if (t.isInterrupted())              // Interrupt state check for threads
        return null;

    for (Node q; ; ) {
        if ((q = slot) != null) {       // Slot!= Null, indicating that a thread has arrived first and occupied slot
            if (U.compareAndSwapObject(this, SLOT, q, null)) {
                Object v = q.item;      // Get Exchange Value
                q.match = item;         // Set Exchange Value
                Thread w = q.parked;
                if (w != null)          // Wake up the thread waiting in this slot
                    U.unpark(w);
                return v;               // Exchange succeeded and results returned
            }
            // Create an arena array with more than one CPU core and bound of 0 and set bound to SEQ size
            if (NCPU > 1 && bound == 0 && U.compareAndSwapInt(this, BOUND, 0, SEQ))
                arena = new Node[(FULL + 2) << ASHIFT];
        } else if (arena != null)       // slot == null && arena != null
            // An operation to initialize arena occurred in the middle of single-slot switching, requiring re-routing directly to arena Exchange
            return null;
        else {                          // The slot is occupied if the current thread arrives first
            p.item = item;
            if (U.compareAndSwapObject(this, SLOT, null, p))    // Occupy slot slot slot
                break;
            p.item = null;              // CAS operation failed, continue next spin
        }
    }

    // Execution to this indicates that the current thread arrives first, has occupied the slot slot slot, and needs to wait for the paired thread to arrive
    int h = p.hash;
    long end = timed ? System.nanoTime() + ns : 0L;
    int spins = (NCPU > 1) ? SPINS : 1;             // Number of spins, in relation to the number of CPU cores
    Object v;
    while ((v = p.match) == null) {                 // p.match == null indicates that the paired thread has not arrived yet
        if (spins > 0) {                            // Optimized operation: random release of CPU during spin
            h ^= h << 1;
            h ^= h >>> 3;
            h ^= h << 10;
            if (h == 0)
                h = SPINS | (int) t.getId();
            else if (h < 0 && (--spins & ((SPINS >>> 1) - 1)) == 0)
                Thread.yield();
        } else if (slot != p)                       // Optimize operation: The pairing thread has arrived, but is not fully ready, so it needs to spin a little longer
            spins = SPINS;
        else if (!t.isInterrupted() && arena == null &&
                (!timed || (ns = end - System.nanoTime()) > 0L)) {  //Has been spinning for a long time or can't wait for a pair before blocking the current thread
            U.putObject(t, BLOCKER, this);
            p.parked = t;
            if (slot == p)
                U.park(false, ns);               // Blocking current thread
            p.parked = null;
            U.putObject(t, BLOCKER, null);
        } else if (U.compareAndSwapObject(this, SLOT, p, null)) {   // Timeout or other (cancel) to make slot s for other threads
            v = timed && ns <= 0L && !t.isInterrupted() ? TIMED_OUT : null;
            break;
        }
    }
    U.putOrderedObject(p, MATCH, null);
    p.item = null;
    p.hash = h;
    return v;
}

 

The overall flow of the multi-slot exchange method arenaExchange is similar to slotExchange, except that it calculates the hit slots based on the index field in the node Node carried by the current thread's data.

If slots are occupied, there are already threads that arrive first and then process as slotExchange does;

If the slot is valid and null, the current thread is first-come, occupies the slot, and then waits optimally in the order of lock upgrade: spin->yield->block, until the paired thread enters the blockage.

In addition, since arenaExchange makes use of slot arrays, the expansion and reduction of slot arrays are involved and readers can read the source code themselves.

Second, when locating valid slots in the arena array, you need to consider the effect of the cache rows. Since data is exchanged between the cache and memory in units of caching behavior, according to locality, data from adjacent address spaces is loaded onto the same cached data block (cache row) and the array is contiguous (logical, involving virtual memory) memory address space, so multiple slots are loaded onto the same cache row. When a slot changes, all data (including other slots) on the cache line where the slot is located will be invalid and will need to be reloaded from memory, affecting performance.

It is important to note that because different JDK versions have different implementation details within the synchronization tool class, it is critical to understand its design thinking. Exchanger's design is similar to LongAdder in that it improves performance by unlocking + dispersing hot spots, but it feels like JDK1. Exchanger implementations in 8 are more complex, especially for multi-slot swaps, and involve things related to cached rows.

 

Topics: Concurrent Programming