High concurrency Semaphore, Exchanger, LockSupport (3)

Semaphore

A semaphore (Semaphore), sometimes called a semaphore, is a facility used in a multi-threaded environment. It is responsible for coordinating various threads to ensure that they can use common resources correctly and reasonably. Semaphore is divided into two types: single-value and multi-value. The former can only be obtained by one thread, and the latter can be obtained by several threads.

Context introduction

Take the operation of a parking lot as an example. For simplicity, assume that there are only three parking spaces in the parking lot, and all three parking spaces are empty at the beginning. This is if five cars come at the same time, and the gatekeeper allows three of them to enter unimpeded, and then puts down the barrier, the remaining cars must wait at the entrance, and all subsequent cars will have to wait at the entrance. At this time, a car left the parking lot. After the gatekeeper learned about it, he opened the car barrier and put in one car. If two cars left, he could put in two more cars, and so on.

In this parking lot system, parking spaces are public resources, each car is like a thread, and the gatekeeper acts as a semaphore. Further, the characteristics of the semaphore are as follows: the semaphore is a non-negative integer (the number of parking spaces), and all threads (vehicles) passing through it will decrease the integer by one (passing it, of course, to use resources), when the integer value is At zero time, all threads attempting to pass through it will be in a waiting state. We define two operations on the semaphore: Wait (wait) and Release (release). When a thread calls the Wait (wait) operation, it either passes through and then decrements the semaphore by one, or waits until the semaphore is greater than one or times out. Release (release) actually performs an add operation on the semaphore, corresponding to the vehicle leaving the parking lot. The reason why this operation is called "release" is that the add operation actually releases the resources guarded by the semaphore.

Use the code example

public class TestSemaphore {
    public static void main(String[] args) {
        //Semaphore s = new Semaphore(2);
        Semaphore s = new Semaphore(2, true);
        //允许一个线程同时执行
        //Semaphore s = new Semaphore(1);

        new Thread(()->{
            try {
                s.acquire();

                System.out.println("T1 running...");
                Thread.sleep(200);
                System.out.println("T1 running...");

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                s.release();
            }
        }).start();

        new Thread(()->{
            try {
                s.acquire();

                System.out.println("T2 running...");
                Thread.sleep(200);
                System.out.println("T2 running...");

                s.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

Exchanger

Exchanger, which allows data to be exchanged between concurrent tasks. Specifically, the Exchanger class allows defining synchronization points between two threads. When both threads reach the synchronization point, they exchange data structures, so the first thread's data structure goes into the second thread, and the second thread's data structure goes into the first thread.

An Exchanger is a fence that exchanges objects between two tasks that each own an object when they enter the fence. When they leave, they both own the object that was previously held by the object. Its typical application scenario is: one task is creating objects, which are expensive to produce, and another task is consuming these objects. In this way, more objects can be consumed while being created.

Application example

The implementation of Exchange is more complicated. Let's see how to use it first, and then analyze its source code. Now we use Exchange to simulate the producer-consumer problem:

public class ExchangerTest {

    static class Producer implements Runnable{

        //生产者、消费者交换的数据结构
        private List<String> buffer;

        //步生产者和消费者的交换对象
        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("生产者第" + i + "次提供");
                for(int j = 1 ; j <= 3 ; j++){
                    System.out.println("生产者装入" + i  + "--" + j);
                    buffer.add("buffer:" + i + "--" + j);
                }

                System.out.println("生产者装满,等待与消费者交换...");
                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++) {
                //调用exchange()与消费者进行数据交换
                try {
                    buffer = exchanger.exchange(buffer);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                System.out.println("消费者第" + i + "次提取");
                for (int j = 1; j <= 3 ; j++) {
                    System.out.println("消费者 : " + 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();
    }
}

print result

生产者第1次提供
生产者装入1--1
生产者装入1--2
生产者装入1--3
生产者装满,等待与消费者交换...
生产者第2次提供
生产者装入2--1
生产者装入2--2
生产者装入2--3
生产者装满,等待与消费者交换...
消费者第1次提取
消费者 : buffer:1--1
消费者 : buffer:1--2
消费者 : buffer:1--3
消费者第2次提取
......

First, the Producer and the Consumer first create a buffer list, and exchange data synchronously through the Exchanger. The consumer obtains data by calling the Exchanger to synchronize with the producer, and the producer stores data in the cache queue through a for loop and uses the exchanger object consumer to synchronize. After the consumer gets the data from the exchanger, there are 3 data in his buffer list, while the producer gets an empty list. The above example fully demonstrates how the consumer-producer uses Exchanger to complete data exchange.

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

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

Realize analysis

The core of the Exchanger algorithm is through a slot that can exchange data, and a participant that can carry data items. 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
      }

The following important member variables are defined in Exchanger:

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

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

slot is a single slot, and arena is an array slot. They are all of type Node. You may feel confused here, the scenario where the slot is used as an Exchanger to exchange data should only need one? Why is there an additional participant and array type arena? A slot exchange should be possible in principle, but this is not the case in reality. When multiple participants use the same exchange, there will be serious scalability problems. Since there is a problem with a single exchange place, then we arrange multiple, that is, an array arena. The array arena is used to arrange different threads to use different slots to reduce competition problems and ensure that data will eventually be exchanged in pairs. However, the Exchanger does not generate an arena array as soon as it comes to reduce competition. It only generates an arena array when competition occurs. So how to bind Node to the current thread? Participant, the role of the Participant is to reserve a unique Node node for each thread, which inherits ThreadLocal, and records the subscript index in the 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: subscript of arena;
  • bound: the last recorded Exchanger.bound;
  • collides: 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 that needs to be exchanged;
  • match: the item passed by the thread doing the releasing operation;
  • parked: Set the thread value when it is suspended, otherwise it is null;

There are two variables worth thinking about in the Node definition: bound and collides. As mentioned above, the array area is created to avoid competition. If there is no competition problem in the system, there is no need to open up an efficient arena to increase the complexity of the system. First, data is exchanged through a single slot exchanger. When competition is detected, slots in different positions will be arranged to save the thread Node, and it can be ensured that no slot will be on the same cache line. How to judge that there will be competition? CAS fails to replace the slot. If it fails, the size of the arena will be expanded by recording the number of conflicts. We will track the value of "bound" during the process of recording conflicts, and will recalculate the number of conflicts when the value of bound is changed. The explanation here may be a bit vague, don’t worry, we have this concept first, and we will elaborate again in arenaExchange later, let’s directly look at the exchange() method.

exchange(V x)

exchange(V x) : Waits for another thread to reach this exchange point (unless the current thread is interrupted), then transfers the given object to this thread, and receives this thread's object. 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 easier to understand: arena is an array slot, if it is null, execute the slotExchange() method, otherwise judge whether the thread is interrupted, if the interrupted value throws InterruptedException, if there is no interruption, execute the arenaExchange() method. The whole set of logic is: if the execution of the slotExchange(Object item, boolean timed, long ns) method fails, execute the arenaExchange(Object item, boolean timed, long ns) method, and finally return the result V.

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

slotExchange(Object item, boolean timed, long ns)

    private final Object slotExchange(Object item, boolean timed, long ns) {
        // 获取当前线程的节点 p
        Node p = participant.get();
        // 当前线程
        Thread t = Thread.currentThread();
        // 线程中断,直接返回
        if (t.isInterrupted())
            return null;
        // 自旋
        for (Node q;;) {
            //slot != null
            if ((q = slot) != null) {
                //尝试CAS替换
                if (U.compareAndSwapObject(this, SLOT, q, null)) {
                    Object v = q.item;      // 当前线程的项,也就是交换的数据
                    q.match = item;         // 做releasing操作的线程传递的项
                    Thread w = q.parked;    // 挂起时设置线程值
                    // 挂起线程不为null,线程挂起
                    if (w != null)
                        U.unpark(w);
                    return v;
                }
                //如果失败了,则创建arena
                //bound 则是上次Exchanger.bound
                if (NCPU > 1 && bound == 0 &&
                        U.compareAndSwapInt(this, BOUND, 0, SEQ))
                    arena = new Node[(FULL + 2) << ASHIFT];
            }
            //如果arena != null,直接返回,进入arenaExchange逻辑处理
            else if (arena != null)
                return null;
            else {
                p.item = item;
                if (U.compareAndSwapObject(this, SLOT, null, p))
                    break;
                p.item = null;
            }
        }

        /*
         * 等待 release
         * 进入spin+block模式
         */
        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 Node through the participant. Check whether it is interrupted, and if it returns null, wait for the InterruptedException to be thrown later.

If the slot is not null, the slot will be eliminated, and the data V will be returned directly if it succeeds. Otherwise, an arena elimination array will be created.

If the slot is null but the arena is not, return null and enter the arenaExchange logic.

If the slot is null and the arena is also null, try to occupy the slot, fail to retry, and successfully jump out of the loop and enter the spin+block (spin+block) mode.

In the spin+block mode, the end time and the number of spins are obtained first. If match (the item passed by the thread doing the releasing operation) is null, it first tries spins+random spins (change the spin to use the hash in the current node and change it) and back down. When the spin number is 0, if the slot changes (slot != p), reset the spin number and try again. Otherwise if: currently uninterrupted & arena is null & (currently not a time-limited version or a time-limited version + the current time is not over): blocking or time-limited blocking. If: the current interruption or arena is not null or the current time-limited version + time has ended: no-time-limited version: set v to null; time-limited version: if the time is over and not interrupted, then TIMED_OUT; otherwise, give null (because the arena is detected non-null or the current thread is interrupted).

Jump out of the loop when match is not empty.

The whole slotExchange is clear and straightforward.

arenaExchange

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 obtain the current node Node through the participant, and then obtain the corresponding node node in the arena according to the index of the current node Node. As mentioned earlier, arena can ensure that different slots will not conflict in the arena, so how is it guaranteed? 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, then the arena array of size 768 is obtained. Then get the nodes in the arena with the following code:

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

Node is still obtained 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 if you want to access the Nth element of type T, your offset should be arrayOffset+N*arrayScale. That is BASE = arrayOffset + 128 . Next, let's look at the definition of the Node node

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

In Java 8, we can use sun.misc.Contended to avoid false sharing. So by adding sun.misc.Contended by <<ASHIFT method, any two available Nodes will not be in the same cache line.

LockSupport

LockSupportIt is a thread blocking tool class. All methods are static methods, which can make threads block at any position. Of course, there must be a wake-up method after blocking.

common method

Next, let's take a look at LockSupportthe commonly used methods. There are two main categories of methods: parkand unpark.

public static void park(Object blocker); // 暂停当前线程
public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间
public static void park(); // 无期限暂停当前线程
public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间
public static void unpark(Thread thread); // 恢复当前线程
public static Object getBlocker(Thread t);

Park English means parking. If we regard Thread as a car, park means to stop the car, and unpark means to let the car start and run.

Write an example to see how to use this tool class

public class LockSupportDemo {

    public static Object u = new Object();
    static ChangeObjectThread t1 = new ChangeObjectThread("t1");
    static ChangeObjectThread t2 = new ChangeObjectThread("t2");

    public static class ChangeObjectThread extends Thread {
        public ChangeObjectThread(String name) {
            super(name);
        }
        @Override public void run() {
            synchronized (u) {
                System.out.println("in " + getName());
                LockSupport.park();
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("被中断了");
                }
                System.out.println("继续执行");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        t1.start();
        Thread.sleep(1000L);
        t2.start();
        Thread.sleep(3000L);
        t1.interrupt();
        LockSupport.unpark(t2);
        t1.join();
        t2.join();
    }
}

The result of the operation is as follows:

in t1
被中断了
继续执行
in t2
继续执行

Here parkand unparkactually realize the function of waitsum notify, but there are still some differences.

  1. parkNo need to acquire a lock on an object
  2. parkBecause no exception will be thrown during the interruption InterruptedException, you need to parkjudge the interruption status by yourself later, and then do additional processing

Let's see again Object blocker, what is this thing? This is actually convenient to see the specific blocking object information when the thread is dumped.

"t1" #10 prio=5 os_prio=31 tid=0x00007f95030cc800 nid=0x4e03 waiting on condition [0x00007000011c9000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304)
    // `下面的这个信息`
    at com.wtuoblist.beyond.concurrent.demo.chapter3.LockSupportDemo$ChangeObjectThread.run(LockSupportDemo.java:23) // 
    - locked <0x0000000795830950> (a java.lang.Object)

Compared with threads stop和resume, park和unparkthe order of priority is not so strict. stop和resumeIf the order is reversed, a deadlock will occur. And park和unparkit doesn't. This is why? or look at an example

public class LockSupportDemo {

    public static Object u = new Object();
    static ChangeObjectThread t1 = new ChangeObjectThread("t1");

    public static class ChangeObjectThread extends Thread {

        public ChangeObjectThread(String name) {
            super(name);
        }

        @Override public void run() {
            synchronized (u) {
                System.out.println("in " + getName());
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                LockSupport.park();
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("被中断了");
                }
                System.out.println("继续执行");
            }
        }
    }

    public static void main(String[] args) {
        t1.start();
        LockSupport.unpark(t1);
        System.out.println("unpark invoked");
    }
}

There is an operation of sleeping for 1s inside t1, so unpark must be called before park, but t1 can still end in the end. This is because park和unparka permit (boolean value) is maintained per thread

  1. When unpark is called, if the current thread has not entered the park, the permission is true
  2. When park is called, judge whether the permission is true, if it is true, continue to execute; if it is false, wait until the permission is true

Learn more JAVA knowledge and skills, pay attention to and private message bloggers (learning) to learn for free JAVA courseware,
source code, installation package, and the latest interview materials of big factories, etc.

Guess you like

Origin blog.csdn.net/m0_67788957/article/details/123781920