[big data Java foundation - Java concurrency 05] concurrency tool class of J.U.C: Exchanger

Exchange is the simplest and most complex. The simplicity lies in that the API is very simple. For one constructor and two exchange() methods, the most complex is that its implementation is the most complex (I was stunned anyway).

The API describes the synchronization point of threads that can pair and exchange elements in pairs. Each thread presents a method on the entry to the exchange method, matches the partner thread, and receives the object of its partner on return. Exchange may be regarded as a two-way form of synchronous queue. Exchange can be useful in applications such as genetic algorithms and pipeline design.

Exchange, which allows data to be exchanged between concurrent tasks. Specifically, the exchange class allows you to define synchronization points between two threads. When both threads reach the synchronization point, they exchange data structures, so the data structure of the first thread enters the second thread, and the data structure of the second thread enters the first thread.

Application example

The implementation of Exchange is complex. Let's see how to use it first, and then analyze its source code. Now let's use Exchange to simulate production consumer problems:

public class ExchangerTest {

    static class Producer implements Runnable{

        //Data structure of producer consumer exchange
        private List<String> buffer;

        //The exchange object between producers and consumers
        private Exchanger<List<String>> exchanger;

        Producer(List<String> buffer,Exchanger<List<String>> exchanger){
            this.buffer = buffer;
            this.exchanger = exchanger;
        }

        @Override
        public void run() {
            for(int i = 1 ; i < 5 ; i++){
                System.out.println("Producer section" + i + "Secondary provision");
                for(int j = 1 ; j <= 3 ; j++){
                    System.out.println("Producer load" + i  + "--" + j);
                    buffer.add("buffer: " + i + "--" + j);
                }

                System.out.println("Producers are full, waiting to exchange with consumers...");
                try {
                    exchanger.exchange(buffer);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class Consumer implements Runnable {
        private List<String> buffer;

        private final Exchanger<List<String>> exchanger;

        public Consumer(List<String> buffer, Exchanger<List<String>> exchanger) {
            this.buffer = buffer;
            this.exchanger = exchanger;
        }

        @Override
        public void run() {
            for (int i = 1; i < 5; i++) {
                //Call exchange() to exchange data with the consumer
                try {
                    buffer = exchanger.exchange(buffer);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("Consumer section" + i + "Secondary extraction");
                for (int j = 1; j <= 3 ; j++) {
                    System.out.println("consumer : " + buffer.get(0));
                    buffer.remove(0);
                }
            }
        }
    }

    public static void main(String[] args){
        List<String> buffer1 = new ArrayList<String>();
        List<String> buffer2 = new ArrayList<String>();

        Exchanger<List<String>> exchanger = new Exchanger<List<String>>();

        Thread producerThread = new Thread(new Producer(buffer1,exchanger));
        Thread consumerThread = new Thread(new Consumer(buffer2,exchanger));

        producerThread.start();
        consumerThread.start();
    }
}

Operation results:

  First, producers and consumers create a buffer list to exchange data synchronously through exchange. In consumption, exchange is called to synchronize with the Producer to obtain data, while the Producer stores data to the cache queue through the for loop and uses the exchange object to synchronize with the Consumer. When the Consumer gets the data from the exchange, there are three data in his buffer list, while the Producer gets an empty list. The above example fully shows how consumers producers use exchange to complete data exchange.

In exchange, if a thread has reached the exchange node, there are three situations for its partner node:

1. If its partner node has called the exchange method before the thread arrives, it will wake up its partner and then exchange data to get their respective data returns.
2. If its partner node has not reached the exchange point, the thread will be suspended and wait for its partner node to wake up to complete the data exchange.
3. If the current thread is interrupted, an exception will be thrown, or if the wait times out, a timeout exception will be thrown.

Implementation analysis

The core of the exchange algorithm is through a slot that can exchange data and a participant that can have data item s. The description in the source code is as follows:

 for (;;) {
        if (slot is empty) {                       // offer
          place item in a Node;
          if (can CAS slot from empty to node) {
            wait for release;
            return matching item in node;
          }
        }
        else if (can CAS slot from node to empty) { // release
          get the item in node;
          set matching item in node;
          release waiting thread;
        }
        // else retry on CAS failure
      }

Several important member variables are defined in exchange:

private final Participant participant;
private volatile Node[] arena;
private volatile Node slot;

The role of participant is to reserve a unique Node for each thread.

Slot is a single slot and arena is an array slot. They are all Node types. You may feel puzzled here. As a scenario of exchange data exchange, you should only need one slot? Why is there a participant and an array type arena? A slot exchange site should be possible in principle, but this is not the case in practice. When multiple participants use the same exchange site, there will be serious scalability problems. Since there are problems with a single exchange site, we will arrange multiple, that is, array arena. The array arena is used to arrange different threads to use different slots to reduce the competition problem, and it can ensure that the data will be exchanged in pairs in the end. However, exchange does not generate arena arrays to reduce contention. Arena arrays are generated only when contention occurs. So how to bind the Node to the current thread? Participant. The role of participant is to reserve a unique Node node for each thread. It inherits ThreadLocal and records the subscript index in arena in the Node node.

Node is defined as follows:

@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
    }
  • index: the subscript of arena;
  • Bound: the last recorded exchange.bound;
  • Collades: the number of CAS failures under the current bound;
  • hash: pseudo-random number, used for spin;
  • Item: the current item of this thread, that is, the data to be exchanged;
  • match: the item passed by the thread performing the releasing operation;
  • parked: set the thread value when suspending, otherwise it is null;

There are two variables worth thinking about in the Node definition: bound and aggregates. As mentioned earlier, array area is produced to avoid competition. If there is no competition problem in the system, it is absolutely unnecessary to open up an efficient area to increase the complexity of the system. First, exchange data through the exchanger of a single slot. When competition is detected, slots in different locations will be arranged to save thread nodes, and it can be ensured that no slots will be on the same cache line. How to judge whether there will be competition? CAS fails to replace slot. If it fails, the size of arena will be expanded by recording the number of conflicts. During the process of recording conflicts, we will track the value of "bound" and recalculate the number of conflicts when the value of bound is changed. The explanation here may be a little vague. Don't worry. Let's have this concept first and elaborate again in arena exchange later.

Let's look directly at the exchange() method

exchange(V x)

exchange(V x): wait for another thread to reach the exchange point (unless the current thread is interrupted), then transfer the given object to the thread and receive the object of the thread. The method is defined as follows:

 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;
    }

This method is easy to understand: arena is an array slot. If it is null, execute the slotExchange() method. Otherwise, judge whether the thread is interrupted. If the interrupt value throws an InterruptedException exception, if there is no interrupt, execute the arenasexchange () method. The whole logic is: if the execution of the slotExchange(Object item, boolean timed, long ns) method fails, execute the arena exchange (object item, Boolean timed, long NS) method, and finally return the result V.

NULL_ITEM is an empty node, which is actually an Object object Object. slotExchange() is a single slot exchange.

slotExchange(Object item, boolean timed, long ns)

 private final Object slotExchange(Object item, boolean timed, long ns) {
        // Gets the node p of the current thread
        Node p = participant.get();
        // Current thread
        Thread t = Thread.currentThread();
        // Thread interrupt, direct return
        if (t.isInterrupted())
            return null;
        // spin
        for (Node q;;) {
            //slot != null
            if ((q = slot) != null) {
                //Try CAS replacement
                if (U.compareAndSwapObject(this, SLOT, q, null)) {
                    Object v = q.item;      // The item of the current thread, that is, the data exchanged
                    q.match = item;         // The item passed by the thread performing the releasing operation
                    Thread w = q.parked;    // Set thread value on suspend
                    // The suspended thread is not null, the thread is suspended
                    if (w != null)
                        U.unpark(w);
                    return v;
                }
                //If it fails, create arena
                //Bound is the last exchange.bound
                if (NCPU > 1 && bound == 0 &&
                        U.compareAndSwapInt(this, BOUND, 0, SEQ))
                    arena = new Node[(FULL + 2) << ASHIFT];
            }
            //If arena= Null, return directly and enter arena exchange logic processing
            else if (arena != null)
                return null;
            else {
                p.item = item;
                if (U.compareAndSwapObject(this, SLOT, null, p))
                    break;
                p.item = null;
            }
        }

        /*
         * Waiting for release
         * Enter spin+block mode
         */
        int h = p.hash;
        long end = timed ? System.nanoTime() + ns : 0L;
        int spins = (NCPU > 1) ? SPINS : 1;
        Object v;
        while ((v = p.match) == null) {
            if (spins > 0) {
                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)
                spins = SPINS;
            else if (!t.isInterrupted() && arena == null &&
                    (!timed || (ns = end - System.nanoTime()) > 0L)) {
                U.putObject(t, BLOCKER, this);
                p.parked = t;
                if (slot == p)
                    U.park(false, ns);
                p.parked = null;
                U.putObject(t, BLOCKER, null);
            }
            else if (U.compareAndSwapObject(this, SLOT, p, null)) {
                v = timed && ns <= 0L && !t.isInterrupted() ? TIMED_OUT : null;
                break;
            }
        }
        U.putOrderedObject(p, MATCH, null);
        p.item = null;
        p.hash = h;
        return v;
    }
  • The program first obtains the current thread Node through participant. Check whether to interrupt. If the interrupt returns null, wait for the InterruptedException exception to be thrown later.
  • If the slot is not null, the slot is eliminated, and the data V is returned directly after success. Otherwise, if it fails, an arena elimination array is created.
  • If the slot is null but arena is not null, null will be returned to enter the arena exchange logic.
  • If the slot is null and arena is null, try to occupy the slot. If it fails, try again. If it succeeds, jump out of the loop and enter spin+block mode.
     

In spin + blocking mode, the end time and spin times are obtained first. If match (the item passed by the thread doing the releasing operation) is null, it first attempts spins + random secondary spin (change the spin to use the hash in the current node and change it) and concession. When the spin number is 0, if the slot changes (slot! = P), reset the spin number and try again. Otherwise, if: the current is not interrupted & arena is null & (the current is not a time limited version or a time limited version + the current time is not over): blocking or time limited blocking. If: the current interrupt or arena is not null or the current time limited version + time has ended: the time unlimited version: set v to null; Time limited version: timed if the time ends and is not interrupted_ OUT; Otherwise, null is given (because arena is detected to be non empty or the current thread is interrupted).

Jump out of loop when match is not empty.

The whole slotExchange is clear.

arenaExchange(Object item, boolean timed, long ns)

private final Object arenaExchange(Object item, boolean timed, long ns) {
        Node[] a = arena;
        Node p = participant.get();
        for (int i = p.index;;) {                      // access slot at i
            int b, m, c; long j;                       // j is raw array offset
            Node q = (Node)U.getObjectVolatile(a, j = (i << ASHIFT) + ABASE);
            if (q != null && U.compareAndSwapObject(a, j, q, null)) {
                Object v = q.item;                     // release
                q.match = item;
                Thread w = q.parked;
                if (w != null)
                    U.unpark(w);
                return v;
            }
            else if (i <= (m = (b = bound) & MMASK) && q == null) {
                p.item = item;                         // offer
                if (U.compareAndSwapObject(a, j, null, p)) {
                    long end = (timed && m == 0) ? System.nanoTime() + ns : 0L;
                    Thread t = Thread.currentThread(); // wait
                    for (int h = p.hash, spins = SPINS;;) {
                        Object v = p.match;
                        if (v != null) {
                            U.putOrderedObject(p, MATCH, null);
                            p.item = null;             // clear for next use
                            p.hash = h;
                            return v;
                        }
                        else if (spins > 0) {
                            h ^= h << 1; h ^= h >>> 3; h ^= h << 10; // xorshift
                            if (h == 0)                // initialize hash
                                h = SPINS | (int)t.getId();
                            else if (h < 0 &&          // approx 50% true
                                     (--spins & ((SPINS >>> 1) - 1)) == 0)
                                Thread.yield();        // two yields per wait
                        }
                        else if (U.getObjectVolatile(a, j) != p)
                            spins = SPINS;       // releaser hasn't set match yet
                        else if (!t.isInterrupted() && m == 0 &&
                                 (!timed ||
                                  (ns = end - System.nanoTime()) > 0L)) {
                            U.putObject(t, BLOCKER, this); // emulate LockSupport
                            p.parked = t;              // minimize window
                            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)) {
                            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
                    p.item = null;                     // clear offer
            }
            else {
                if (p.bound != b) {                    // stale; reset
                    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;
            }
        }
    }

First, get the current node through participant, and then get the corresponding node in arena according to the index of the current node. As mentioned earlier, arena can ensure that different slot s will not conflict in arena. How do you ensure that? Let's first look at the creation of arena:

arena = new Node[(FULL + 2) << ASHIFT];

How big is this arena? Let's first look at the definitions of FULL and ASHIFT:

static final int FULL = (NCPU >= (MMASK << 1)) ? MMASK : NCPU >>> 1;
private static final int ASHIFT = 7;

private static final int NCPU = Runtime.getRuntime().availableProcessors();
private static final int MMASK = 0xff;		// 255

If my machine NCPU = 8, I get an arena array of size 768. Then obtain the nodes in arena through the following code:

 Node q = (Node)U.getObjectVolatile(a, j = (i << ASHIFT) + ABASE);

It still obtains the Node by shifting the ASHIFT bit to the right. ABASE is defined as follows:

Class<?> ak = Node[].class;
ABASE = U.arrayBaseOffset(ak) + (1 << ASHIFT);

U.arrayBaseOffset obtains the length of the object header, and the size of the array element can be obtained through the unsafe.arrayIndexScale(T[].class) method. This means that to access the nth element of type T, your offset should be arrayOffset+N*arrayScale. That is, BASE = arrayOffset+ 128. Secondly, let's look at the definition of Node

 @sun.misc.Contended static final class Node{
	....
  }

In Java 8, we can use sun.misc.contented to avoid pseudo sharing. Therefore, sun.misc.contented is added through < < ashift, so that any two available nodes will not be in the same cache line.

Let's go back to arena exchange (). After obtaining the node node in arena, if the located node q is not empty and the CAS operation is successful, exchange data, return the exchanged data and wake up the waiting thread.

If q equals null and the subscript is within the bound & mmask range, try to occupy the position. If successful, wait for data exchange in the way of spin + blocking.

If the subscript is not within the bound & mmask range, when the competition fails because q is not null: eliminate P. If the added bound is not equal to the current node's bond (b! = p.bound), update p.bound = b, aggregates = 0, i = m or m - 1. If the number of conflicts is less than m, obtaining that M has been the maximum value or modifying the value of the current bound fails, increase the collides once and decrease the value of subscript i circularly; Otherwise, the current bound value is updated successfully: we make I m+1, that is, the maximum subscript at this time. Finally, update the current index value.

The use and principle of exchange are easy to understand, but the source code looks really complex and difficult to understand. However, Doug Lea will also mention the idea of this exchange in subsequent blog posts, such as SynchronousQueue and LinkedTransferQueue.

Finally, end this blog with a paragraph you see on the Internet( http://brokendreams.iteye.com/blog/2253956 ), the blogger made a little modification to make it more consistent with the exchange in the 1.8 environment:
 

In fact, it means that "I" and "you" (there may be multiple "I" and multiple "you") make a transaction in a place called Slot (pay with one hand and deliver with the other). The process is divided into the following steps:

  1. I first went to a trading place called Slot to trade and found that you had arrived. Then I tried to call you to trade. If you responded to me and decided to trade with me, then go to step 2; If others call you away first, I will enter step 5.
  2. I take out the money and give it to you. You may receive my money and give me the goods. The transaction is over; Maybe I pay too slowly (Overtime) or answer the phone (interruption). TM doesn't sell. If I leave, I can only find someone else to buy goods (from scratch).
  3. When I arrive at the trading place and you are not there, I will try to occupy this trading point (making a stool...). If I succeed in seizing a single room (trading point), I will sit here and wait for you to get the goods for trading, and enter step 4; If someone grabs my seat, I can only find another place. Go to step 5.
  4. You come with the goods, call me to trade, and then complete the trade; Or maybe I waited for a long time and you didn't come. I didn't wait. I continued to trade with others. When I left, I took a look. There were not many people. I made so many single rooms (trading place Slot). It was too TM wasteful. I called the trading place Administrator: there were not many people in total. I made so many single rooms dry hair. Withdraw one for my brother!. Then find someone else to buy goods (from scratch); Or my boss called me and didn't let me buy goods (interrupted).
  5. I ran to call the administrator, NIMA, just one pit and one hair. Then the administrator opened up many single rooms in a more open place, and then I looked at each single room one by one to see if there was anyone. If someone, I ask him if he can trade. If he responds to me, I go to step 2. If I have no one, I will occupy this single room and wait for others to trade. Enter step 4.
  6. If I tried several times without success, I would think, is this single room feng shui I TM chose bad? No, we have to move on (from the beginning); If I try many times and find that I haven't succeeded, I'm angry and call the administrator: open another slot for my brother and add a stool. So many people have so many broken stools. Who can use them!
     

Tags: Java C Big Data

Posted on Wed, 27 Oct 2021 12:30:08 -0400 by rulian