多线程高并发编程(6) -- Semaphore、Exchanger源码分析

一.Semaphore

  1.概念

  一个计数信号量。在概念上,信号量维持一组许可证。如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它。每个release()添加许可证,潜在地释放阻塞获取方。但是,没有使用实际的许可证对象;Semaphore只保留可用数量的计数,并相应地执行。即一个Semaphore维护了一组permits【许可证】。每次调用acquire()方法都会阻塞,直到获取到许可证。每次调用release()方法都会添加一个许可证,也就是释放一个被阻塞的获取者。但是实际上并不存在这个许可证,Semaphore仅仅是记录可用资源的数量,并且做出对应的行为(有资源就获取,没有资源就阻塞)。

  信号量通常用于限制线程数,而不是访问某些(物理或逻辑)资源。

  应用场景:控制系统的流量,拿到信号量的线程可以进入,否则就等待。通过acquire()和release()获取和释放访问许可。

  • 线程池控制的是线程数量,而信号量控制的是并发数量,虽然说看起来一样,但两者还是有区别的。

  • 信号量类似于锁机制,信号量的调用,当达到数量后,线程还是存在的,只是被挂起了而已。而线程池,同时执行的线程数量是固定的,超过了数量的只能等待。

  在获得项目之前,每个线程必须从信号量获取许可证,以确保某个项目可用。当线程完成该项目后,它将返回到池中,并将许可证返回到信号量,允许另一个线程获取该项目。请注意,当调用acquire()时,不会保持同步锁定,因为这将阻止某个项目返回到池中。信号量封装了限制对池的访问所需的同步,与保持池本身一致性所需的任何同步分开。【即将限制对池的访问和对池中数据的操作所需要的锁分开】。

  信号量被初始化为一个,并且被使用,使得它只有至多一个允许可用,可以用作互斥锁。这通常被称为二进制信号量,因为它只有两个状态:一个许可证可用,或零个许可证可用。当以这种方式使用时,二进制信号量具有属性(与许多Lock实现不同),“锁”可以由除所有者之外的线程释放(因为信号量没有所有权概念)。这在某些专门的上下文中是有用的,例如死锁恢复。

Semaphore(int permits) 创建一个 Semaphore与给定数量的许可证和非公平公平设置。  
Semaphore(int permits, boolean fair) 创建一个 Semaphore与给定数量的许可证和给定的公平设置。

  此类的构造函数可选择接受公平参数。当设置为false时,此类不会保证线程获取许可的顺序。特别是,闯入是允许的,也就是说,一个线程调用acquire()可以提前已经等待线程分配的许可证-在等待线程队列的头部逻辑新的线程将自己【新线程将自己放在等待线程队列的最前面】。当公平设置为真时,信号量保证调用acquire方法的线程被选择以按照它们调用这些方法的顺序获得许可(先进先出; FIFO)【FIFO的顺序是指是依据到达方法内部的执行点的时间,并不是方法执行的时间。】。请注意,FIFO排序必须适用于这些方法中的特定内部执行点。因此,一个线程可以在另一个线程之前调用acquire,但是在另一个线程之后到达排序点,并且类似地从方法返回。另请注意,未定义的tryAcquire方法不符合公平性设置,但将采取任何可用的许可证。【不定时的tryAcquire()方法会任意选取可用的许可证。】【非公平锁可以插队获取运行,公平锁按照线程顺序执行。

  通常,用于控制资源访问的信号量应该被公平地初始化,以确保线程没有被访问资源【确保没有线程因为长时间获取不到许可证而饿死】。当使用信号量进行其他类型的同步控制时,非正常排序的吞吐量优势往往超过公平性。

  2.用法

  线程可以通过acquire()方法获取到一个许可,然后对共享资源进行操作,如果许可集已分配完了,那么线程将进入等待状态,直到其他线程释放许可才有机会再获取许可,线程释放一个许可通过release()方法完成,"许可"将被归还给Semaphore。

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        final Semaphore sp = new Semaphore(3);
        for (int i = 0; i < 7; i++) {
            Runnable runnable = () -> {
                try {
                    sp.acquire();
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                System.out.println("线程" + Thread.currentThread().getName() +
                        "进入,当前已有" + (3 - sp.availablePermits()) + "个并发");
                try {
                    Thread.sleep((long) (Math.random() * 10000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程" + Thread.currentThread().getName() +
                        "即将离开");
                sp.release();
                //下面代码有时候执行不准确,因为其没有和上面的代码合成原子单元
                System.out.println("线程" + Thread.currentThread().getName() +
                        "已离开,当前已有" + (3 - sp.availablePermits()) + "个并发");
            };
            service.execute(runnable);
        }
    }
结果:
线程pool-1-thread-1进入,当前已有1个并发
线程pool-1-thread-2进入,当前已有2个并发
线程pool-1-thread-3进入,当前已有3个并发
线程pool-1-thread-3即将离开
线程pool-1-thread-4进入,当前已有3个并发
线程pool-1-thread-3已离开,当前已有3个并发
线程pool-1-thread-1即将离开
线程pool-1-thread-1已离开,当前已有2个并发
线程pool-1-thread-5进入,当前已有3个并发
线程pool-1-thread-5即将离开
线程pool-1-thread-5已离开,当前已有2个并发
线程pool-1-thread-6进入,当前已有3个并发
线程pool-1-thread-4即将离开
线程pool-1-thread-4已离开,当前已有2个并发
线程pool-1-thread-7进入,当前已有3个并发
线程pool-1-thread-2即将离开
线程pool-1-thread-2已离开,当前已有2个并发
线程pool-1-thread-7即将离开
线程pool-1-thread-7已离开,当前已有1个并发
线程pool-1-thread-6即将离开
线程pool-1-thread-6已离开,当前已有0个并发

  3.acquire解析

acquire() 从该信号量获取许可证,阻止直到可用,或线程为 interrupted 。  
void acquire(int permits) 从该信号量获取给定数量的许可证,阻止直到所有可用,否则线程为 interrupted 。
    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);//调用AQS的acquireSharedInterruptibly
    }
        /**
         * AQS的acquireSharedInterruptibly
         * Acquires in shared mode, aborting if interrupted.  Implemented
         * by first checking interrupt status, then invoking at least once
         * {@link #tryAcquireShared}, returning on success.  Otherwise the
         * thread is queued, possibly repeatedly blocking and unblocking,
         * invoking {@link #tryAcquireShared} until success or the thread
         * is interrupted.
         * 以共享模式获取,如果中断被中止。
         * 实现首先检查中断状态,然后至少调用一次tryacquirered,成功返回。
         * 否则,线程排队,可能会重复阻塞和取消阻塞,
         * 调用tryacquiremred直到成功或线程被打断了。
         */
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())//中断抛出异常
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)//获取失败,加入同步队列等待
            doAcquireSharedInterruptibly(arg);
    }
    //由Semaphore的FairSync或NonfairSync实现,共享模式下资源可以被多个线程通知占用,tryAcquireShared返回int类型,表示还有多少个资源可以同时被占用,用于共享模式下传播唤醒。
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
    //以共享中断模式获取
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);//创建当前线程的节点,并且锁的模型是共享锁,将其添加到AQS CLH队列的末尾
        boolean failed = true;
        try {
            for (;;) {//自旋
                final Node p = node.predecessor();//获得当前节点的前驱节点
                if (p == head) {//是头节点,没有等待节点
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {//获取成功当前节点设置为头节点并传播【传播指的是,同步状态剩余的许可数值不为0,通知后续结点继续获取同步状态】
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //前继节点非head节点,没资源获取,将前继节点状态设置为SIGNAL,通过park挂起node节点的线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);//结束该结点线程的请求
        }
    }
  public void acquire(int permits) throws InterruptedException {
        if (permits < 0) throw new IllegalArgumentException();//数量小于0抛出异常
        sync.acquireSharedInterruptibly(permits);//调用AQS的acquireSharedInterruptibly
    }

  tryAcquireShared:

    static final class FairSync extends Sync {//公平锁获取
        protected int tryAcquireShared(int acquires) {
            for (;;) {//自旋
                //有前驱节点,表示当前线程前面有阻塞线程,当前线程获取失败,先让前节点线程获取运行【比非公平锁获取多了判断前驱节点的操作】
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();//可获取的许可证数量
                int remaining = available - acquires;//剩下的许可证数量
                //如果剩余数量小于0或更新剩余数量成功,返回剩余数量
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
    }
    static final class NonfairSync extends Sync {//非公平锁获取
        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);//调用Semaphore的内部类Sync的nonfairTryAcquireShared
        }
    }
    final int nonfairTryAcquireShared(int acquires) {
        for (;;) {//自旋
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 ||
                compareAndSetState(available, remaining))
                return remaining;
        }
    }

  4.release解析

    public void release() {
        sync.releaseShared(1);//调用AQS的releaseShared
    }
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {//释放同步状态成功
            doReleaseShared();//唤醒同步队列中后继结点的线程
            return true;
        }
        return false;
    }
    protected boolean tryReleaseShared(int arg) {//由Semaphore的Sync实现
        throw new UnsupportedOperationException();
    }
    private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         * 保证释放动作(向同步等待队列尾部)传递,即使没有其他正在进行的
         * 请求或释放动作。如果头节点的后继节点需要唤醒,那么执行唤醒
         * 动作;如果不需要,将头结点的等待状态设置为PROPAGATE保证
         * 唤醒传递。另外,为了防止过程中有新节点进入(队列),这里必
         * 需做循环,所以,和其他unparkSuccessor方法使用方式不一样
         * 的是,如果(头结点)等待状态设置失败,重新检测。
         */
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;//头节点状态
                 // 如果头节点对应的线程是SIGNAL状态,则意味着头
                 //结点的后继结点所对应的线程需要被unpark-唤醒。
                if (ws == Node.SIGNAL) {
                    // 修改头结点对应的线程状态设置为0。失败的话,则继续循环。
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    // 唤醒头结点h的后继结点所对应的线程
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            // 如果头结点发生变化,则继续循环。否则,退出循环。
            if (h == head)                   // loop if head changed
                break;
        }
    }
    //唤醒传入结点的后继结点对应的线程
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
          if (ws < 0)
              compareAndSetWaitStatus(node, ws, 0);
           //拿到后继结点
          Node s = node.next;
          if (s == null || s.waitStatus > 0) {
              s = null;
              for (Node t = tail; t != null && t != node; t = t.prev)
                  if (t.waitStatus <= 0)
                      s = t;
          }
          if (s != null)
              //唤醒该线程
              LockSupport.unpark(s.thread);
    }
    protected final boolean tryReleaseShared(int releases) {
        for (;;) {//自旋
            int current = getState();//获取当前同步状态
            int next = current + releases;//状态+1
            if (next < current) // overflow
                throw new Error("Maximum permit count exceeded");
            if (compareAndSetState(current, next))//更新状态成功返回true
                return true;
        }
    }

二.Exchanger

  1.概念

  线程可以在成对内配对和交换元素的同步点。每个线程在输入exchange方法时提供一些对象,与合作者线程匹配,并在返回时接收其合作伙伴的对象。交换器可以被视为一个的双向形式SynchronousQueue。交换器在诸如遗传算法和管道设计的应用中可能是有用的。

  一个用于两个工作线程之间交换数据的封装工具类,简单说就是一个线程在完成一定的事务后想与另一个线程交换数据,则第一个先拿出数据的线程会一直等待第二个线程,直到第二个线程拿着数据到来时才能彼此交换对应数据。
 

  2.用法

  Exchanger<V> 泛型类型,其中 V 表示可交换的数据类型

  • V exchange(V v):等待另一个线程到达此交换点(除非当前线程被中断),然后将给定的对象传送给该线程,并接收该线程的对象。

  • V exchange(V v, long timeout, TimeUnit unit):等待另一个线程到达此交换点(除非当前线程被中断或超出了指定的等待时间),然后将给定的对象传送给该线程,并接收该线程的对象。

        Exchanger<Integer> exchanger = new Exchanger<>();
        ExecutorService executor = Executors.newCachedThreadPool();
        Runnable run = () ->{
            try {
                int num = new Random().nextInt(10);
                System.out.println(Thread.currentThread().getName()+"开始交换数据:"+num);
                num = exchanger.exchange(num);//交换数据并得到交换后的数据
                System.out.println(Thread.currentThread().getName()+"交换数据结束后的数据:"+num);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        executor.execute(run);
        executor.execute(run);
        executor.shutdown();
结果:
pool-1-thread-2开始交换数据:9
pool-1-thread-1开始交换数据:8
pool-1-thread-2交换数据结束后的数据:8
pool-1-thread-1交换数据结束后的数据:9

  3.exchange源码解析 参考下面的博客,写的很详细

  • https://blog.csdn.net/qq_31865983/article/details/105620881
  • https://www.xuebuyuan.com/2736097.html
posted @ 2020-04-23 10:32  码猿手  阅读(597)  评论(0编辑  收藏  举报
Live2D